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 |
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.
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.