VetoVetoDocs
Hosted REST API

Errors & conventions

The single protocol error shape, status-code semantics, idempotency, ID prefixes, and the money rule shared by every hosted REST endpoint.

The error shape

Every error response — on every route — has exactly one shape:

{
  "reason_codes": ["SCREAMING_SNAKE_CASE", "…"],
  "error_human": "one-line, non-load-bearing explanation"
}

reason_codes is the stable, machine-keyable vocabulary; agents and clients branch on the codes. error_human is for humans and may change — never parse it. Checkout/policy codes come from the SDK reason-code registry; transport and auth codes are the API-plane additions below.

API-plane reason codes

CodeTypical statusMeaning
UNAUTHENTICATED401No API key presented.
INVALID_API_KEY401Unknown or revoked key.
INVALID_SESSION401Bad/expired management session.
INSUFFICIENT_SCOPE403Key lacks a required scope.
LIVE_KEY_REQUIRED403Live-only route, test key.
TEST_MODE_RAIL_FORBIDDEN403Test-only route, live key.
VALIDATION_FAILED400Request body failed validation.
BAD_REQUEST400Body was not valid JSON.
PAYLOAD_TOO_LARGE413Body exceeded 256 KB.
RATE_LIMITED429Too many requests (e.g. sign-in).
NOT_FOUND404No such resource / no route.
DATABASE_UNAVAILABLE503Dependency down (readiness).
INTERNAL_ERROR500Unexpected server error.

Status-code semantics

Status codes carry meaning — the table on each endpoint page is authoritative, but the defaults are:

StatusMeaning
200OK (read or idempotent write).
201Created (a new row / version).
204Done, no body (soft-delete).
304Not modified (manifest ETag match).
400Malformed body / failed validation.
401 / 403Auth / scope / mode failure.
404Not found (also: cross-mode access).
409Uniqueness conflict (slug / SKU taken).
410Gone (expired magic link).
422Semantically rejected (e.g. config invalid on publish).
429Rate limited.
503Not ready (dependency down).

Money

Money is an exact decimal string on the wire and in storage — never a float, never a number. Prices are stored as numeric(20,8) verbatim and round-tripped byte-for-byte.

{ "price": { "amount": "25.00", "currency": "USD" } }

A price amount must match ^\d{1,12}(\.\d{1,8})?$ (≤ 8 decimal places). A malformed amount is rejected with PRICE_AMOUNT_INVALID.

Idempotency

The reporting path is idempotent by resource key, so a self-hosted SDK can retry safely:

  • POST /v1/ingest/session upserts on session.id.
  • POST /v1/ingest/order is idempotent on session.id; a replay returns the original 201 with "replayed": true and runs the reputation delta at most once.

For interactive writes (merchant/product/policy create), idempotency comes from uniqueness constraints — re-creating a taken slug or SKU returns 409, not a duplicate.

The CORS preflight advertises an Idempotency-Key header for clients that send one.

ID prefixes

IDs are opaque, roughly time-sortable, prefixed strings (<prefix>_<base32>):

PrefixResource
mrch_Merchant
prod_Product
pol_Policy version
ord_Order
rcpt_Receipt
evt_Webhook event
wha_Webhook delivery attempt
key_API key
proj_ / org_Project / org