LettrLabs API

Response shapes and errors

The two response envelopes, how to tell success from failure, and the error cases that do not look like errors.

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.

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.

FieldOrdersConversions, automations, fonts, templates, address book, profileMeaning
payloadThe result. Always present; check errors before reading it.
errorsArray of human-readable messages. Empty on success.
hasErrorsRead-only, true when errors is non-empty.
warningsNon-fatal messages. The operation still succeeded.
hasWarningsRead-only, true when warnings is non-empty.
errorDetailsStructured error objects. The shape varies by operation, so treat it as opaque unless that operation documents it.
profileThe account the API key belongs to.
metadataFree-form. Populated only where the operation says so — see below.
orderIdThe order the call was about, or 0 when the operation is not about one order.
statusThe call's outcome. Not the order's status.

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"

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

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:

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 before assuming the account has no such orders.

Status codes

CodeWhat it meansBody
200Request was accepted. Still check status and errors.Envelope, except the proof operations — see below
400Malformed request, an unparseable value, or a batch over its size limitEnvelope, with the reason in errors
401API key missing or invalidVaries: an envelope on some operations, a plain string on others
403Your subscription tier does not include this featureNone — see the warning below
404The referenced object does not exist, or is not yoursEnvelope
409Conflicts with the object's current stateEnvelope
422Nothing in the request was usableEnvelope, with detail in metadata and warnings
500Unhandled server errorUsually 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

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:

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

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.

On this page