API reference
Base URL in dev: the Vite server proxies same-origin /api/* and /auth/* to the backend (http://localhost:8743) — call these paths from the web origin (http://localhost:5873), not the backend port directly, or CORS/cookies won't line up. In prod (just prod) everything is same-origin already.
All responses are JSON. There is no API key / bearer auth for /api/* — auth state is a single server-side character session tied to your login cookie (see Auth); this is a single-user local tool, not a multi-tenant API.
Board
GET /api/health
Liveness + current config snapshot.
{"ok": true, "region_id": 10000002, "hub_station_id": 60003760,
"selected_hub_id": 60003760, "sso_configured": true}GET /api/me
Current character session, derived fees, own-order sync health.
{"sso_configured": true, "logged_in": true, "char_id": 123, "char_name": "...",
"scopes": ["esi-markets.read_character_orders.v1", "..."],
"skills_verified": true,
"fees": {"broker_pct": 2.0, "sales_tax_pct": 7.5, "relist_discount_pct": 50.0,
"round_trip_min_spread_pct": 10.27, "derived": true},
"own_order_count": 4,
"hub_owner": {"location_id": 1049588174021, "owner_corp_id": 98765, "system_faction_id": null, "name": "..."},
"structure_scopes_ok": true,
"orders_last_at": 1732481234.5, "orders_error": null}derived: false means you're logged out (or skill-ID verification failed) and fees are the FLIPDESK_DEFAULT_* placeholders instead of your live skills/standings.
GET /api/book/status
Ingest health: orderbook snapshot age, scan/refresh counters, ESI client stats (fetches / cache hits / 304s / errors / rate-limit headroom), price-history status, and the most recent refresh diff ({added, removed, updated, total, hub, at, seq, cached}) that drives the "synced +N −N ~N" pill in the UI. Poll this to build any external dashboard on top of the ingest loop.
GET /api/opportunities?limit=100
The spread-scanner output for the currently selected hub, ranked by score. limit caps at 500. Each item:
{"type_id": 34, "name": "Tritanium", "best_bid": 4.5, "best_ask": 4.8,
"bid_depth": 120000, "ask_depth": 95000, "gross_spread": 0.3, "gross_pct": 6.67,
"net_per_unit_flip": 0.12, "net_per_unit_cap": -0.02, "flip_net_pct": 2.5,
"executable_qty": 95000, "isk_outlay": 456000.0, "score": 118.4,
"kind": "flip_only", "computed_at": 1732481200.0,
"hist": {"samples_n": 12, "momentum_pct": 1.3, "spread_stable": true}}kind is one of capture_only / flip_only — see the README's opportunities table section for what each means and how it maps to the UI badges. Response also carries fees_used (the rates applied to every row) and maker (the liquidity/spread gates currently configured — see FLIPDESK_MAKER_* in configuration.md).
GET /api/reprice
Reprice suggestions for your own open orders at the selected hub. Empty / logged_in: false if not authenticated. Each item's action is modify / recreate / hold / cancel — see architecture.md#reprice-advisor for the decision logic, or the README's reprice queue section for the short version.
GET /api/trends?limit=100
Investment recommender over self-collected price history. items is empty (with a populated history block explaining why) until FLIPDESK_TREND_MIN_SAMPLES worth of samples exist for a type. signal is one of rising / dip / wide / watch.
GET /api/history?type_id=34&hours=24
Top-of-book time series for one type, for sparklines. Returns {type_id, points: [{ts, bid, ask, mid}, ...]}.
Hubs
GET /api/hubs
{"selected_hub_id": 60003760,
"npc_hub": {"structure_id": 60003760, "name": "Jita IV - Moon 4 - Caldari Navy Assembly Plant",
"system_id": 30000142, "system_name": "Jita", "kind": "npc"},
"citadels": [{"structure_id": 1049588174021, "name": "...", "system_id": ..., "system_name": "...", "kind": "citadel"}]}POST /api/hubs/select
Body {"structure_id": 1049588174021}. Switches the board's scan target. Returns {"ok": true, "selected_hub_id": 1049588174021} or {"ok": false, "selected_hub_id": null} if the id isn't a known hub yet — resolve it first with /api/hubs/add.
POST /api/hubs/add
Body {"structure_id": 1030049082711}. Resolves an unknown structure via your character's token and adds it to the hub list (then selects it). Requires login and the structure market-scan scope. Failure modes surface as {"ok": false, "error": "..."} with a human-readable reason (not logged in, missing scope, no ESI access to that structure, or an unresolvable id).
Refresh
POST /api/refresh
Wakes the ingest loop for an immediate pull + scan of the currently selected hub, bypassing the normal refresh cadence for this one call. Still respects Expires/ETag on the actual ESI request — this doesn't bypass the anti-spam guarantees, it just stops waiting for the next scheduled tick. Returns {"ok": true} immediately; poll /api/book/status (pulling / refresh.seq) to see it land.
Auth (/auth/*)
PKCE-based EVE SSO flow. There's no server-side session store — state + PKCE verifier ride in a short-lived HttpOnly cookie during the handshake, and the resulting refresh token is persisted (rotated on every use) for the single character session.
GET /auth/login— redirects to the EVE SSO authorize URL. No-ops (redirects back with?login=unconfigured) ifFLIPDESK_ESI_CLIENT_ID/_SECRETaren't set.GET /auth/callback— SSO redirect target; exchanges the code, persists the token, primes skills/standings/own-orders, redirects back to the board.GET|POST /auth/logout— clears the character session.
You never call these directly from code — the frontend's log in with EVE SSO button just navigates to /auth/login.