Skip to main content

API Reference

REST API for the AT1AS v2 server (Rust / Axum / Postgres). All responses are JSON.

  • Base URL (dev): http://127.0.0.1:8080 (use 127.0.0.1, not localhost)
  • Auth: session-token bearer header on protected routes: Authorization: Bearer <token> (obtained from register/login)
  • Bodies: Content-Type: application/json
  • :id params marked (id|code) accept either a numeric id or a text code (e.g. /api/station/terra or /api/station/1).

Error formatโ€‹

Every handler error returns:

{ "error": { "code": "...", "message": "..." } }
codeHTTPmeaning
bad_request400invalid input / rule violation
unauthorized401missing/invalid token or bad credentials
not_found404resource (or owned ship) doesn't exist
conflict409uniqueness clash (email/username)
internal500unexpected server/DB error

Note: an unmatched path currently returns a bare 404 with empty body (no global JSON fallback yet).

Legendโ€‹

๐Ÿ”“ public ยท ๐Ÿ”‘ requires auth ยท ๐Ÿ‘ค requires auth and ownership of the ship ยท โš“ ship must be docked ยท ๐Ÿญ station must offer the noted service


Auth & accountโ€‹

POST /api/user/register ๐Ÿ”“โ€‹

Create an account. Bootstraps a full starting state in one transaction: user + player character + wallet (50,000 โ‚ก) + a starter shuttle docked at Terra + 3 NPC crew (pilot/engineer/gunner).

// body
{ "email": "pilot@at1as.test", "password": "min8chars" }
// 200
{ "token","user_id","expires_at","character_id","credits":50000,
"ship": { "ship_id","name","hull","hp","station" },
"crew": [ { "character_id","name","role" } ] }

409 if the email is already registered.

POST /api/user/login ๐Ÿ”“โ€‹

{ "email","password" } // body
{ "token","user_id","expires_at" } // 200 ยท 401 on bad credentials

POST /api/user/logout ๐Ÿ”‘โ€‹

Deletes the presented session token. โ†’ { "ok": true, "ended": <n> }

POST /api/user/forgot ๐Ÿ”“โ€‹

{ "email" } โ†’ { "ok": true, "reset_token"? } (token returned only in dev; never reveals whether the account exists).

GET /api/user/oauth?provider=google ๐Ÿ”“โ€‹

Stub. Returns { provider, configured:false, status:"not_configured", authorize_url, redirect_uri }.

GET /api/user/me ๐Ÿ”‘โ€‹

The account: user + each character with wallet + ship count.

{ "user": { "id","email","username","status","created_at","last_login_at" },
"characters": [ { "character_id","name","kind","archetype",
"wallet": { "credits","tokens" }, "ship_count" } ] }

POST /api/user/username ๐Ÿ”‘โ€‹

{ "username" } (3โ€“32 chars) โ†’ { "ok":true, "username" } ยท 409 if taken.

POST /api/user/password ๐Ÿ”‘โ€‹

{ "current_password","new_password" } (new โ‰ฅ 8). Verifies the current password, then revokes all sessions. โ†’ { "ok":true, "sessions_revoked":true } ยท 401 if the current password is wrong.

GET /api/wallet ๐Ÿ”‘โ€‹

โ†’ { "character_id","credits","tokens" }

GET /api/standing ๐Ÿ”‘โ€‹

Faction reputation (signed; 0 = neutral). โ†’ { "standing": [ { "faction","name","value" } ] }

GET /api/transactions ๐Ÿ”‘โ€‹

The caller's ledger, most-recent first (capped 100). Each row: { id, ts, kind, item, qty, unit_price, credits_amount, station_id, role } (role = buyer/seller/party).


Shipsโ€‹

GET /api/ship/list ๐Ÿ”‘โ€‹

Every ship owned by the caller. โ†’ { "count", "ships": [ <ship object> ] }

GET /api/ship/:id ๐Ÿ”“โ€‹

Full ship object (see Ship object). 404 if missing.

POST /api/ship/buy ๐Ÿ”‘ โš“๐Ÿญshipyardโ€‹

