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 -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"
}'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.
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
400Bad 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/.
…/openapi.json), and any OpenAPI validator, such asajvwithajv-formatsoropenapi-schema-validator, names the path that fails. Some400s carry a tag:UnreadableUploadfor a CSV that cannot be read,AssetUnreadablefor a file that cannot be read, andAsOfInvalidfor a badx-manifold-as-ofheader. 401UnauthorizedUnauthorized: the token is missing, has expired or is not valid;reasonsays which. Get a new token and send again.TokenAlreadyUsed: a single-use app token was sent a second time. See Authentication and app tokens.402Payment requiredSubscriptionRequired: 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).statussays where the subscription stands.403ForbiddenScopeRequired: your token holds none of the scopes inrequired; any one would do.TenantRequired: the token names no organization. Some operations have a 403 of their own, such asGrantExceedsYourswhen you try to give someone more access than you hold. See Scopes and roles.404Not found- The record named in the path does not exist in this organization, such as
ItemNotFoundwith itssku. 409Conflict- 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.sourcenames 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 thex-manifold-as-ofheader, which views the past. Drop the header to act now.
429Too 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-Afterheader gives, then send the call again. The other services have no rate limit today. See Rate limits. 5xx- Manifold could not finish the request.
502means a service the request needed did not answer, such asInventoryUnavailable;503means something it depends on is down for now;500is a fault in Manifold. The body may be empty, or carry a tag. The write may or may not have been applied. Two503s 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
InsufficientStockclears when stock arrives. A conflict on a retry can mean the work is already done: a retried create that meetsSkuConflictfound the SKU it made, and a retried pick that meetsShipmentNotInStatefound 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:
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