299 lines
28 KiB
Markdown
299 lines
28 KiB
Markdown
# 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.
|