# Response shapes and errors (/api-contract) Every JSON response wraps its result in an envelope. Reading the envelope correctly is the difference between a robust integration and one that reports success on a failed order — because a few failures on this API arrive inside a `200`. **Check `status` before you trust the payload.** An HTTP `200` is not on its own proof that the operation succeeded. See [Failures that arrive as 200](#failures-that-arrive-as-200). ## The two envelopes [#the-two-envelopes] Order operations use one shape and the remaining operations use another. They overlap, so code against the fields you need rather than assuming one shape everywhere. | Field | Orders | Conversions, automations, fonts, templates, address book, profile | Meaning | | -------------- | :----: | :---------------------------------------------------------------: | ---------------------------------------------------------------------------------------------------------------------- | | `payload` | ✓ | ✓ | The result. Always present; check `errors` before reading it. | | `errors` | ✓ | ✓ | Array of human-readable messages. Empty on success. | | `hasErrors` | ✓ | ✓ | Read-only, true when `errors` is non-empty. | | `warnings` | ✓ | ✓ | Non-fatal messages. The operation still succeeded. | | `hasWarnings` | ✓ | ✓ | Read-only, true when `warnings` is non-empty. | | `errorDetails` | ✓ | — | Structured error objects. **The shape varies by operation**, so treat it as opaque unless that operation documents it. | | `profile` | — | ✓ | The account the API key belongs to. | | `metadata` | — | ✓ | Free-form. Populated only where the operation says so — see below. | | `orderId` | ✓ | — | The order the call was about, or `0` when the operation is not about one order. | | `status` | ✓ | — | The **call's** outcome. Not the order's status. | ### `metadata` is usually empty [#metadata-is-usually-empty] Only **create conversions** populates `metadata`, where it carries validation counts (`TotalRecords`, `ValidRecords`, `InvalidRecords`, and per-identifier counts). Everywhere else it is an empty object — including the conversions list and conversion status operations. Do not look for totals or paging in it. ### `orderId` of `0` means "not applicable" [#orderid-of-0-means-not-applicable] Operations scoped to one order set it. The order listing and the multi-order analytics and transaction operations leave it at `0`. That is not order 0 — read each item's own identifier from the payload instead. ## Failures that arrive as 200 [#failures-that-arrive-as-200] Three cases return a success status code for a request that did not fully succeed. **1. `status` is not `"Success"`.** On order operations this field carries `"Success"` or `"Error"`. On the per-order analytics and transaction operations it can also carry `"Error: "` followed by the underlying exception message, for an individual order that failed while others succeeded. Always branch on it: ```js const body = await response.json(); if (body.status && body.status !== "Success") { throw new Error(`Order ${body.orderId} failed: ${body.status}`); } ``` **2. `errors` is non-empty.** Partial-success operations report per-record problems here while still returning `200`. Creating conversions is the clearest example: it processes the valid records and tells you about the invalid ones. **3. A filter matched nothing.** Filter values on the order listing are not validated — they are compared for equality. A misspelled status returns `200` with an empty list, not a `400`. If you get an empty list unexpectedly, check the spelling against [the published values](/api-reference/orders/v1-order-get) before assuming the account has no such orders. ## Status codes [#status-codes] | Code | What it means | Body | | ----- | ----------------------------------------------------------------------- | ---------------------------------------------------------------- | | `200` | Request was accepted. **Still check `status` and `errors`.** | Envelope, except the proof operations — see below | | `400` | Malformed request, an unparseable value, or a batch over its size limit | Envelope, with the reason in `errors` | | `401` | API key missing or invalid | Varies: an envelope on some operations, a plain string on others | | `403` | Your subscription tier does not include this feature | **None** — see the warning below | | `404` | The referenced object does not exist, or is not yours | Envelope | | `409` | Conflicts with the object's current state | Envelope | | `422` | Nothing in the request was usable | Envelope, with detail in `metadata` and `warnings` | | `500` | Unhandled server error | Usually none | **A bodiless `500` on an entitlement-gated operation is a permissions failure, not a transient one.** When your subscription tier lacks API access the intended response is a `403`, but it currently surfaces as a `500` with an empty body. Do not retry it — retrying will fail identically. Check your plan, or contact support. This is a known defect and is being fixed; a `403` with no body means the same thing. ## Not every response is JSON [#not-every-response-is-json] The two proof operations return the PDF itself — `application/pdf`, raw bytes. Write the body to a file; do not parse it. Their **error** responses are still JSON, so branch on the status code before deciding how to read the body: ```js const response = await fetch(url, { headers: { "X-API-KEY": key } }); if (response.ok) { await writeFile("proof.pdf", Buffer.from(await response.arrayBuffer())); } else { const { errors } = await response.json(); throw new Error(errors.join("; ")); } ``` ## Enumerated values [#enumerated-values] Fields with a fixed set of accepted or returned values publish that set in the reference, in the exact spelling the API uses. Two things to know: * **Order status uses spaced display text**, not identifiers: send `"Ready For Production"`, not `ReadyForProduction`. One value breaks the pattern and has no space — `InShipping` — which is why you should copy values from the reference rather than deriving them. * **`product` is deliberately not enumerated.** It is a server-defined list that changes as products are added and retired, so treat it as an opaque string and do not switch exhaustively on it.