Errors
Every non-2xx response from the API carries a machine-readable error envelope. Handle failures on their type — an unknown result and an absent one are not the same thing, and a failed read is never an empty result.
The error body
Every error response is JSON with a single error object:
{
"error": {
"code": "invalid_body",
"message": "url and a non-empty events array are required",
"correlationId": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed"
}
}code— a stable, machine-matchable string. Branch on this, not onmessage.message— a human-readable explanation. It may name the offending field or scope. Treat its exact wording as unstable.correlationId— optional. When present, include it in support reports.
HTTP status codes
| Status | Meaning here |
|---|---|
400 | Malformed request — bad JSON, a missing required field, a bad tenant ref, or a missing Idempotency-Key on a money-mutating call. |
401 | No valid credential — the key/token is missing, malformed, unknown, or revoked. |
403 | Authenticated but not allowed — the credential lacks the route’s required scope, or an issuance would escalate scope. |
404 | No such resource for this tenant. Also returned cross-tenant, so a probe cannot distinguish “not yours” from “does not exist”. |
402 | Payment required — the payment provider declined the charge. |
409 | Conflict with current state — an illegal status transition, or an action refused because the order is already in a terminal or fulfilled state. |
412 | Precondition failed — the resource state changed, or was not in the state the action requires. |
422 | The request was well-formed but semantically unprocessable. |
429 | Rate limited — a verification or management limiter is spent. Respect the retry-after header. |
5xx | A server-side failure. 503 specifically means a dependency read was unavailable — it is an honest “we could not find out”, never a laundered empty result. Retry with backoff. |
Common error codes
These are real codes emitted by the platform today. The code is stable; match
on it. This is a representative set, not the exhaustive list.
| Code | Typical status | Meaning |
|---|---|---|
invalid_body | 400 | The request body failed validation. |
invalid_query | 400 | A query parameter is malformed or out of range. |
invalid_tenant | 400 | The tenant ref in the path is malformed. |
invalid_card | 400 | card.ccnumber and card.ccexp are required for this charge. |
invalid_asset_ref | 400 | A media ref is not asset:<storeKey>/<sha256>.<ext>. |
missing_idempotency_key | 400 | A money-mutating call arrived without the Idempotency-Key header. |
unauthorized | 401 | The credential is missing, unknown, or revoked. |
payment_declined | 402 | The charge was declined (payment or fraud floor). |
provider_declined | 402 | The payment provider declined the transaction. |
forbidden | 403 | The credential lacks the route’s required scope. |
scope_escalation | 403 | An issued key requested a scope the issuing key does not hold. |
not_found | 404 | No such resource for this tenant. |
order_not_found | 404 | No such order for this tenant/shopper. |
charge_not_found | 404 | No charge matches the supplied charge_id. |
gift_card_not_found | 404 | No gift card matches that code. |
coupon_not_found | 404 | The coupon code does not exist. |
coupon_inactive | 412 | The coupon exists but is not active. |
coupon_exhausted | 412 | The coupon has no redemptions left. |
invalid_transition | 409 | The requested state change is not legal from the current state. |
order_fulfilled_use_returns | 409 | A shipped/delivered order cannot be cancelled — use a return. |
insufficient_stock | 409 | The requested quantity exceeds available inventory. |
precondition_failed | 412 | The resource was not in the state the action requires, or changed concurrently. |
rate_limited | 429 | A verification or management limiter is spent; see retry-after. |
lookup_unavailable | 503 | A dependency read failed — this is not an empty result. |
internal_error | 500 | An unexpected server-side failure. |
Errors in the SDK — FlightdeckError
Every non-2xx response throws FlightdeckError. It carries the HTTP status and
the parsed body:
import { FlightdeckError } from "@dscodotco/sdk";
try {
const product = await store.products.get(slug);
return product;
} catch (err) {
if (err instanceof FlightdeckError && err.status === 404) {
return notFound(); // a real "no such product"
}
throw err; // a transport/5xx error — do NOT swallow into []
}FlightdeckError exposes:
| Property | Type | Meaning |
|---|---|---|
status | number | The HTTP status. |
body | unknown | The parsed error body — { error: { code, message, correlationId? } }. Branch on err.body.error.code. |
For example, distinguishing a decline from any other failure at checkout:
try {
const result = await store.checkout.submit(body, { idempotencyKey });
return confirm(result.order.id);
} catch (err) {
if (err instanceof FlightdeckError && err.status === 402) {
// e.g. err.body.error.code === "payment_declined"
return declined(err); // show "try another card"; do NOT auto-retry
}
throw err; // a real failure — never a fake success
}Do not turn a thrown error into an empty result.
catch { return [] }tells the caller “you have none of these” when the truth is “we could not find out.” Distinguish404(real absence) from every other failure (rethrow or show an error state). A503is explicitly “the read was unavailable”, never an empty catalog or a zero balance.
Rate limiting and 429
Some verification and management routes return 429 with a retry-after
header (in seconds) when a limiter is spent; the body’s code is rate_limited.
Handle 429 defensively everywhere — respect retry-after and back off —
rather than assuming any route’s current throttling is permanent. See
Pagination and rate limits for what is and is not
throttled today.