Quickstart

From zero to a committed sandbox booking in one terminal session.

Everything below runs against the production engine with an instant self-serve sandbox key. No approval, no credit card, no real money, no real PHI — the sandbox is a confined synthetic world with simulated payments.

SignupPOST /developers/signup
Keyodk_sandbox_
First bookingS0 → S4

Step 1

Get a sandbox key.

One public, rate-limited call. The key is shown once — save it.

curl https://api.opendoc.com/developers/signup \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{ "email": "[email protected]", "name": "Your Name" }'

Response (201):

{
  "data": {
    "developerAccountId": "…",
    "apiKey": "odk_sandbox_…",        // shown ONCE — store it now
    "keyMode": "sandbox",
    "sandboxPatientId": "…",           // the synthetic patient your key acts as
    "sandboxProviderHsoId": "…",       // the ONE offering your key may book
    "docs": "https://docs.opendoc.com",
    "quickstart": "curl -H \"Authorization: Bearer <apiKey>\" https://api.opendoc.com/search?q=knee%20surgery",
    "note": "SANDBOX key: payments are simulated (non-live) and only the sandbox provider is bookable. …"
  }
}

Export the two values the rest of this page uses:

export OPENDOC_KEY="odk_sandbox_…"
export SANDBOX_OFFER_ID="<sandboxProviderHsoId from the response>"

Step 2

Understand what your key already carries.

Real patients must create a Health Key, promote it to IA2, and grant Layer-2 consent before transacting (the ceremony in the Auth guide). Signup already did all of that for your sandbox subject: it provisioned a synthetic patient with an active Health Key at IA2 and Layer-2 BOOK and CANCEL consent. The same gates that protect real patients run on every call — your sandbox subject simply passes them from the start.

Sandbox keys skip the grant ceremony. You do not create a Health Key, promote to IA2, or grant consent to run this quickstart. Use your odk_sandbox_ key as a Bearer token and go straight to the transaction flow. Building the ceremony itself is only needed when you integrate real patient identity.

Step 3

Know the stable sandbox fixtures.

The sandbox world is seeded with fixed IDs so integration tests can pin them. These are sandbox-only anchors — they never exist in live data, and a sandbox key can only book the sandbox offering.

SANDBOX_SCP_ID5a4d0000-0000-4000-8000-0000000005c9 — "OpenDoc Sandbox Clinic", the synthetic service counterparty.
SANDBOX_PROVIDER_ID5a4d0000-0000-4000-8000-0000000009d0 — "Sandy Sandbox, MD (SANDBOX)", the synthetic provider.
SANDBOX_HSO_SLUGsandbox-demo-visit — the bookable "Sandbox Demo Visit" service ($50.00 synthetic price).
sandboxProviderHsoIdThe rate-card (offer) ID returned by your signup response — this is the providerHsoId you pass to declare-intent.
Slots never run dry. When a sandbox key declares intent, the server mints a fresh open slot on the sandbox provider automatically — you may omit availabilitySlotId entirely.

Step 4 (optional)

Look around with public discovery.

Search and availability are public reads on the production engine. Useful for orientation; not required for the sandbox booking below, because signup already gave you the offer ID and the slot is auto-minted.

# Search the real provider directory
curl "https://api.opendoc.com/search?q=knee%20surgery" \
  -H "Authorization: Bearer $OPENDOC_KEY"

# Open slots for the sandbox provider (next 30 days by default)
curl "https://api.opendoc.com/providers/5a4d0000-0000-4000-8000-0000000009d0/availability"

Step 5

Drive the booking: S0 → S4.

Four state transitions, each with an idempotency key. This is the same state machine live mode runs — sandbox only swaps in the synthetic provider and simulated (non-live) payments.

1
Declare intent (S0 → S1).Creates the booking context against the sandbox offering.
2
Authorize (S1 → S2).Locks the price and returns the signed price-lock (JWS).
3
Accept terms (S2 → S3).Binds terms and prepares the (simulated) payment boundary.
4
Commit (S3 → S4).The atomic, irreversible booking step.
# S0 → S1: declare intent
curl https://api.opendoc.com/transactions/declare-intent \
  -X POST \
  -H "Authorization: Bearer $OPENDOC_KEY" \
  -H "Content-Type: application/json" \
  -d "{ \"providerHsoId\": \"$SANDBOX_OFFER_ID\", \"idempotencyKey\": \"qs-001-declare\" }"

# → 201 { "data": { "transactionId": "…", "state": "S1_intent",
#          "cashPriceCents": …, "platformFeeCents": …, "currency": "usd",
#          "hsoTitle": "Sandbox Demo Visit" } }

export TXN_ID="<transactionId from the response>"

# S1 → S2: authorize (price lock)
curl https://api.opendoc.com/transactions/$TXN_ID/authorize \
  -X POST \
  -H "Authorization: Bearer $OPENDOC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "idempotencyKey": "qs-001-authorize" }'

# → 200 { "data": { "transactionId": "…", "state": "S2_authorized",
#          "replayed": false,
#          "priceLock": { "jws": "…", "claims": { "max_obligation_cents": …, … } } } }

# S2 → S3: accept terms (payment boundary; simulated in sandbox)
curl https://api.opendoc.com/transactions/$TXN_ID/accept-terms \
  -X POST \
  -H "Authorization: Bearer $OPENDOC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "idempotencyKey": "qs-001-terms" }'

# → 200 { "data": { "transactionId": "…", "state": "S3_terms_accepted",
#          "replayed": false, "payment": { "mode": "non_live", … } } }

# S3 → S4: atomic commit
curl https://api.opendoc.com/transactions/$TXN_ID/commit \
  -X POST \
  -H "Authorization: Bearer $OPENDOC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "idempotencyKey": "qs-001-commit" }'

# → 200 { "data": { "transactionId": "…", "state": "S4_committed",
#          "replayed": false, "payment": { … } } }
The signed price-lock is the guarantee. The priceLock.jws returned by authorize is a signed claim of the maximum obligation for this transaction — verify and store it. Retrying any step with the same idempotencyKey replays the original transition ("replayed": true) instead of double-booking.

Step 6

Read the transaction state.

curl https://api.opendoc.com/transactions/$TXN_ID \
  -H "Authorization: Bearer $OPENDOC_KEY"

# → 200 { "data": { "transactionId": "…", "state": "S4_committed",
#          "booking": { "id": "…", "status": "…", "cashPriceCents": …,
#                       "currency": "usd", "scheduledFor": "…",
#                       "fulfilledAt": null, "canceledAt": null },
#          "payment": { … } } }

Next

Where to go from here.

GET /protocolThe live machine-readable contract: routes, tools, auth, event types. OpenAPI at GET /protocol/openapi.json.
TransactionsThe full S0–S8 state machine, idempotency, and readback details.
AuthThe real Health Key / IA2 / consent ceremony your sandbox key pre-satisfied.
EventsSubscribe to transaction.state_changed and friends over SSE or webhooks.
SDKThe TypeScript client that wraps everything above.