Files
2026-09-18 23:15:05 -04:00

299 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](./eve-openapi-evidence.json); live compatibility/changelog responses are [`eve-compatibility.json`](./eve-compatibility.json) and [`eve-changelog.json`](./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](https://developers.eveonline.com/docs/services/esi/overview/#versioning) · [live compatibility dates](https://esi.evetech.net/meta/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](https://developers.eveonline.com/docs/services/sso/#how-it-works)
- 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](https://esi.evetech.net/meta/openapi.json)
- 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](https://docs.esi.evetech.net/docs/scenarios/resolve_structure_ids.html) · [Current OpenAPI](https://esi.evetech.net/meta/openapi.json)
## ESI versioning and current OpenAPI
The live API exposes OpenAPI 3.1 at [`/meta/openapi.json`](https://esi.evetech.net/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`](https://esi.evetech.net/meta/compatibility-dates) response lists, newest first:
```text
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](https://esi.evetech.net/meta/changelog) · [Equinox ESI announcement](https://developers.eveonline.com/blog/equinox-on-esi-structures-sovereignty-and-access-lists)
## Assets: exact current contract
### Character assets
```http
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](https://developers.eveonline.com/docs/services/esi/pagination/x-pages/)
- 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):
```text
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
```http
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:
```text
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_id`s** 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](./eve-openapi-evidence.json)
## Reference-price valuation
```http
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](https://developers.eveonline.com/docs/services/esi/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](https://developers.eveonline.com/docs/services/esi/best-practices/#error-limit)
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`](https://login.eveonline.com/.well-known/oauth-authorization-server). On the check date it returned:
```json
{
"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](https://developers.eveonline.com/docs/services/sso/) · [SSO endpoint deprecations](https://developers.eveonline.com/blog/sso-endpoint-deprecations-2)
### 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:
```text
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](https://esi.evetech.net/meta/openapi.json)
### 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:
```json
{
"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)](https://docs.esi.evetech.net/docs/sso/refreshing_access_tokens.html) · [Rotation notice](https://developers.eveonline.com/blog/sso-endpoint-deprecations-2)
### 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](https://github.com/ErikKalkoken/eveauth/blob/main/jose.go) explicitly handles both scalar strings and arrays in `extractScopes`, with a [singleton regression test](https://github.com/ErikKalkoken/eveauth/blob/main/jose_test.go). 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](https://developers.eveonline.com/docs/services/sso/#validating-jwt-tokens)
## 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](https://developers.eveonline.com/docs/services/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
- [ESI overview/versioning](https://developers.eveonline.com/docs/services/esi/overview/) — compatibility-date and API contract rules.
- [Current ESI OpenAPI](https://esi.evetech.net/meta/openapi.json) — exact route, schema, scope, cache, pagination, and limiter extensions; captured with `X-Compatibility-Date: 2026-08-18`.
- [Compatibility dates](https://esi.evetech.net/meta/compatibility-dates) and [changelog](https://esi.evetech.net/meta/changelog) — live supported-date and 2026 route-release evidence.
- [ESI rate limiting](https://developers.eveonline.com/docs/services/esi/rate-limiting/) and [best practices/error limit](https://developers.eveonline.com/docs/services/esi/best-practices/) — new and legacy headers/costs.
- [X-Pages pagination](https://developers.eveonline.com/docs/services/esi/pagination/x-pages/) — page and cache-expiry behavior.
- [Current SSO docs](https://developers.eveonline.com/docs/services/sso/), [OAuth metadata](https://login.eveonline.com/.well-known/oauth-authorization-server), and [SSO deprecation/rotation notice](https://developers.eveonline.com/blog/sso-endpoint-deprecations-2) — auth code, PKCE, JWT, v2 endpoints, and refresh rotation.
- [Equinox ESI structures/access lists](https://developers.eveonline.com/blog/equinox-on-esi-structures-sovereignty-and-access-lists) — current 2026 structure/access-list release context.
- [Retained structure scenario](https://docs.esi.evetech.net/docs/scenarios/resolve_structure_ids.html) and [retained refresh contract](https://docs.esi.evetech.net/docs/sso/refreshing_access_tokens.html) — 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](https://developers.eveonline.com/docs/services/static-data/) — capital/type classification dependency; no archive downloaded.