Buy a hull from a colony's shipyard stock; the new ship docks at that colony.

{ "colony":"pallas", "hull":"gunship" } // body
{ "ship_id","hull","name","station","credits_spent","wallet_credits" }

400 if the colony has no shipyard service, no stock, or you can't afford it.

POST /api/ship/:id/rename ๐Ÿ‘คโ€‹

{ "name" } (โ‰ค48) โ†’ { "ship_id","name" }

POST /api/ship/:id/dock ๐Ÿ‘คโ€‹

{ "station": "(id|code)" } โ†’ { "ship_id","status":"docked","station":{id,code,name} }

POST /api/ship/:id/undock ๐Ÿ‘คโ€‹

โ†’ { "ship_id","status":"flying" }

POST /api/ship/:id/refuel ๐Ÿ‘ค โš“๐Ÿญrefuelโ€‹

Buy reaction mass / fusion fuel into the ship's tanks. Priced per-kg from item_types.base_price รท mass_kg_per_unit.

{ "remass_kg"?: 2000, "fusion_kg"?: 0 }
{ "ship_id","remass_kg","fusion_kg","credits_spent","wallet_credits" }

POST /api/ship/:id/provision ๐Ÿ‘ค โš“๐Ÿญmarketโ€‹

Stock the food / water tanks (life-support stores, separate from cargo/fuel); capped by the hull's food_cap_kg/water_cap_kg.

{ "food_kg"?: 4000, "water_kg"?: 5000 }
{ "ship_id","food_kg","water_kg","credits_spent","wallet_credits" }

POST /api/ship/:id/repair ๐Ÿ‘ค โš“๐Ÿญrepairโ€‹

Restore hp to max (50 โ‚ก/hp). โ†’ { "ship_id","hp","max_hp","credits_spent","wallet_credits" }

POST /api/ship/:id/transfer ๐Ÿ‘ค โš“โ€‹

Move cargo between the ship hold and the station hangar. Loading enforces the hull's cargo_cap_kg.

{ "item_type":"(id|code)", "qty":50, "direction":"hangar_to_hold"|"hold_to_hangar" }
{ "ship_id","item_type_id","qty","direction" }

POST /api/ship/:id/sell ๐Ÿ‘ค โš“๐Ÿญshipyardโ€‹

Sell the ship for 60% of hull base price. Requires an empty hold. Hull returns to the local yard's stock if the station backs a colony. โ†’ { "sold_ship_id","hull","credits_gained","wallet_credits","returned_to_yard","fleet_remaining" }


Outfittingโ€‹

All mutating refits are ๐Ÿ‘ค โš“๐Ÿญshipyard.

GET /api/ship/:id/loadout ๐Ÿ‘คโ€‹

โ†’ { ship_id, drive, drive_name, armor, hp, max_hp, hull, hull_size, slots, used_slots, weapons:[ { slot, weapon_id, name, class, damage, tier } ] }

Hull-size ladder: shuttle < light < gunship < cruiser < capital. Hardpoints by size: 1 / 2 / 3 / 4 / 6.

POST /api/ship/:id/armorโ€‹

{ "tiers"?: 1 } โ€” each tier +500 hp & max_hp, costs 5,000 โ‚ก. โ†’ { ship_id, armor, hp, max_hp, credits_spent, wallet_credits }

POST /api/ship/:id/driveโ€‹

{ "drive_type":"fusion_torch" } โ€” cost = 20,000 ร— drive tier. โ†’ { ship_id, drive, drive_name, credits_spent, wallet_credits }

POST /api/ship/:id/mountโ€‹

{ "weapon_id":"torpedo", "slot"?: 0 } โ€” cost = 10,000 ร— weapon tier. The weapon's min_hull must fit the hull; omit slot to use the lowest free hardpoint. โ†’ { ship_id, weapon_id, slot, credits_spent, wallet_credits }

POST /api/ship/:id/unmountโ€‹

{ "slot": 0 } โ€” refunds 5,000 ร— tier. โ†’ { ship_id, weapon_id, slot, refund, wallet_credits }


