The OpenApi3 Client

Accessing ESI

Django-ESI aims to provide a convenience wrapper around AIOpenAPI3

  • A Bravado Style Wrapper providing convenient Endpoint.results() functions and more

  • Access to the Raw “Sad Smiley” Interface from AIOpenAPI3

  • Python Stubs for easier development

Getting a client object

All access to ESI happens through a dynamically generated Client. ESIClientProvider

It is strongly recommended to re-use this Client wherever possible. Generating a new client is slow and in concurrent environments could lead to memory leaks.

Important notes on memory use

The new client provider can use substantially more memory than the old swagger client. This is due to the in memory pydantic models that are being created. There are now parameters for your client to only load the methods or tags that it needs, testing has shown this to be in the realm of up to 90mb’s of wasted RAM use per client without filtering enabled. This will add up very fast with multiple apps running different clients.

Failing to use a Tag or an Operation will throw an AttributeError(“No tag/path filtering supplied to ESI Client.”). If settings.DEBUG=True this exception is not thrown and a warning is printed to the logger, this is useful for tests, development and stub generation.

Example for creating a Provider

Your provider should be instantiated at import time, usually in a providers.py then imported to the other modules that need a client, or results.

Below are examples of creating some clients with both operation and tag filters and some helper functions to use the endpoints, ultimately how you do this is up to you.

Client with a specific endpoints

This example creates a client with only GetAlliances and GetAlliancesAllianceId operations.

# providers.py
from esi.openapi_clients import ESIClientProvider
# Generate a client
esi = ESIClientProvider(
    compatibility_date = "2025-07-23",
    ua_appname = "MyProject",
    ua_version = "0.0.1a1",
    operations=["GetAlliances", "GetAlliancesAllianceId"]
)

def get_alliances():
    operation = esi.client.Alliance.GetAlliances()
    alliances = operation.results()
    return alliances

def get_alliances_alliance_id(alliance_id):
    operation = esi.client.Alliance.GetAlliancesAllianceId(alliance_id=alliance_id)
    detail = operation.result()
    return detail

# tasks.py / views.py
from .providers import get_alliances
...

Client with a single Tag

This example creates a client with all operations under the Alliance Tag

# providers.py
from esi.openapi_clients import ESIClientProvider
# Generate a client
esi = ESIClientProvider(
    compatibility_date = "2025-07-23",
    ua_appname = "MyProject",
    ua_version = "0.0.1a1",
    tags=["Alliance"]
)

def get_alliances():
    operation = esi.client.Alliance.GetAlliances()
    alliances = operation.results()
    return alliances

def get_alliances_alliance_id(alliance_id):
    operation = esi.client.Alliance.GetAlliancesAllianceId(alliance_id=alliance_id)
    detail = operation.result()
    return detail

def get_list_alliance_corporations(alliance_id):
    operation = esi.client.Alliance.GetAlliancesAllianceIdCorporations(alliance_id=alliance_id)
    corporations = operation.result()
    return corporations

def get_alliance_icon_urls(alliance_id):
    operation = esi.client.Alliance.GetAlliancesAllianceIdIcons(alliance_id=alliance_id)
    urls = operation.result()
    return urls
# tasks.py / views.py
from .providers import get_alliances, get_alliances_alliance_id, get_list_alliance_corporations, get_alliance_icon_urls
...

Using public endpoints

Here is a complete example how to use a public endpoint. Public endpoints can in general be accessed without any authentication.

from esi.openapi_clients import ESIClientProvider
# Generate a client
esi = ESIClientProvider(
    compatibility_date = "2025-07-23",
    ua_appname = "MyProject",
    ua_version = "0.0.1a1",
    operations=["GetAlliances"]
)
def main():
    # Create a ESI Request
    operation = esi.client.Alliance.GetAlliances()
    # Send the Request, with return_response=True to receive
    alliances = operation.results()
    # Do Things!
    print(alliances)

main()

Using authenticated endpoints

Non-public endpoints will require authentication. You will therefore need to provide a valid access token with your request.

The following example shows how to retrieve data from a non-public endpoint using an already existing token in your database.

See also

See also the section Usage in views on how to create tokens in your app.

