Files
eden/docs/research/eve.md
T
2026-09-18 23:15:05 -04:00

28 KiB
Raw Blame History

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.net and send X-Compatibility-Date: 2026-08-18. This is the newest value in the live compatibility-date list on this date; 2026-09-18 is 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; a structure_id in GET /characters/{character_id}/location is 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.
  • page is optional, integer, minimum 1; response header X-Pages is the total number of pages. Fetch page 1, read X-Pages, then pages 2..N. ESI's X-Pages guidance warns that cache expiry between pages can duplicate items; de-duplicate by item_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 supports Cache-Control, ETag, Last-Modified, and requests support If-None-Match / If-Modified-Since.
  • Rate extension: group char-asset, 1800 tokens per 15m (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 role Director.
  • Same page/cache/header contract, with rate group corp-asset, 1800/15m.
  • Same fields and requiredness as character assets. The exact corporation location_flag enum 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/locations accepts a required JSON array of 1..1000 unique int64 item_ids and returns {item_id, position:{x,y,z}}; coordinates for items in hangars/stations are (0,0,0).
  • POST /characters/{character_id}/assets/names accepts 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's Director role, and use the corp-asset group.
  • 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 optional adjusted_price:number(double) and optional average_price:number(double); only type_id is required by OpenAPI. Do not assume either price field exists for every type.
  • Cache: x-cache-age: 3600, client/server TTL 3600, server mode ttl-based; response advertises Cache-Control, ETag, and Last-Modified, and accepts If-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_price fallback for a missing average_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-asset or meta.
  • X-Ratelimit-Limit: total tokens/window, e.g. 150/15m.
  • X-Ratelimit-Remaining: available tokens.
  • X-Ratelimit-Used: tokens consumed by the request.
  • Retry-After on 429, 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)

  1. Register the app and exact callback URL in the EVE Developers Portal. Generate a random state and retain it in the login session.

  2. Redirect to authorization_endpoint with URL-encoded parameters:

    response_type=code
    client_id=<client id>
    redirect_uri=<registered exact URL>
    scope=<space-separated requested scopes>
    state=<random CSRF value>
    
  3. Verify callback state, then POST form-urlencoded to token_endpoint with grant_type=authorization_code, code=<one-time-code>, and the registered redirect_uri, using HTTP Basic credentials client_id:client_secret. The current strict-conformance notice shows redirect_uri in the token body; the current SSO page's simplified Python example omits it, so send the registered value and test against the registered app.

  4. 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; send code_challenge = base64url(SHA-256(code_verifier)) without = padding, with code_challenge_method=S256.
  • Add code_challenge and code_challenge_method=S256 to 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 registered redirect_uri. Retain the verifier until exchange; verify state exactly as in the web flow. The strict-conformance notice is the reason to include redirect_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:

  • iss should be the EVE issuer (https://login.eveonline.com/ or, in some cases, login.eveonline.com; use metadata and accept only the documented EVE forms).
  • aud is an array containing the app client_id and static EVE Online; check both.
  • sub is CHARACTER:EVE:<character-id>; name is the character name; scp enumerates 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

  1. Public visibility/discovery only: an ID in /universe/structures proves the structure is in ESI's fully-public list (and, with filter, that the selected service is online). It does not enumerate private/ACL structures and does not prove this particular character can dock.
  2. 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 unconditional canDock boolean. Ship class, structure state, subsequent ACL changes and cache age still matter, especially for capitals.
  3. Observed docking: /characters/{character_id}/location requires esi-location.read_location.v1, is event-based with a 5-second cache, and returns required solar_system_id plus optional station_id or structure_id. A matching structure_id is 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.
  4. ACL membership: the Access List detail route returns allow_everyone plus character/corporation/alliance entries whose access is Unspecified, Allowed, Blocked, Manager, or Admin. It does not return structure_id, structure profile, docking service, or a computed per-character permission. Therefore membership alone cannot prove docking.
  5. 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 only ship_type_id, ship_item_id, and ship_name; it is 5-second TTL-cached and uses char-location 1200/15m. Assets likewise expose type_id, not a capital boolean.
  • Capital/supercapital classification therefore requires joining ship_type_id/asset type_id to 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_id plus 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. Asset location_id and type_id need separate name/static-data joins.
  • /markets/prices is 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_uri at exchange, while the official strict-conformance notice includes the registered redirect_uri; use the latter and verify in a registered test app before launch.

Sources kept