Errors

A refused request answers with a status code and a body that names the reason.

The rule

A refused request answers 4xx with a JSON body. The body's _tag names the reason, and its other fields give the details. A refused request records nothing: no fact, no change.

This call takes twelve units off by hand when ten are on hand:

curl
curl -X POST 'https://inventory.getmanifold.io/items/WIDGET-BLUE/adjust' \
  -H "Authorization: Bearer $MANIFOLD_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
  "delta": -12,
  "reason": "damaged",
  "reference": "damage-0042"
}'
Response · 409
HTTP/1.1 409 Conflict
content-type: application/json
x-request-id: req_0192f3a4-9d10-7e21-8a3b-4c5d6e7f8091

{
  "_tag": "InsufficientStock",
  "sku": "WIDGET-BLUE",
  "available": 10,
  "requested": 12,
  "locationId": "loc_0192e8b1-6a7b-7c8d-9e0f-1a2b3c4d5e6f"
}

To record a sale, create an order; see Order to cash.

Read _tag and the fields beside it: they are part of each operation's contract. Do not match on wording.

JavaScript
const res = await fetch(url, init);
if (!res.ok) {
  const body = await res.json().catch(() => undefined); // a 400 or a 5xx may have no body
  switch (body?._tag) {
    case "InsufficientStock":
      // tell the person: only body.available are on hand at body.locationId
      break;
    case "ScopeRequired":
      // ask an owner or admin for a role that holds one of body.required
      break;
    default:
      // the operation's OpenAPI document lists every refusal it can give
  }
}

Why

A tag tells your code exactly what went wrong, and the fields carry what it needs to tell a person or put it right: how many are on hand, which scope is missing, what the balance is. Because a refusal records nothing, you never need to undo one.

Status codes

400 Bad request
The request does not match the operation. A request the schemas refuse answers with an empty body. Check, in order:
  • a required field is missing;
  • a field has the wrong type, such as a number sent as a string;
  • a value is not one of the field's allowed values;
  • an id has the wrong prefix for its field (see Identifiers);
  • a SKU holds a character other than letters, digits, space, -, _, . or /.
Start from the Example on the operation's page, which is checked against the operation, and change one field at a time. To find the field, validate your body against the operation's schema: every operation page links its own OpenAPI document (…/openapi.json), and any OpenAPI validator, such as ajv with ajv-formats or openapi-schema-validator, names the path that fails. Some 400s carry a tag: UnreadableUpload for a CSV that cannot be read, AssetUnreadable for a file that cannot be read, and AsOfInvalid for a bad x-manifold-as-of header.
401 Unauthorized
Unauthorized: the token is missing, has expired or is not valid; reason says which. Get a new token and send again. TokenAlreadyUsed: a single-use app token was sent a second time. See Authentication and app tokens.
402 Payment required
SubscriptionRequired: the organization's subscription has lapsed, and writes are refused until it is paid. Reads still work, and you can still export your data (POST /exports). status says where the subscription stands.
403 Forbidden
ScopeRequired: your token holds none of the scopes in required; any one would do. TenantRequired: the token names no organization. Some operations have a 403 of their own, such as GrantExceedsYours when you try to give someone more access than you hold. See Scopes and roles.
404 Not found
The record named in the path does not exist in this organization, such as ItemNotFound with its sku.
409 Conflict
The request is well formed, but the record's state or a business rule refuses it: InsufficientStock, SkuConflict (the SKU already exists), OverPayment (more than the order owes), LineNotRepriceable (the order's prices are locked). Two apply to many operations:
  • MirroredFromSource: this area is still copied from the system you are moving from, and only that system changes it until you go live. source names it. It refuses people and agents; an app's own token is not refused. See Read-only mode and the mirror lock.
  • AsOfIsReadOnly: a write was sent with the x-manifold-as-of header, which views the past. Drop the header to act now.
429 Too many requests
Only the auth service limits how fast you call it: 100 calls in any ten seconds from one address, and three sign-ins in ten seconds. Wait the seconds its X-Retry-After header gives, then send the call again. The other services have no rate limit today. See Rate limits.
5xx
Manifold could not finish the request. 502 means a service the request needed did not answer, such as InventoryUnavailable; 503 means something it depends on is down for now; 500 is a fault in Manifold. The body may be empty, or carry a tag. The write may or may not have been applied. Two 503s say more:
  • OrderCreationPending: the order is still being made. Send the same request again; it answers the order once it is saved.
  • RequestLogUnavailable: nothing was spent or recorded. Send again with a fresh app token.

Each operation's page lists every refusal it can give under Errors: the status, the tag, its fields, what it means and what to do.

What to do

No answer, or a 5xx
Send the same request again, with the same key and body, after a short wait. It is applied once. With a single-use app token, mint a fresh token for each try. See Idempotency and retries.
401
Get a new token, then send the same request again: same key, same body.
Any other 4xx: 400, 402, 403, 404
Do not retry unchanged; the answer will be the same. Fix the request, the access or the subscription first. The exception is a refusal an operation marks Can answer a retry under its Errors: your earlier call may have landed. See When a retry is answered with a refusal.
409
Read the tag. A conflict about state can pass later, once the state changes: an InsufficientStock clears when stock arrives. A conflict on a retry can mean the work is already done: a retried create that meets SkuConflict found the SKU it made, and a retried pick that meets ShipmentNotInState found the parcel it picked. Read the record to tell.

Request ids

The answer to every write, refused or not, carries an x-request-id header (req_…). Log it, and quote it when you ask for help: it names the request and what it recorded. Reads carry no request id; quote the time, the path and the status.

Sign-in and membership

The sign-in, session and membership routes under /api/auth answer refusals with code and message instead of _tag. For example, when the last owner with a verified email tries to leave:

Response · 409
HTTP/1.1 409 Conflict
content-type: application/json

{
  "code": "LAST_VERIFIED_OWNER",
  "message": "This is the organization's last active owner with a verified email: elect another verified owner first: …"
}

Manifold's own operations under /api/auth, such as apps, roles, going live and the organization's mode, answer with _tag bodies like every other service, for example GrantExceedsYours or GoLiveIsFinal.

Related: Idempotency and retries · Identifiers · What happens when you get it wrong, or send it twice