from esi.helpers import get_token
from esi.openapi_clients import ESIClientProvider
# Generate a client
esi = ESIClientProvider(
    compatibility_date = "2025-07-23",
    ua_appname = "MyProject",
    ua_version = "0.0.1a1",
    operations=["GetCharactersCharacterIdAssets"]

)
def fetch_assets():
    req_scopes = ['esi-assets.read_assets.v1']
    token = get_token(90406623, req_scopes)
    res = esi.client.Assets.GetCharactersCharacterIdAssets(
        character_id=90406623,
        token=token,
    )
    assets = res.results()
    return(assets)

fetch_assets()

results() vs. result()

Django-ESI offers two similar methods for requesting the response from an endpoint: results() and result(). Here is a quick overview how they differ:

Paging

Headers

results()

Automatically returns all data through pages or cursors if there is more than one

Returns the headers for the last retrieved page

result()

Only returns the first page or the requested page (when specified with page parameter)

Returns the headers for the first requested page

cursor()

None, WIP

In general we recommend to use results(), so you don’t have to worry about paging. Nevertheless, result() gives you more direct control of your API request and has it’s uses, e.g when you are only interested in the first page and do not want to wait for all pages to download from the API.

Getting localized responses from ESI

Some ESI endpoints support localization, which means they are able to return the content localized in one of the supported languages.

To retrieve localized content just provide the language code in your request. The following example will retrieve the type info for the Svipul in Korean:

result = esi.client.Universe.GetUniverseTypesTypeId(
    type_id=11567,
    Accept_Language='ko'
).results()

A common use case it to retrieve localizations for all languages for the current request. For this django-esi provides the convenience method results_localized(). It substitutes results() and will return the response in all officially supported languages by default.

results_localized = esi.client.Universe.GetUniverseTypesTypeId(
        type_id=11567
).results_localized()

Alternatively you can pass the list of languages (as language code) that you are interested in:

results_localized = esi.client.Universe.GetUniverseTypesTypeId(
    type_id=11567
).results_localized(languages=['ko', 'de'])

Rate Limiting and Exceptions

ESI currently has a few separate rate limiting mechanisms.

The Global Error Limit

If you trip the global error limit (currently 100errors/60seconds) ESIClientProvider will raise a ESIErrorLimitException(), Your apps should either:

  • retry their celery tasks after the reset timer, Preferred, non blocking

  • wrap requests in the wait_for_esi_error_limit_reset() decorator, which will block other celery tasks from running.

Custom Rate Limits

Two are currently known

  • Market Data History, 300 Requests / 60 seconds

  • Character Corporation History, 300 Requests / 60 Seconds

Wrapping your requests in the esi_rate_limiter_bucketed() decorator will raise ESIBucketLimitException(bucket) if you exceed the rate limit.

  • retry their celery tasks after the reset timer, Preferred, non blocking

  • wrap requests in the esi_rate_limiter_bucketed(raise_on_limit=False) decorator, which will block other celery tasks from running.

Floating Window Rate Limiting

These rate limits are exposed in the specifications e.g. GetStatus This route is part of the rate limit group status. This group is limited to 600 tokens per 15 minutes.

Our client dynamically extracts these rate limits from the spec per endpoint/grouping and tracks their refresh from headers. Exhausting these Rate Limits will raise ESIBucketLimitException(bucket)

Token System

