These are the contracts that hold across every endpoint — the rules you can rely on before you read a single per-domain reference. Money is always integer cents. Tenancy is server-derived. Every route fails closed and loud. Where a convention is only partial or still gated, this page says so plainly rather than advertise a scheme that is not enforced yet.
Endpoints are either available in the sandbox or not yet available; an unavailable one fails closed with a typed 501/503 and zero side effects. Items labelled partial or gated mean the contract is incomplete or awaiting a named human/legal gate. These boundaries are how the rails stay honest.
Every amount crossing an API boundary or entering the WORM ledger is a conserving integer-cent value. Floating dollars never cross the boundary.
advanceAmountCents, availableCents, paidAmountCents, and the 835 reconciliation waterfall’s pool / fee / reserve / residual splits are all integer cents. The waterfall preserves the cent total exactly — the sum of the splits equals the pool, with no rounding drift.
The one documented convenience exception is claim inline-create: totalCharge may be supplied in dollars and is immediately stored as conserving integer cents. Nothing else accepts dollars. The nightly FBO reconciliation compares the ledger, the FBO position, and the rail in cents and raises a variance on any disagreement.
{
"instructionId": "si_7c1e",
"status": "SIGNED",
"advanceAmountCents": 42350,
"availableCents": 118900,
"waterfall": {
"poolCents": 42350,
"feeCents": 1271,
"reserveCents": 2118,
"residualCents": 38961
}
}// totalCharge may be supplied in dollars on inline-create only;
// it is stored immediately as conserving integer cents.
{
"patientId": "pat_44a1",
"totalCharge": 423.50
}
// → persisted as totalChargeCents: 42350Pass x-correlation-id on a request and it is echoed back on the response; omit it and one is generated for you. It is a trace id only — never an auth credential and never a tenant selector.
The value is echoed as a correlationId field on claim submit / status / appeal, and propagated across the auth, gate, and settlement surfaces so one trace id follows an encounter through to settled money. If you do not supply one, the server mints it — you never get an untraced request.
It carries no authority. A correlation id never authenticates you and never selects a tenant; tampering with it changes nothing but your logs. Tenancy comes from the session (see below), not from any header you send.
curl -X POST "https://your-sandbox-origin.example/api/claims/submit" \
-H "Content-Type: application/json" \
-H "x-correlation-id: 8b41d2c0-trace-4e11" \
-H "Cookie: $SHTEG_SESSION" \
-d '{ "claimId": "clm_0091" }'
# Response echoes: { "correlationId": "8b41d2c0-trace-4e11", ... }const res = await fetch("/api/claims/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-correlation-id": crypto.randomUUID(),
},
credentials: "include",
body: JSON.stringify({ claimId }),
});
const { correlationId } = await res.json();
// omit the header and the server generates one for youTwo real mechanisms keep writes exactly-once: a client-supplied idempotency key on claim submit, and server-side canonical WORM keys on every money movement.
(1) Client-supplied idempotency key. Pass an idempotencyKey on POST /api/claims/submit. A reused key returns 409 rather than creating a duplicate claim.
(2) Server-side canonical WORM keys. Money movement is idempotent on a canonical key you never have to supply. Settlement execution is keyed on the canonical instruction key and gated by an atomic SIGNED → DISBURSED compare-and-swap, so an instruction disburses at most once. ERA posting and the ledger post are idempotent on the source event / remittance id, so a re-delivery never double-books.
# Re-sending the same idempotencyKey returns 409, not a duplicate claim
curl -i -X POST "https://your-sandbox-origin.example/api/claims/submit" \
-H "Content-Type: application/json" \
-H "Cookie: $SHTEG_SESSION" \
-d '{ "claimId": "clm_0091", "idempotencyKey": "clm-0091-submit-01" }'{
"code": "IDEMPOTENCY_KEY_REUSED",
"message": "This idempotencyKey has already been submitted. No duplicate claim was created."
}Every route refuses loudly rather than degrading. One table for the whole developer surface — each code has a single, consistent meaning.
| Status | Meaning |
|---|---|
400 | Invalid JSON, schema violation, or a missing required field. |
401 | Authentication rejected the request, a webhook signature failed, or a job / SMART token is invalid. |
403 | Authenticated but not authorized — missing scope, or cross-tenant / out-of-clinic access. |
404 | No such resource in your tenant. Never a fabricated one — a miss is a miss, not a stub. |
409 | Idempotency-key reuse, or a not-executable instruction (wrong status, past TTL, non-positive advance, or an inactive pool). |
422 | Unprocessable — a scrub-blocked claim, an unsigned instruction, or an empty / unresolvable 835 or failed batch-wire validation. |
423 | Account lockout on sign-in after repeated failures. |
429 | Rate-limited — factor brute-force, step-up, or WebAuthn attempt limits exceeded. |
501 | An integration credential is unconfigured (EMR SMART/partner pull, FHIR store, CDS KB, EMR/fax/banking webhook secret) — zero network calls, no synthetic resource. |
502 | A transport threw during transmission. The route surfaces the failure — never a fabricated accept. |
503fail-closed | An unconfigured money / auth dependency, an absent HMAC signing secret, or a kill-switch / global-halt. Nothing is fabricated. |
501 means an integration is not wired; 503 means a dependency is down or halted. Both mean nothing was fabricated. Honest note: 402 is not emitted on this surface — it exists only on the patient-facing payments / checkout route, and is documented there, not here.Requests are automatically scoped to your organization from your credentials — you never pass an organization or clinic identifier. One clinic can never read or execute against another's data.
Every request is automatically scoped to your organization from your credentials. There is no clinicId parameter to spoof — cross-tenant reads and executions are structurally impossible, not merely checked.
Patient-touching claims and eligibility additionally enforce requireClinicPatient: the patient must be enrolled in the caller’s clinic. Webhook paths that carry a clinic scope (settlement / claims ERA) take it only from the signed body, never from an untrusted field, so the scope cannot be tampered independently of the signature.
# No clinicId anywhere — the session decides your tenant
curl "https://your-sandbox-origin.example/api/claims?status=SUBMITTED" \
-H "Cookie: $SHTEG_SESSION"
# A clinicId in the body is ignored, not honored:
curl -X POST "https://your-sandbox-origin.example/api/settlement/gate/enc_9f2a41" \
-H "Content-Type: application/json" \
-H "Cookie: $SHTEG_SESSION" \
-d '{ "clinicId": "someone_elses" }' # → still scoped to YOUR tenantEvery API request is authenticated before it reaches a handler — an unauthenticated request never reaches one. Moving money or writing a chart additionally requires a short-lived step-up claim.
Scopes are enforced inline per route: shtegpay:read/write claims:read/write eligibility:read integrations:read patients:read appointments:read.
Being signed in is not enough to move money or write a chart. Those routes gate on requireStepUp: a short-lived, tenant- and user-bound claim obtained from POST /api/auth/step-up and carried as the httpOnly shtegmed-step-up cookie or the x-shtegmed-step-up header.
# 1. Obtain a short-lived, tenant- and user-bound step-up claim
curl -X POST "https://your-sandbox-origin.example/api/auth/step-up" \
-H "Content-Type: application/json" \
-H "Cookie: $SHTEG_SESSION" \
-d '{ "factor": "webauthn" }'
# sets httpOnly cookie: shtegmed-step-up=...
# 2. Carry it into a money write (cookie or header)
curl -X POST \
"https://your-sandbox-origin.example/api/settlement/console/si_7c1e/execute" \
-H "Content-Type: application/json" \
-H "Cookie: $SHTEG_SESSION; shtegmed-step-up=$STEP_UP" \
-H "x-shtegmed-step-up: $STEP_UP" \
-d '{ "railReference": "RAIL-2026-071401" }'Reads are bounded, but there is no cursor / offset pagination contract yet. Document the caps that exist; do not imply a page cursor that does not.
GET /api/claims caps results at 100, newest-first, with ?patientId= and ?status= filters. The eligibility roster is a bounded batch with a concurrency control. FHIR search returns standard R4 searchset Bundles whose paging is governed by the CapabilityStatement.
A first-class cursor / next-link pagination scheme across list endpoints is aspirational and not yet built. Until it ships, page against the caps above; there is no next cursor to follow.
# Newest-first, capped at 100, filtered by patient + status
curl "https://your-sandbox-origin.example/api/claims?patientId=pat_44a1&status=SUBMITTED" \
-H "Cookie: $SHTEG_SESSION"
# No next-cursor is returned — the cap is the contract today.There is no /v1 URL prefix or API-version header today; routes are unversioned at their real paths. The living contracts are the artifacts that already exist.
The authoritative interaction list is the FHIR CapabilityStatement at GET /api/fhir/metadata — it wins over any prose on this site. The CDS Hooks discovery document and the developer changelog are the other living contracts.
A formal semver’d API version and a deprecation policy are gated / aspirational. We state that plainly rather than advertise a version scheme that is not enforced: there is no version to pin against today.
# The CapabilityStatement is the authoritative interaction list
curl "https://your-sandbox-origin.example/api/fhir/metadata" \
-H "Accept: application/fhir+json"
# Unversioned path; no X-API-Version header is required or honored.Real, enforced per-user limits exist on the authentication factor surface. There is no global per-tenant quota or X-RateLimit-* header contract across the data / money APIs.
Step-up factor verification, WebAuthn registration / authentication, and MFA checks return 429 under brute-force, and repeated sign-in failures trigger a 423 account lockout. These are real, enforced, per-user limits.
There is no documented global per-tenant request quota and no standard X-RateLimit-* response-header contract across the data / money APIs. We will not fabricate quota headers — a global quota is aspirational and labelled as such.
{
"code": "RATE_LIMITED",
"message": "Too many verification attempts. Wait before retrying."
}{
"code": "ACCOUNT_LOCKED",
"message": "Account temporarily locked after repeated failed sign-ins."
}Every webhook is inbound, arriving from an integration into the rail. There is deliberately no outbound customer-subscribable event dispatcher and no SSE / streaming endpoint. To learn a long job finished, you poll a status route.
Payment processors, EMR vendors, banking partners, and telephony providers connect behind the rail as replaceable integrations, alongside token-verified scheduled jobs. Each inbound source is signature- or token-verified before any work is done. There is no event you can subscribe to from the outside and no stream to hold open.
To follow a long-running job, poll its status route — for example GET /api/fhir/bulk-status/[jobId]: 202 while running (with Retry-After), then 200 with a manifest when done. When an outbound signed-delivery surface is built, it will be documented with a real signing contract — not before.
# 202 while running, 200 with a manifest when done
while :; do
status=$(curl -s -o body.json -w '%{http_code}' \
"https://your-sandbox-origin.example/api/fhir/bulk-status/job_7f13" \
-H "Cookie: $SHTEG_SESSION")
[ "$status" = "200" ] && break
sleep "$(cat retry-after 2>/dev/null || echo 5)" # honor Retry-After
done{
"code": "IN_PROGRESS",
"jobId": "job_7f13",
"retryAfterSeconds": 5
}Integer cents, server-derived tenancy, idempotent money movement, authentication plus step-up, and the fail-closed status codes are available in the sandbox today. Cursor pagination, a semver’d API version, and a global rate-limit / outbound-dispatcher contract are honestly labelled partial or gated. An unavailable endpoint fails closed with a typed 501/503 and zero side effects.