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 on message.
  • 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

StatusMeaning here
400Malformed request — bad JSON, a missing required field, a bad tenant ref, or a missing Idempotency-Key on a money-mutating call.
401No valid credential — the key/token is missing, malformed, unknown, or revoked.
403Authenticated but not allowed — the credential lacks the route’s required scope, or an issuance would escalate scope.
404No such resource for this tenant. Also returned cross-tenant, so a probe cannot distinguish “not yours” from “does not exist”.
402Payment required — the payment provider declined the charge.
409Conflict with current state — an illegal status transition, or an action refused because the order is already in a terminal or fulfilled state.
412Precondition failed — the resource state changed, or was not in the state the action requires.
422The request was well-formed but semantically unprocessable.
429Rate limited — a verification or management limiter is spent. Respect the retry-after header.
5xxA 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.

CodeTypical statusMeaning
invalid_body400The request body failed validation.
invalid_query400A query parameter is malformed or out of range.
invalid_tenant400The tenant ref in the path is malformed.
invalid_card400card.ccnumber and card.ccexp are required for this charge.
invalid_asset_ref400A media ref is not asset:<storeKey>/<sha256>.<ext>.
missing_idempotency_key400A money-mutating call arrived without the Idempotency-Key header.
unauthorized401The credential is missing, unknown, or revoked.
payment_declined402The charge was declined (payment or fraud floor).
provider_declined402The payment provider declined the transaction.
forbidden403The credential lacks the route’s required scope.
scope_escalation403An issued key requested a scope the issuing key does not hold.
not_found404No such resource for this tenant.
order_not_found404No such order for this tenant/shopper.
charge_not_found404No charge matches the supplied charge_id.
gift_card_not_found404No gift card matches that code.
coupon_not_found404The coupon code does not exist.
coupon_inactive412The coupon exists but is not active.
coupon_exhausted412The coupon has no redemptions left.
invalid_transition409The requested state change is not legal from the current state.
order_fulfilled_use_returns409A shipped/delivered order cannot be cancelled — use a return.
insufficient_stock409The requested quantity exceeds available inventory.
precondition_failed412The resource was not in the state the action requires, or changed concurrently.
rate_limited429A verification or management limiter is spent; see retry-after.
lookup_unavailable503A dependency read failed — this is not an empty result.
internal_error500An 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:

PropertyTypeMeaning
statusnumberThe HTTP status.
bodyunknownThe 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.” Distinguish 404 (real absence) from every other failure (rethrow or show an error state). A 503 is 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.