(Copied from https://developers.eveonline.com/docs/services/esi/rate-limiting/#bucket-system for awareness)

As shown, you are encouraged to use ETags (If-Match)

Status Code

Token Cost

Reasoning

2XX

2 tokens

3XX

1 token

Promote the use of If-Modified-Since and If-Match.

4XX

5 tokens

Discourage hitting user-errors.

5XX

0 tokens

You shouldn’t be penalized for server-side errors.

Compatibility Date Header

After 2025-07-10 This Blog Post ESI will now be versioned as a whole spec, and not with versioned paths specific to each endpoint.

Compatibility Date must be included in order to generate an ESIClientProvider instance, in the format YYYY-MM-DD.

Note

You can also provide a python date object (datetime.date) to the ESIClientProvider. As a compatibility date to the client factory.

This header tells ESI, “This application’s ESI implementation was updated or reviewed at this date - give me the API behavior as it was at that date”, please use a date that you genuinely tested or built your app. Setting this to “today” will cause you issues later.

For best results, you should develop against Django-ESI with a consistent specific build date in mind which will Type Hint your code, Set this as your Compatibility Date and only roll forward once fully tested. Django-ESI can still be updated safely by users as your Compatibility Date will still define the ESIs behaviour.

User Agent header

CCP asks developers to provide a “good User-Agent header” with all requests to ESI, so that CCP can identify which app the request belongs to and is able to contact the server owner running the app in case of any issues. This requirement is specified in the CCP’s Developer Guidelines and detailed in the ESI guidelines.

Django-ESI provides a user-agent generator for setting the User-Agent header following a consistent format:

Application Info

When using ESIClientProvider Two parameters ua_appname and ua_version are required to build the user-agent dynamically, whilst ua_url may be optionally included to link to a Repository or Documentation.

Our User-Agent Generator follows RFC9110 and The MDN Docs

Application Name Format

The ua_appname should be formatted as PascalCase without spaces and/or hyphens, and ua_version should follow Semantic Versioning. If your ua_appname contains spaces or hyphens, they will be removed and the following character will be capitalized to convert it to PascalCase.

The conversion follows this defined behaviour:

Any string containing spaces or hyphens will be converted to PascalCase. Strings without spaces or hyphens will be returned unchanged.

This gives you the opportunity to use already formatted strings as needed.

Examples

Input Format

Output Format

app name

AppName

app-name

AppName

appname

appname

AppName

AppName

appName

appName

app_name

app_name

AppName/1.2.3 (foo@example.com) DjangoEsi/1.2.3 (+https://gitlab.com/allianceauth/django-esi)

or if ua_url is specified

AppName/1.2.3 (foo@example.com; +https://gitlab.com/) DjangoEsi/1.2.3 (+https://gitlab.com/allianceauth/django-esi)

Here is a complete example for defining an application string with your app:

from esi.openapi_clients import ESIClientProvider

esi = ESIClientProvider(ua_appname="AppName", ua_version="1.2.3", ua_url="https://gitlab.com/")

Hint

Linking to this information Dynamically will reduce duplication

from esi.openapi_clients import ESIClientProvider
from . import __title__, __version__, __url__

esi = ESIClientProvider(ua_appname=__title__, ua_version=__version__, ua_url=__url__)

Note

If you do not define both ua_appname and ua_version, the application string used will be "DjangoEsi/1.2.3 (+https://gitlab.com/allianceauth/django-esi)".

Contact email

To enable CCP to contact the maintainer of a server that is using ESI it is mandatory to specify a contact email. This can be done through the setting ESI_USER_CONTACT_EMAIL.

Example:

ESI_USER_CONTACT_EMAIL = "admin@example.com"

In case you are not hosting the app yourself, we would recommend including this setting in the installation guide for your app.

Caching / ETags, Why and How

Django-ESI 8.x and the OpenAPI client now assume your data is, and should be cached. This is part of an ongoing effort to be more respectful of ESI as a shared resource and our impact on it.

You should stay with the default Caching and ETags where possible, but also consider the frequency with with you pull data and the Redis/Memory demands of the hosts of your applications.

Cache: Default = True

Caching requests will store the entire HTTP response in memory.

If you have a large set of data you pull much less regularly than the expected cache TTL, or you know you will never want the cached response consider not saving it to save memory.

You may also, in some specific circumstances wish to forcibly ignore our cache and pull data from ESI for diagnostics or maintenance reasons.

use_cache -- check cache prior to fetching from ESI (default True)
store_cache -- store the returned data from ESI in cache (default True)
  • PRO: Allows you full access to the HTTP Response to work on in-place.

  • PRO: Does not hit ESI at all whilst the cache is valid.

  • CON: Consumes the most memory

  • CON: Shortlived, Cached for Max-Age or Expires which can vary on the endpoint

  • CON: Not Smart, if CCP invalidate the cache voluntarily our cache will hold on to the data until it expires.

ETags (If-None-Match): Default = True

An ETag is a hash of the content of a response. These allow you to ask ESI politely if the response has changed. If the response has not changed, you will receive HTTPNotModified.

Again, you can not send or store ETags for specific reasons with the following.

use_etag -- Use the inbuilt ETag matching system (default True)
  • PRO: Minimal Memory Usage

  • CON: Does hit ESI (consuming 1 Token where relevant)

  • CON: Does not return the HTTP Response if you need it.

  • NOTE: Decent life, currently 7 Days.

Last-Modified (If-Modified-Since): Optional Datetime

Similar to an ETag, this is the datetime the resource was last modified. These allow you to query if an object has changed since the last time you pulled the data, irrelevant of the content. If this matches you will receive HTTPNotModified

Unlike ETags, If-Modified-Since is not managed internally in Django-ESI (Yet?), instead you can store them along side your models and pass it with your request, These are very situational although improvements on CCPs end may end up meaning these see a lot more use in time to come.

last_modified -- Optional datetime to send as If-Modified-Since
  • PRO: No Memory Usage

  • PRO: Valid forever? if an object doesn’t change for a year, neither will If-Modified-Since

  • Does Hit ESI (Consuming 1 Token where relevant)

  • CON: Does not return the HTTP Response if you need it.

  • CON: Requires additional handling. On single object models this is easy.

class Example(models.Model):
    example_id = models.PositiveIntegerField(unique=True)
    last_updated = models.DateTimeField(default=now)  # auto_now_add=True doesnt allow override from Last_Modified header

Force Refresh

Use with responsibility. This will not only not use ETags and the HTTP Cache, it will wipe them.

force_refresh -- clear ETag and cache, force a re-fetch from ESI (default False)
  • PRO: Sometimes a hammer is the right tool for a job

  • CON: Sometimes a hammer is not the right tool for a job…

  • CON: Will affect following tasks impact on ESI

Advanced Features

Using a local spec file

Using a local resource file is less useful in OpenAPI3 than it was with Swagger.

You may pass the spec__file parameter to your local openapi.json, but you will be missing out on the Non Breaking Features that ccp may add to a given compatibility date.

Of course this may be a reason to use a spec_file, but it it is the hope with OpenAPI3 these are indeed non breaking changes time will tell.

import os
from esi.openapi_clients import ESIClientProvider
SWAGGER_SPEC = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'openapi.json')
esi = ESIClientProvider(
    compatibility_date = "2025-07-23",
    ua_appname = "MyProject",
    ua_version = "0.0.1a1",
    spec_file=SWAGGER_SPEC
)

Getting Response Data

Sometimes you may want to also get the internal response object from an ESI response. For example to inspect the response header. For that simply use return_response=True in your call. This works in the same way for both .result() and .results()

from esi.openapi_clients import ESIClientProvider
# Generate a client
esi = ESIClientProvider(
    compatibility_date = "2025-07-23",
    ua_appname = "MyProject",
    ua_version = "0.0.1a1",
    operations=["GetAlliances"]
)
def main():
    # Create a ESI Request
    operation = esi.client.Alliance.GetAlliances()

    # Send the Request, with return_response=True to receive
    alliances, response = operation.results(return_response=True)

    # Do Things!
    print(alliances, response)

Accessing alternate data sources

Currently Functionally Useless, CCP use this internally but there is only one Tenant exposed publicly.

If singularity or the AT servers are ever exposed again, this may be of some use.

Hint

This used to be Datasource in Swagger

from esi.openapi_clients

# create your own provider
esi = ESIClientProvider(tenant='tranquility')

Using the Raw AIOpenAPI3 Interface

The developer of AIOpenAPI3 calls this the “Sad Smiley” interface, named from ._., This will have none of our niceties or faux-bravado wrapper, but the client is available should you wish.

esi = ESIClientProvider(
    compatibility_date = "2025-07-23",
    ua_appname = "MyProject",
    ua_version = "0.0.1a1",
    operations=["GetAlliances"]
)
req = esi._.GetAlliances
alliances = req()
print(alliances)

Exploring ESI Endpoints

Three options are available

  • CCP’s API Explorer

  • Python Autocomplete, your IDE should expose Tags and Operations if you generate a client and type esi.client.

  • Manually from a Django Shell

from esi.openapi_clients import ESIClientProvider
# Generate a client
esi = ESIClientProvider(
    compatibility_date = "2025-07-23",
    ua_appname = "MyProject",
    ua_version = "0.0.1a1"
)
client = esi.client

# Print all tags and their endpoints
print("Available ESI endpoints:\n")
for tag in sorted(client._tags):
    tag_obj = getattr(client, tag)
    print(f"[{tag}]")
    for op in sorted(tag_obj._operations):
        print(f"  - {op}")
    print()
[Alliance]
  - GetAlliances
  - GetAlliancesAllianceId
  ...

[Assets]
  - GetCharactersCharacterIdAssets
  ...

[Calendar]
  - GetCharactersCharacterIdCalendar
  ...

Generating ESI Stubs

Django-ESI Ships with a series of Stubs to make development easier, should you want to release a new version of Django-ESI or develop against a specific compatibility date with handy typehints

python manage.py generate_esi_stubs --compatibility_date=2020-01-01

For easier release management, compatibility_date is set to __build_date__ of Django-ESI by default.