28 KiB
EVE ESI/SSO research brief
Checked: 2026-09-18 (UTC)
Scope: current official ESI/SSO documentation and live ESI OpenAPI/metadata only. No SDE archive was downloaded. The captured OpenAPI evidence is eve-openapi-evidence.json; live compatibility/changelog responses are eve-compatibility.json and eve-changelog.json.
Executive contract
- Use
https://esi.evetech.netand sendX-Compatibility-Date: 2026-08-18. This is the newest value in the live compatibility-date list on this date;2026-09-18is not yet a supported value. Pinning the header avoids silently receiving the oldest behavior. ESI versioning · live compatibility dates - Character-scoped ESI tokens are character-specific. A local EVE app user may link multiple characters, but each character needs its own authorization/token record and the token's granted scopes apply only to that character. SSO
- The asset route is paged (
page, 1-based;X-Pages) and has a 3,600-second server/client cache. Market reference prices are one unpaged global array, also cached 3,600 seconds. Current OpenAPI - An Access List membership is not proof of docking: the current Access List routes return list membership but no structure/profile association. A successful authenticated
GET /universe/structures/{structure_id}is character-scoped ACL/access evidence, not an unconditional docking guarantee; astructure_idinGET /characters/{character_id}/locationis evidence the character was docked there at the cached observation time. Resolve Structure IDs · Current OpenAPI
ESI versioning and current OpenAPI
The live API exposes OpenAPI 3.1 at /meta/openapi.json. It was fetched with X-Compatibility-Date: 2026-08-18; the response's X-Compatibility-Date and info.version were both 2026-08-18. The live /meta/compatibility-dates response lists, newest first:
2026-08-18, 2026-08-04, 2026-07-21, 2026-07-17, 2026-06-09,
2026-05-19, 2025-12-16, 2025-11-06, 2025-09-30, 2025-09-26,
2025-08-26, 2025-04-02, 2025-04-01, 2020-01-01
ESI documents X-Compatibility-Date: YYYY-MM-DD as the reviewed API behavior date; compatibility_date is the fallback for clients unable to set headers. It also says the API changes date at 11:00 UTC and a date cannot be future or below the oldest available. The live endpoint is the authoritative check for the value to pin.
The 2026-05-19 changelog release added Access Lists, Mercenary Dens/MTOs, Skyhooks, Sovereignty Hubs, raidable Skyhooks, and /sovereignty/systems; it removed /sovereignty/map and /sovereignty/structures at that compatibility date. Live changelog · Equinox ESI announcement
Assets: exact current contract
Character assets
GET https://esi.evetech.net/characters/{character_id}/assets?page={page}
Authorization: Bearer <character-token>
X-Compatibility-Date: 2026-08-18
- Scope:
esi-assets.read_assets.v1. pageis optional, integer, minimum1; response headerX-Pagesis the total number of pages. Fetch page 1, readX-Pages, then pages 2..N. ESI's X-Pages guidance warns that cache expiry between pages can duplicate items; de-duplicate byitem_id(and consider a stable page-fetch strategy). X-Pages pagination- Cache:
x-cache-age: 3600,x-client-cache-ttl: 3600,x-server-cache-mode: ttl-based,x-server-cache-ttl: 3600; response supportsCache-Control,ETag,Last-Modified, and requests supportIf-None-Match/If-Modified-Since. - Rate extension: group
char-asset,1800tokens per15m(where the new limiter is active). - Response is an array. Each item has this exact shape:
| Field | Type | Required | Contract |
|---|---|---|---|
is_blueprint_copy |
boolean | no | Whether the asset is a blueprint copy |
is_singleton |
boolean | yes | Singleton flag |
item_id |
int64 | yes | Asset item ID |
location_flag |
enum string | yes | Exact character enum below |
location_id |
int64 | yes | Parent/container/location ID |
location_type |
enum string | yes | station, solar_system, item, other |
quantity |
int64 | yes | Quantity |
type_id |
int64 | yes | EVE type ID; no type name or capital classification is included |
Character location_flag enum (exact OpenAPI values):
AssetSafety, AutoFit, BoosterBay, CapsuleerDeliveries, Cargo,
CorporationGoalDeliveries, CorpseBay, Deliveries, DroneBay, ExpeditionHold,
FighterBay, FighterTube0, FighterTube1, FighterTube2, FighterTube3, FighterTube4,
FleetHangar, FrigateEscapeBay, Hangar, HangarAll, HiSlot0, HiSlot1, HiSlot2,
HiSlot3, HiSlot4, HiSlot5, HiSlot6, HiSlot7, HiddenModifiers, Implant,
InfrastructureHangar, LoSlot0, LoSlot1, LoSlot2, LoSlot3, LoSlot4, LoSlot5,
LoSlot6, LoSlot7, Locked, MedSlot0, MedSlot1, MedSlot2, MedSlot3, MedSlot4,
MedSlot5, MedSlot6, MedSlot7, MobileDepotHold, MoonMaterialBay, QuafeBay,
RigSlot0, RigSlot1, RigSlot2, RigSlot3, RigSlot4, RigSlot5, RigSlot6, RigSlot7,
ShipHangar, Skill, SpecializedAmmoHold, SpecializedAsteroidHold,
SpecializedCommandCenterHold, SpecializedFuelBay, SpecializedGasHold,
SpecializedIceHold, SpecializedIndustrialShipHold, SpecializedLargeShipHold,
SpecializedMaterialBay, SpecializedMediumShipHold, SpecializedMineralHold,
SpecializedOreHold, SpecializedPlanetaryCommoditiesHold, SpecializedSalvageHold,
SpecializedShipHold, SpecializedSmallShipHold, StructureDeedBay, SubSystemBay,
SubSystemSlot0, SubSystemSlot1, SubSystemSlot2, SubSystemSlot3, SubSystemSlot4,
SubSystemSlot5, SubSystemSlot6, SubSystemSlot7, Unlocked, Wardrobe
Corporation assets
GET https://esi.evetech.net/corporations/{corporation_id}/assets?page={page}
- Scope:
esi-assets.read_corporation_assets.v1; current OpenAPI declares required roleDirector. - Same page/cache/header contract, with rate group
corp-asset,1800/15m. - Same fields and requiredness as character assets. The exact corporation
location_flagenum is:
AssetSafety, AutoFit, Bonus, Booster, BoosterBay, Capsule, CapsuleerDeliveries,
Cargo, CorpDeliveries, CorpSAG1, CorpSAG2, CorpSAG3, CorpSAG4, CorpSAG5,
CorpSAG6, CorpSAG7, CorporationGoalDeliveries, CrateLoot, Deliveries, DroneBay,
DustBattle, DustDatabank, ExpeditionHold, FighterBay, FighterTube0, FighterTube1,
FighterTube2, FighterTube3, FighterTube4, FleetHangar, FrigateEscapeBay, Hangar,
HangarAll, HiSlot0, HiSlot1, HiSlot2, HiSlot3, HiSlot4, HiSlot5, HiSlot6, HiSlot7,
HiddenModifiers, Implant, Impounded, InfrastructureHangar, JunkyardReprocessed,
JunkyardTrashed, LoSlot0, LoSlot1, LoSlot2, LoSlot3, LoSlot4, LoSlot5, LoSlot6,
LoSlot7, Locked, MedSlot0, MedSlot1, MedSlot2, MedSlot3, MedSlot4, MedSlot5,
MedSlot6, MedSlot7, MobileDepotHold, MoonMaterialBay, OfficeFolder, Pilot,
PlanetSurface, QuafeBay, QuantumCoreRoom, Reward, RigSlot0, RigSlot1, RigSlot2,
RigSlot3, RigSlot4, RigSlot5, RigSlot6, RigSlot7, SecondaryStorage,
ServiceSlot0, ServiceSlot1, ServiceSlot2, ServiceSlot3, ServiceSlot4, ServiceSlot5,
ServiceSlot6, ServiceSlot7, ShipHangar, ShipOffline, Skill, SkillInTraining,
SpecializedAmmoHold, SpecializedAsteroidHold, SpecializedCommandCenterHold,
SpecializedFuelBay, SpecializedGasHold, SpecializedIceHold,
SpecializedIndustrialShipHold, SpecializedLargeShipHold, SpecializedMaterialBay,
SpecializedMediumShipHold, SpecializedMineralHold, SpecializedOreHold,
SpecializedPlanetaryCommoditiesHold, SpecializedSalvageHold, SpecializedShipHold,
SpecializedSmallShipHold, StructureActive, StructureFuel, StructureInactive,
StructureOffline, SubSystemBay, SubSystemSlot0, SubSystemSlot1, SubSystemSlot2,
SubSystemSlot3, SubSystemSlot4, SubSystemSlot5, SubSystemSlot6, SubSystemSlot7,
Unlocked, Wallet, Wardrobe
Asset helper routes
The same asset scope/rate group applies to the character helper routes:
POST /characters/{character_id}/assets/locationsaccepts a required JSON array of 1..1000 unique int64item_ids and returns{item_id, position:{x,y,z}}; coordinates for items in hangars/stations are(0,0,0).POST /characters/{character_id}/assets/namesaccepts the same 1..1000 unique-ID array and returns{item_id, name}for custom-namable items.- Corporation equivalents use
esi-assets.read_corporation_assets.v1, require the current OpenAPI'sDirectorrole, and use thecorp-assetgroup. - These POST responses expose the same cache validators but have no page parameter. Current OpenAPI evidence
Reference-price valuation
GET https://esi.evetech.net/markets/prices
X-Compatibility-Date: 2026-08-18
- Public; no OAuth scope and no query parameters.
- Response is an unpaged array. Item schema is exactly
{type_id:int64}plus optionaladjusted_price:number(double)and optionalaverage_price:number(double); onlytype_idis required by OpenAPI. Do not assume either price field exists for every type. - Cache:
x-cache-age: 3600, client/server TTL 3600, server modettl-based; response advertisesCache-Control,ETag, andLast-Modified, and acceptsIf-None-Match/If-Modified-Since. The live response is global rather than region/structure-specific. - The endpoint does not provide a price timestamp, region, order depth, or valuation semantics. Store fetch time and compatibility date with a valuation snapshot. Choose and document a missing-price policy; the ESI schema does not authorize an automatic
adjusted_pricefallback for a missingaverage_price. - For each asset item with a known price, a transparent reference valuation is
quantity × selected price; blueprint-copy, fitted/container, and location semantics remain application policy.
For actual structure-market orders, use GET /markets/structures/{structure_id}?page=N with esi-markets.structure_markets.v1; it is paged by X-Pages, cached for 300 seconds, and is not the same data as global reference prices.
Rate limiting and cache headers
ESI currently documents two mutually exclusive limiter header families:
New floating-window/token limiter (only on routes that advertise x-rate-limit in OpenAPI):
X-Ratelimit-Group: route bucket, e.g.char-assetormeta.X-Ratelimit-Limit: total tokens/window, e.g.150/15m.X-Ratelimit-Remaining: available tokens.X-Ratelimit-Used: tokens consumed by the request.Retry-Afteron429, in seconds.
The documented token costs are 2 for 2xx, 1 for 3xx, 5 for 4xx (excluding 429), and 0 for 5xx. Buckets are per rate-limit group and user ID; authenticated user ID is <applicationID>:<characterID>, not a global app-only bucket. Rate limiting
Legacy error limiter (still active for routes without the new limiter):
X-ESI-Error-Limit-Remain: errors left in the current fixed window.X-ESI-Error-Limit-Reset: seconds until that window resets.- It allows at most 100 non-2xx/3xx responses per minute; once reached, ESI returns 420s on routes. Best practices
A live GET /markets/prices response on the check date exposed the legacy X-ESI-* headers (and ETag/Last-Modified); a live GET /meta/openapi.json response with the pinned date exposed the new X-Ratelimit-* family (meta, 150/15m). Header values such as remaining/reset are volatile. The ESI Access-Control-Expose-Headers response also lists both families, Retry-After, X-Compatibility-Date, and X-Pages.
Use ETag validators first, respect endpoint TTLs, and do not poll at the limiter edge. A 304 is the expected cache-saving path where validators are honored.
SSO: web login, PKCE, JWT, and refresh
Discovery and endpoints
Fetch and cache the OAuth metadata document at https://login.eveonline.com/.well-known/oauth-authorization-server. On the check date it returned:
{
"issuer": "https://login.eveonline.com",
"authorization_endpoint": "https://login.eveonline.com/v2/oauth/authorize",
"token_endpoint": "https://login.eveonline.com/v2/oauth/token",
"userinfo_endpoint": "https://login.eveonline.com/v2/oauth/verify",
"jwks_uri": "https://login.eveonline.com/oauth/jwks",
"revocation_endpoint": "https://login.eveonline.com/v2/oauth/revoke",
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "client_secret_jwt"]
}
Use metadata URLs rather than hard-coding them; the current SSO page explicitly recommends this. The current official deprecation notice says the old oauth/* endpoints are obsolete: use v2/oauth/authorize and v2/oauth/token, send a form-encoded body, and do not send JSON or token parameters in the query string. SSO docs · SSO endpoint deprecations
Authorization Code (server/web)
-
Register the app and exact callback URL in the EVE Developers Portal. Generate a random
stateand retain it in the login session. -
Redirect to
authorization_endpointwith URL-encoded parameters:response_type=code client_id=<client id> redirect_uri=<registered exact URL> scope=<space-separated requested scopes> state=<random CSRF value> -
Verify callback
state, then POST form-urlencoded totoken_endpointwithgrant_type=authorization_code,code=<one-time-code>, and the registeredredirect_uri, using HTTP Basic credentialsclient_id:client_secret. The current strict-conformance notice showsredirect_uriin the token body; the current SSO page's simplified Python example omits it, so send the registered value and test against the registered app. -
The response contains an access token and refresh token. Keep the refresh token server-side; access tokens are time-limited and character/scope-bound.
Authorization Code + PKCE (native/public client)
- Generate a high-entropy
code_verifier; sendcode_challenge = base64url(SHA-256(code_verifier))without=padding, withcode_challenge_method=S256. - Add
code_challengeandcode_challenge_method=S256to the authorization query above. Do not use a client secret in this flow. - Exchange the callback code with a form-urlencoded POST body containing
grant_type=authorization_code,code,client_id,code_verifier, and the registeredredirect_uri. Retain the verifier until exchange; verifystateexactly as in the web flow. The strict-conformance notice is the reason to includeredirect_uri; its newer SSO-page sample omits that field.
The current ESI OpenAPI OAuth2 security scheme also declares the same v2 authorize/token URLs and all ESI scopes. For this app's chosen data, the primary scopes are esi-assets.read_assets.v1; add esi-location.read_location.v1 for current docking, esi-location.read_ship_type.v1 for current ship/capital type ID, and esi-universe.read_structures.v1 for authenticated structure resolution. Current OpenAPI
Refresh
The retained official refresh-token page documents the exact current v2 request shape: web apps POST form-urlencoded grant_type=refresh_token&refresh_token=<token> with Basic client_id:secret; native/public apps send grant_type=refresh_token, refresh_token, and client_id without Basic. Optional scope may narrow to a subset of the original grant. URL-encode the token. The response shape is:
{
"access_token": "<jwt>",
"token_type": "Bearer",
"expires_in": 1200,
"refresh_token": "<possibly-new-token>"
}
The current SSO page describes refresh tokens as long-lived while access is not revoked, and the official deprecation notice says refresh-token rotation may change the returned token: always replace the stored refresh token with the returned one. Official refresh contract (retained page) · Rotation notice
JWT verification and local user/character identity
The access token is a JWT. Fetch jwks_uri from metadata, select the key by JWT header kid/algorithm, verify the signature with a standard JWT library, validate expiration, issuer, and audience, and only then trust claims. The current SSO docs specify:
issshould be the EVE issuer (https://login.eveonline.com/or, in some cases,login.eveonline.com; use metadata and accept only the documented EVE forms).audis an array containing the appclient_idand staticEVE Online; check both.subisCHARACTER:EVE:<character-id>;nameis the character name;scpenumerates granted scopes. The official guide describes an array, but singleton scope claims can also be scalar strings. Normalize one valid scope string into a one-element array only after JWT verification; do not infer permissions from missing or malformed claims.
Runtime compatibility follow-up (2026-09-19): a live callback reached verified token processing but failed the original array-only scope parser. The maintained eveauth implementation explicitly handles both scalar strings and arrays in extractScopes, with a singleton regression test. Our signed-token regression reproduced the scalar failure and now verifies both login and refresh normalization. Invalid/missing claims and whitespace-delimited ambiguity remain rejected; all signature, issuer, audience, owner/session and exact scope-membership checks remain in force.
The current JWKS observed RS256, but the official deprecation notice warns signature methods can change; do not hard-code the observed key/algorithm. A local app user should be an application identity with a child record per character_id, token, refresh token, scopes, and last refresh. SSO selects one character per authorization, so linking another character is another flow. JWT guidance
Upwell structures, Access Lists, and docking evidence
General structure routes
| Route | Auth/scope | Current contract and cache |
|---|---|---|
GET /universe/structures?filter=market|manufacturing_basic |
Public | Unique array of public structure int64 IDs; filter only selects structures with that service online; TTL 3600. |
GET /universe/structures/{structure_id} |
esi-universe.read_structures.v1 |
200 returns name, solar_system_id, owner_id, optional type_id/position; default error includes 403 when not on ACL; TTL 3600. |
GET /corporations/{corporation_id}/structures?page=N |
esi-corporations.read_structures.v1; current OpenAPI role Station_Manager |
Paged array with structure ID/type/corporation/system/profile, name, services, state, fuel/reinforcement fields; X-Pages; TTL 3600; corp-structure 300/15m. |
GET /markets/structures/{structure_id}?page=N |
esi-markets.structure_markets.v1 |
Paged order array (X-Pages); TTL 300; no claim that market visibility means docking access. |
The current OpenAPI explicitly describes /universe/structures/{id} as returning information only if the caller is “on the ACL”, otherwise “Forbidden”. The retained official structure scenario is more explicit: /universe/structures lists only fully public structures (an ACL that prevents even one character from docking excludes it), while the authenticated ID route requires a token for a character with docking access. Treat the scenario as corroborating operational guidance because its page warns that it is no longer updated; the pinned OpenAPI is the current field/scope source.
2026-05-19 routes
These routes are present in the current (2026-08-18) OpenAPI and have route compatibility date 2026-05-19:
| Route | Scope | Shape/cache/limit |
|---|---|---|
GET /characters/{character_id}/access-lists |
esi-access.read_lists.v1 |
{access_lists:[{id:int64}]}; character must manage/admin an ACL; event cache 300; char-access 600/15m. |
GET /characters/{character_id}/access-lists/{access_list_id} |
same | {id,name,description,membership}; event cache 300; same limiter. |
GET /characters/{character_id}/structures/mercenary-dens and /{mercenary_den_id} |
esi-structures.read_character.v1 |
Character structure data; event cache 3600; char-structure 30/15m. |
GET /characters/{character_id}/mercenary-tactical-operations and /{operation_id} |
esi-activities.read_character.v1 |
MTO data; event cache 300; char-activity 150/15m. |
GET /corporations/{corporation_id}/structures/skyhooks and /{skyhook_id} |
esi-structures.read_corporation.v1 |
Listing wrapper {skyhooks:[...]}; detail includes id/planet/state/activity/reagents/timers; event cache 3600; corp-structure 300/15m; current OpenAPI says Station_Manager. |
GET /corporations/{corporation_id}/structures/sovereignty-hubs and /{sovereignty_hub_id} |
same | Listing wrapper {sovereignty_hubs:[...]}; detail includes id/system/upgrades/reagent bay/resources/transport; event cache 3600; same limiter; detail declares Station_Manager. |
GET /skyhooks/raidable |
Public | Upcoming/current raidable Skyhooks; TTL cache 300/client, server TTL 60; activity 30/15m. |
GET /sovereignty/systems |
Public | Combined K-space sovereignty details; TTL 300; sovereignty 600/15m. |
What proves what
- Public visibility/discovery only: an ID in
/universe/structuresproves the structure is in ESI's fully-public list (and, withfilter, that the selected service is online). It does not enumerate private/ACL structures and does not prove this particular character can dock. - Character-scoped ACL/access evidence: a successful 200 from
/universe/structures/{structure_id}using that character's token is the direct ESI structure-access check; a non-authorized caller receives the default error/403. Current OpenAPI says “on the ACL”; the older scenario interprets this as docking access. Preserve that evidence with its timestamp and confidence, but do not promote metadata visibility into an unconditionalcanDockboolean. Ship class, structure state, subsequent ACL changes and cache age still matter, especially for capitals. - Observed docking:
/characters/{character_id}/locationrequiresesi-location.read_location.v1, is event-based with a 5-second cache, and returns requiredsolar_system_idplus optionalstation_idorstructure_id. A matchingstructure_idis evidence of docking at the observation time, not guaranteed present/future docking permission for a different ship. A mere ACL membership or a 200 on structure metadata is not a docking observation. - ACL membership: the Access List detail route returns
allow_everyoneplus character/corporation/alliance entries whose access isUnspecified,Allowed,Blocked,Manager, orAdmin. It does not returnstructure_id, structure profile, docking service, or a computed per-character permission. Therefore membership alone cannot prove docking. - Corporation ownership/management: the corporation-structures route proves the corporation's structures and exposes
profile_id/operational state to an appropriately authorized corporation character. It does not prove that an arbitrary linked character can dock.
Capital/intel-specific limits
GET /characters/{character_id}/ship(esi-location.read_ship_type.v1) returns onlyship_type_id,ship_item_id, andship_name; it is 5-second TTL-cached and useschar-location1200/15m. Assets likewise exposetype_id, not acapitalboolean.- Capital/supercapital classification therefore requires joining
ship_type_id/assettype_idto official static data; ESI itself does not provide that classification. The official static-data page documents SDE as the static game data source and JSON Lines/YAML formats; no archive was downloaded for this research. Static Data - A ship
type_idplus a market price is not proof of current location or docking: use the location route for current presence and structure metadata/ACL check for access.
Confidence, blockers, and implementation handoff
High confidence: current asset/market schemas, scopes, page headers, cache extensions, 2026-08-18 supported compatibility value, 2026-05-19 route additions, SSO endpoints/PKCE parameters, JWT claims, and live rate-header names; all were taken from the pinned OpenAPI, current EVE docs, or live official metadata.
Medium confidence: translating a successful structure metadata request into “docking access” relies on the retained official scenario page; current OpenAPI says “on the ACL” and 403 otherwise, but does not name the ACL permission as docking.
Blockers/decisions:
- There is no current ESI route that joins an Access List to a structure profile or computes a character's docking permission. Do not infer that relationship from Access List membership.
- Asset pages can change while being fetched; use validators/TTL and de-duplicate by
item_id. Assetlocation_idandtype_idneed separate name/static-data joins. /markets/pricesis a global cached reference snapshot with optional fields, not a region/structure order-book valuation. Define missing-price, blueprint, fitted-ship, and stale-price policy before showing totals.- Refresh tokens can rotate. Persist the replacement returned by every successful refresh, and store credentials per linked character.
- The current SSO page's simplified token examples omit
redirect_uriat exchange, while the official strict-conformance notice includes the registeredredirect_uri; use the latter and verify in a registered test app before launch.
Sources kept
- ESI overview/versioning — compatibility-date and API contract rules.
- Current ESI OpenAPI — exact route, schema, scope, cache, pagination, and limiter extensions; captured with
X-Compatibility-Date: 2026-08-18. - Compatibility dates and changelog — live supported-date and 2026 route-release evidence.
- ESI rate limiting and best practices/error limit — new and legacy headers/costs.
- X-Pages pagination — page and cache-expiry behavior.
- Current SSO docs, OAuth metadata, and SSO deprecation/rotation notice — auth code, PKCE, JWT, v2 endpoints, and refresh rotation.
- Equinox ESI structures/access lists — current 2026 structure/access-list release context.
- Retained structure scenario and retained refresh contract — exact docking interpretation and refresh body; both pages warn that the old docs site is no longer updated, so current OpenAPI/current SSO docs take precedence.
- Official static-data docs — capital/type classification dependency; no archive downloaded.