Crew & life supportโ€‹

GET /api/ship/:id/lifesupport ๐Ÿ‘คโ€‹

Age-scaled per-cycle ration need and how long the tanks last. โ†’ { ship_id, crew, food_kg, food_cap_kg, water_kg, water_cap_kg, food_need_per_cycle, water_need_per_cycle, cycles_of_supply, starve_cycles, starving } (cycles_of_supply = -1 when there's no crew.)

GET /api/ship/:id/crew ๐Ÿ‘คโ€‹

โ†’ { ship_id, crew:[ { character_id, name, role, age, status, wage_credits, share_bps } ] }

POST /api/ship/:id/crew ๐Ÿ‘ค โš“ (hire)โ€‹

Mints an NPC and berths them. Roles: pilot|engineer|gunner|medic|quartermaster|scientist|deckhand. Cap 8 active crew.

{ "role":"medic", "name"?: "Doc", "wage_credits"?: 0, "share_bps"?: 0 }
{ "character_id","name","role","ship_id" }

DELETE /api/ship/:id/crew/:character_id ๐Ÿ‘ค (fire)โ€‹

โ†’ { ship_id, character_id, fired:true }

POST /api/ship/:id/crew/:character_id/role ๐Ÿ‘คโ€‹

{ "role" } โ†’ { ship_id, character_id, role }


Movement is handled by a separate process โ€” the sector-server โ€” which integrates every flying ship's orbit in RAM and publishes live positions to Redis each tick. The API writes the order (destination, burn) to Postgres and pokes the engine; reads overlay the live Redis state on top of the durable Postgres row.

How travel works. Everything in the system is a function of time f(t) โ€” planets, moons, and stations all ride their orbits. When you set course the server:

  1. resolves the destination (a body like jupiter, a station code, or a raw point),
  2. solves an intercept: where the moving target will be when you get there,
  3. flies a flip-and-burn (brachistochrone): accelerate to the midpoint, flip, decelerate โ€” so you arrive at rest-relative, not screaming past,
  4. gates on fuel before committing: if you don't have the ฮ”v, the order is rejected and nothing changes.

Two knobs control the burn (mutually exclusive โ€” arrive_in_days wins):

  • accel_g โ€” throttle. Default is the drive's max thrust. A gentler burn costs far less fuel (ฮ”v โˆ โˆša) but takes longer.
  • arrive_in_days โ€” deadline. The server solves the burn needed to hit that arrival time (a = 4ยทd / ฯ„ยฒ) and rejects it if it exceeds the drive or the fuel.

Sim time vs. real time. The world runs at GAME_TIME_SCALE sim-seconds per real second (default 300ร—, so 1 real minute โ‰ˆ 5 sim-hours). ETAs are returned both ways: sim_* is the in-world journey, real_* is the wall-clock wait until you arrive.

POST /api/ship/:id/nav ๐Ÿ‘คโ€‹

Set course. Body โ€” to is required; supply either accel_g or arrive_in_days (or neither, for a max burn):

{ "to": "jupiter", // body code | station code | { "x": <i64>, "y": <i64> }
"accel_g"?: 0.3, // throttle in g (clamped to drive max)
"arrive_in_days"?: 12 } // deadline mode โ€” takes precedence over accel_g

Success โ€” a pre-flight quote (the voyage is now committed and the engine is flying it):

// POST /api/ship/:id/nav { "to":"mars", "accel_g":0.3 } โ†’ 200
{ "ship_id": 191154,
"destination": { "label":"mars", "kind":"body", "x":-83300123430114, "y":-212216799138352 },
"distance_km": 227980000, "distance_au": 1.524,
"burn_g": 0.3,
"dv_needed_ms": 1637945, "dv_available_ms": 3433987,
"eta": { "sim_seconds":556746, "sim_hours":154.7, "sim_days":6.44,
"real_seconds":1856, "real_minutes":30.9 } }

The fuel gate โ€” 400 when the transit needs more ฮ”v than the tanks hold (a high-g burn to a far body is very ฮ”v-hungry; ease off accel_g or refuel):

// POST /api/ship/:id/nav { "to":"jupiter" } (starter shuttle, max burn) โ†’ 400
{ "error": { "code":"bad_request",
"message":"insufficient ฮ”v for this transit: need 5525650 m/s, have 3433987 m/s" } }

Other 400s: unknown destination 'โ€ฆ', arrive_in_days must be > 0, and deadline too tight: needs N g, drive maxes at M g โ€” allow more days.

GET /api/ship/:id/nav ๐Ÿ‘คโ€‹

Live flight telemetry โ€” position/velocity overlaid from Redis, distance + ETA re-resolved against the destination's current orbital position. Docked/idle ships have no live entry and fall back to Postgres (no destination/eta block).

{ "ship_id": 191052, "status":"flying", "nav_status":"cruising", // idle|cruising|braking
"position": { "x":-109338127583560, "y":-206141698485491 }, // FixedCoord mm
"velocity": { "x":46488226, "y":-146293079 }, // mm/s
"speed_ms": 153502, "remass_kg": 3478, "fuel_ok": true,
"destination": { "x":-95838236731348, "y":-206857228010591 },
"distance_remaining_km": 13518840, "distance_remaining_au": 0.0904,
"eta": { "sim_seconds":0, "sim_hours":0, "sim_days":0, "real_seconds":0, "real_minutes":0 } }

POST /api/ship/:id/hold ๐Ÿ‘คโ€‹

Cut thrust and clear the course (cancel the voyage). โ†’ { "ship_id", "nav_status":"idle" }


The sector (live map)โ€‹

Reads off the sector-server's spatial hash grid in Redis (cells of CELL_SIZE_MM, default 1e13 mm โ‰ˆ 0.067 AU). Proximity is O(ships-in-region), not O(all ships) โ€” the engine can hold ~1M ships and these queries still touch only the cell window you ask for.

Area of Interest (AOI). view and sector/fleets register your viewport so the engine starts publishing that region's ships (TTL ~8 s; keep polling to hold it). Your own ships are always published, so /api/fleet works without registering. On the first call to a fresh region the answer can be sparse โ€” the ships appear a tick later, once the engine sees the AOI. Poll again.

GET /api/fleet ๐Ÿ”‘โ€‹

Your ships with live positions โ€” owner-scoped (never NPCs or other players), one call for a fleet panel/map.

{ "count": 1,
"ships": [ { "ship_id":191052, "name":"admiral's Shuttle", "hull":"shuttle",
"status":"flying", "nav_status":"cruising", "nav_label":"mars",
"x":-818116852, "y":-1157270781 } ] } // x,y = live, from Redis

GET /api/sector/view?x=&y=&radius_au= ๐Ÿ”‘โ€‹

Every individual ship in a region (the zoomed-in map). radius_km also accepted; default radius 0.5 AU. Registers the AOI.

{ "center": { "x":202000000000000, "y":202000000000000 }, "radius_au": 0.5,
"count": 2,
"ships": [ { "ship_id":1032, "x":201998โ€ฆ, "y":202001โ€ฆ, "nav_status":"cruising", "distance_au":0.012 } ] }

GET /api/sector/fleets?x=&y=&radius_au= ๐Ÿ”‘โ€‹

The region's ships grouped by owner โ€” the strategic "a bunch of fleets" overview (the LOD you draw when zoomed out: one labelled blip per fleet, sized by ship count, placed at its centroid). NPCs (no owner) collapse into one Unaligned (NPC) group. Default radius 1 AU. Registers the AOI.

{ "center": { "x":202000000000000, "y":202000000000000 }, "radius_au": 0.5,
"fleet_count": 3,
"fleets": [ // sorted by ship_count desc
{ "owner_character_id":0, "name":"Unaligned (NPC)", "ship_count":50,
"center": { "x":201929388874900, "y":202245301394534 } },
{ "owner_character_id":1, "name":"Vex Mara", "ship_count":30,
"center": { "x":202215708817011, "y":202001387415249 } },
{ "owner_character_id":2, "name":"Idris Calloway", "ship_count":20,
"center": { "x":201786097703245, "y":201858846516900 } } ] }

GET /api/ship/:id/contacts?radius_au=0.1 ๐Ÿ‘คโ€‹

Ships near this ship (sensor sweep), nearest first โ€” same grid query, centred on the ship's live position. radius_km also accepted; default 0.1 AU.

{ "center": { "x":-109336381594645, "y":-206147184618113 }, "radius_km":29920000, "count":0,
"contacts": [ { "ship_id":<i64>, "x":<i64>, "y":<i64>,
"distance_km":<f>, "distance_au":<f>, "nav_status":"cruising" } ] }

Stations & marketsโ€‹

GET /api/station/list ๐Ÿ”“โ€‹

โ†’ { count, stations:[ &lt;station object> ] }

GET /api/station/:id ๐Ÿ”“ (id|code)โ€‹

Full station object: { id, code, name, faction, sec_level, parent_body, orbit_radius_km, position, services[], fees{tax_bp,broker_fee_bp}, colony, ships_docked, open_order_count }.

GET /api/market/list ๐Ÿ”“โ€‹

Stations offering a market, with summaries. โ†’ { count, markets:[ { station, fees, open_order_count, items_traded } ] }

GET /api/market/:id ๐Ÿ”“ (id|code)โ€‹

A station's full order book, grouped per item with bids/asks and best bid/ask. โ†’ { station, fees, open_order_count, book:[ { item_type_id, code, name, base_price, best_bid, best_ask, bids[], asks[] } ] }

GET /api/market/resource/:id ๐Ÿ”“ (item id|code)โ€‹

Cross-market view for one commodity: where to buy (cheapest sells) and sell (highest buys). โ†’ { resource, best_buy_price, best_sell_price, buy_from:[ { station, best_price, total_qty, orders } ], sell_to:[ { station, best_price, total_qty, orders } ] }

GET /api/market/orders ๐Ÿ”‘โ€‹

The caller's own open resting orders. โ†’ { orders:[ { order_id, station, item, side, price, qty_remaining, qty_original, status, created_at } ] }

POST /api/market/buy ๐Ÿ”‘ ๐Ÿญmarketโ€‹

Instantly buy, crossing the cheapest asks (โ‰ค limit_price if given). Goods land in your hangar at that station.

{ "station":"(id|code)", "item":"(id|code)", "qty":100, "limit_price"?: 50 }
{ "filled","requested","credits_spent","avg_price","wallet_credits","delivered_to" }

POST /api/market/sell ๐Ÿ”‘ ๐Ÿญmarketโ€‹

Instantly sell from your hangar, crossing the highest bids (โ‰ฅ limit_price). โ†’ { filled, requested, credits_gained, avg_price, wallet_credits }

POST /api/market/order ๐Ÿ”‘ ๐Ÿญmarketโ€‹

Place a resting limit order with escrow (sell escrows goods, buy escrows credits).

{ "station","item","side":"buy"|"sell","price","qty" }
{ "order_id","side","price","qty","station","item","status":"open" }

POST /api/market/order/:id/cancel ๐Ÿ”‘โ€‹

Cancel your open order; refunds the escrow. โ†’ { ok:true, order_id, refunded_qty }


Inventoryโ€‹

GET /api/inventory/ship/:id ๐Ÿ‘คโ€‹

โ†’ { ship_id, name, cargo_cap_kg, used_mass_kg, items:[ { item_id, item_type_id, code, name, qty, unit, mass_kg } ] }

GET /api/inventory/station/:id ๐Ÿ”‘ (id|code)โ€‹

The caller's goods stored at a station hangar. โ†’ { station, items:[ โ€ฆ ] }


World (reads) ๐Ÿ”“โ€‹

GET /api/character/:idโ€‹

{ id, user_id, kind, name, archetype, religion, status, sex, age, home_colony, home_station, wallet, ship_count, crew_berth, created_at } ยท 404 if missing.

GET /api/colony/listโ€‹

{ count, colonies:[ { code, name, body, type, owner_corp, population, security, food_stock, station, corp } ] } (population desc).

GET /api/colony/:codeโ€‹

A colony plus trade: { produces:[], needs:[] }.

GET /api/faction/listโ€‹

{ count, factions:[ { id, code, name, description } ] }

GET /api/corp/list ยท GET /api/corp/:codeโ€‹

Corps; the fetch adds relations[] (corp_relations) and colonies[] owned.


Catalogs (reference data) ๐Ÿ”“โ€‹

EndpointReturns
GET /api/catalog/itemsevery item_types row (goods + hull stats)
GET /api/catalog/hullsships only โ€” hp, weapon, dry mass, cargo/food/water caps, price
GET /api/catalog/weaponsweapon catalog (class, tier, damage, range, guided, cooldown, ammo, min_hull)
GET /api/catalog/drivesdrives + their fuels[] and refuel_sources[]
GET /api/catalog/fuelsfuels + sources[]

Contracts (jobs)โ€‹

GET /api/contract/list?status=&type= ๐Ÿ”“โ€‹

Open contracts by default; filter by status/type. โ†’ { count, contracts:[ โ€ฆ ] }

POST /api/contract ๐Ÿ”‘โ€‹

Post a job. Escrows reward_credits from your wallet. Types: haul|mine|bounty|escort|salvage|supply.

{ "type","target"?,"pickup_location"?,"dropoff_location"?,"reward_credits","expires_at"? }
{ "contract_id","type","reward_credits","status":"open" }

POST /api/contract/:id/accept ๐Ÿ”‘โ€‹

Take an open contract (not your own). โ†’ { contract_id, status:"taken" }

POST /api/contract/:id/complete ๐Ÿ”‘โ€‹

Taker marks done and is paid the escrowed reward. โ†’ { contract_id, status:"done", reward_paid, wallet_credits }

Simplified: does not yet verify actual delivery (needs movement).

POST /api/contract/:id/abandon ๐Ÿ”‘โ€‹

Taker drops it; resets to open (escrow stays). โ†’ { contract_id, status:"open" }


Miscโ€‹

GET /healthz ๐Ÿ”“ โ†’ { ok:true, db:true }โ€‹

GET /ws ๐Ÿ”“ โ€” WebSocket upgrade (echo/hello for now; not yet a real channel)โ€‹


Ship objectโ€‹

Returned by GET /api/ship/:id and each entry of GET /api/ship/list. Keys are alphabetised (serialised from Postgres jsonb).

Position note: the position/velocity here come from Postgres โ€” the durable order plus a coarse checkpoint (~every few seconds). For a flying ship's live position use GET /api/ship/:id/nav or GET /api/fleet, which overlay the engine's per-tick Redis state.

{
"id", "name", "status", // docked|flying|destroyed
"owner": { "character_id","name","kind","user_id" } | null,
"hull": { "id","code","name","category","base_price","hp","weapon" },
"station": { "id","code","name" } | null,
"position": { "x","y" }, "velocity": { "x","y" }, // FixedCoord mm, mm/s
"mass": { "dry_mass_kg","cargo_cap_kg" },
"propulsion": { "drive_type","remass_kg","fusion_kg","dv_used" },
"provisions": { "food_kg","food_cap_kg","water_kg","water_cap_kg" },
"combat": { "hp","max_hp","armor","last_aggro_at" },
"starve_cycles",
"crew": [ { "character_id","name","role","age","status","wage_credits","share_bps" } ],
"weapons": [ { "slot","weapon_id","name","class","damage" } ],
"created_at", "updated_at"
}

Not yet implementedโ€‹

Mining & prospecting ยท combat (attack, kill-feed) ยท refine/manufacture ยท NPC market-maker (books are hand-seeded) ยท life-support consume tick ยท auto-dock on arrival at a station ยท sensor/fog-of-war gating on sector/fleets ยท real WebSocket channels + WS auth ยท OAuth callback ยท password-reset completion ยท pagination / idempotency / TLS.