# 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.