Errors

The API uses conventional HTTP status codes. Every error response has the same JSON envelope:

json
{
  "statusCode": 401,
  "message": "Invalid API key",
  "timestamp": "2026-07-05T12:00:00.000Z",
  "path": "/v1/menu"
}

Validation errors additionally carry an errors array with one entry per violated rule:

json
{
  "statusCode": 400,
  "message": "Validation failed",
  "errors": [
    "products.0.priceMinor must not be less than 0",
    "property unknownField should not exist"
  ],
  "timestamp": "2026-07-05T12:00:00.000Z",
  "path": "/v1/sync"
}

Status codes#

StatusMeaning
200Success (GET, PATCH)
201Success (POST — sync, publish, cleanup)
400Bad request — validation failed, plan limit exceeded, array too large, or an invalid/naive timestamp
401Unauthorized — missing, malformed or invalid API key
403Forbidden — Orders API endpoints on a free plan
404Not found — e.g. an order id that does not exist for your venue
422Unprocessable — invalid order status transition
429Too many requests — see Rate limits
500Internal server error — safe to retry with backoff

401 — Unauthorized#

messageCause
Missing API keyNo Authorization: Bearer ... header
Invalid API key formatToken does not start with dk_live_ / dk_test_
Invalid API keyUnknown, revoked or regenerated key
json
{
  "statusCode": 401,
  "message": "Missing API key",
  "timestamp": "2026-07-05T12:00:00.000Z",
  "path": "/v1/orders"
}

400 — Bad request#

Validation — the body or query violates the schema (wrong type, missing required field, unknown extra field, array over its per-request cap of 200 categories / 200 ingredients / 500 products):

json
{
  "statusCode": 400,
  "message": "Validation failed",
  "errors": ["categories must contain no more than 200 elements"],
  "timestamp": "2026-07-05T12:00:00.000Z",
  "path": "/v1/sync"
}

Timestamps without a timezone — date filters on GET /v1/orders must be ISO 8601 with an explicit timezone (Z or +hh:mm offset):

json
{
  "statusCode": 400,
  "message": "Parameter 'since' must include timezone (Z or +/-offset). Received: '2026-07-05T12:00:00'",
  "timestamp": "2026-07-05T12:00:00.000Z",
  "path": "/v1/orders"
}

Cleanup safety check — passing an empty array to POST /v1/cleanup without force: true:

json
{
  "statusCode": 400,
  "message": "Passing an empty array would affect ALL items. Set force: true to confirm this action.",
  "timestamp": "2026-07-05T12:00:00.000Z",
  "path": "/v1/cleanup"
}

Plan limits — a sync that would create more items than your plan allows is rejected with 400 before anything is written. See Plan limits for the caps per plan.

Plan-limit responses are generic today

Plan-limit rejections currently return a 400 with a generic message body. If a large sync fails with a 400 that is not Validation failed, check your item counts against your plan limits first.

403 — Forbidden#

The Orders API requires a paid plan. On the free Egg plan, GET /v1/orders, GET /v1/orders/:id and PATCH /v1/orders/:id/status are rejected (API keys and the Menu API work on every plan):

json
{
  "statusCode": 403,
  "message": "The Orders API requires a paid plan",
  "timestamp": "2026-07-05T12:00:00.000Z",
  "path": "/v1/orders"
}

404 — Not found#

Returned by the order endpoints when the id does not exist (or belongs to a different venue):

json
{
  "statusCode": 404,
  "message": "Order not found",
  "timestamp": "2026-07-05T12:00:00.000Z",
  "path": "/v1/orders/ord_123"
}

422 — Invalid status transition#

PATCH /v1/orders/:id/status enforces the order state machine. Repeating the current status is idempotent (200); an illegal jump is rejected:

json
{
  "statusCode": 422,
  "message": "Invalid status transition: 'completed' -> 'preparing'. Allowed transitions from 'completed': none (terminal state)",
  "timestamp": "2026-07-05T12:00:00.000Z",
  "path": "/v1/orders/ord_123/status"
}

429 — Too many requests#

json
{
  "statusCode": 429,
  "message": "ThrottlerException: Too Many Requests",
  "timestamp": "2026-07-05T12:00:00.000Z",
  "path": "/v1/orders"
}

Back off and retry — see Rate limits.

500 — Internal server error#

Something failed on our side. These are safe to retry with exponential backoff; if a 500 persists, contact support with the timestamp and path from the response.

Request size#

Request bodies are limited to 10 MB. Larger syncs should be split into multiple requests (the per-request array caps of 200/200/500 usually keep you far below this).

Handling errors#

bash
# -f makes curl exit non-zero on HTTP errors; -w exposes the status code
curl -sf https://api.duck-hub.com/v1/orders \
  -H "Authorization: Bearer dk_live_your_api_key" \
  -w "\nHTTP %{http_code}\n"