Skip to main content

Quickstart

This walks through the dock-control surface end to end against the sandbox: a reference implementation of the dock-control API that drives deterministic fake hardware. Every sample below is a self-contained TypeScript file: copy it, run it with tsx, and it talks to the real sandbox over plain fetch. No SDK, no shared helper library.

What you need

  • Base URL: https://outpost.earthity.com/api/sandbox. The sandbox serves the exact same paths, request/response shapes, and error catalog as production; only the base URL and the dock id differ. Production’s is https://outpost.earthity.com/api — every path below hangs off whichever base you point at.
  • Auth: an Outpost API key, sent as Authorization: Bearer opk_<key>. The same credential works here and in production — only the base URL changes. Two ways to get one:
    Or sign in and mint an org key from Integration in the dashboard, which also commands your real docks. Set it as OUTPOST_API_KEY before running any sample below. An anonymous key authenticates against production too, but owns no docks, so production answers every dock with 404 not_found rather than 401, exactly as if the dock did not exist. If your key works on the sandbox and 404s on production, that is the signal to mint an org key. The mint request is documented at Mint a sandbox API key.
  • Per-organization state: command state and idempotency keys are scoped to your organization (to the key itself, for an anonymous key). Two organizations can use the same Idempotency-Key without colliding, and neither can read the other’s commands; two keys from one organization share one view.
  • Compressed timeline: the sandbox resolves a command in about a second (queued for the first ~300ms, executing until ~1200ms, terminal after) instead of the roughly 45 seconds a command against a real dock takes end to end (the door itself travels for about 20 seconds). Poll intervals and deadlines below are sized for that compressed timeline; widen them against a real dock.
  • One dock, many scenarios: the sandbox has a single dock, sandbox-dock. Which case it plays out is chosen per request with the optional Sandbox-Behavior header — an outcome, a 409, or a command that stays in flight for 45s — so you can exercise every status, outcome and error with zero physical hardware. Omit the header and you get the happy path. Production ignores this header, which is why the selector is a header rather than a body field: nothing you leave in a copied request can change what a real dock does. The full table is at the bottom of this page.
  • One command at a time: the dock runs one command at a time, per organization. Fire two samples back to back inside the same second and the second may answer 409 dock_busy — that is the single-flight guarantee doing its job, not a sandbox defect. Wait for the first to reach a terminal status, or use a Sandbox-Behavior that takes no lock (busy).

Send a command

Expected output (command ids will differ):
A command send returns 202 Accepted immediately; the physical stroke hasn’t happened yet. The body carries a commandId to poll, and the Location header points at that same command’s own resource. The Idempotency-Key header is required: omit it and the request is rejected with 400 validation_failed (see error anatomy below).

Poll to a terminal outcome

Expected output (command ids will differ):
A command moves through queued -> executing -> a terminal state (succeeded or failed, each carrying an outcome). Poll GET .../commands/{commandId} on a short interval until you observe a terminal status, or your own deadline elapses. Never assume the first GET after a 202 is already terminal; treat every non-terminal response as expected, not an error.

Failure is a terminal outcome, not an HTTP error

Expected output (command ids will differ):
Sandbox-Behavior: obstructed resolves failed/OBSTRUCTED: the actuation didn’t complete. The GET that reports that is still HTTP 200: the command lifecycle succeeded (it reached a terminal state), independent of whether the actuation itself did. Decide success or failure from the response body’s status/outcome fields, never from the HTTP status code alone.

Handle dock_busy

Expected output (command ids will differ):
A dock runs one command at a time. Sending a command to a dock that already has one in flight returns 409 dock_busy, a problem+json body, not a 2xx. The right response is to back off and retry later with a fresh Idempotency-Key; retrying with the same key just replays into the same busy dock.

Retry safely with idempotency

Expected output (command ids will differ):
The key covers exactly one failure: a lost response. Resend the identical request with the same key and you get the original 202 back verbatim — same commandId, replayed: true, and the door does not move a second time. That is what makes a retry safe when you never saw the answer. Once you hold a commandId, the key has done its job: poll the command from then on. Reusing a key for a different command is not an error — it issues a new command, with its own commandId. The key identifies one attempt at one request, not a slot you have to keep clear. So open-dock-42 stays usable for the next open and for a later close, and you never have to invent a fresh string to command a door you have commanded before.

Error anatomy

Expected output (command ids will differ):
Every error is an RFC 9457 application/problem+json body; see Problem anatomy for the full member list, and Errors for what each code means and how to recover from it.

Sandbox-Behavior reference

The sandbox resolves exactly one dock id, sandbox-dock. Every scenario below is deterministic; send the header value that exercises the behavior you’re testing, or omit the header for the happy path. An unrecognized value returns 400 validation_failed rather than falling back to the happy path — a silently ignored typo would mean rehearsing a success while believing you rehearsed a jam. The header is part of the request, so the same Idempotency-Key with a different behavior is a separate command with its own commandId, not a replay — rehearsing a jam after rehearsing a success never hands you back the success. Any dock id other than sandbox-dock returns 404 not_found. The seven sbx-* ids this sandbox used to expose (sbx-endstop, sbx-obstructed, …) were retired on 2026-08-13 and now 404 like any unknown id: in production a dock id says which door, and the outcome comes back from the hardware, so encoding the outcome in the id taught the wrong model.