Error Catalog
The real error envelope, every `code` the Brainerce API returns, and how to recover.
The envelope
Every error response from the Brainerce API has this shape:
{
"statusCode": 409,
"code": "IDEMPOTENCY_KEY_REUSED",
"message": "Human-readable explanation",
"details": { "retryAfterSeconds": 12 },
"error": "Conflict",
"timestamp": "2026-08-23T10:15:00.000Z",
"path": "/api/v1/orders"
}| Field | Always present? | What it is |
|---|---|---|
statusCode | yes | The HTTP status, repeated in the body |
code | yes | The stable, machine-readable code. Switch on this. |
message | yes | Human-readable prose. Wording can change at any time — never parse it |
details | no | Structured extras when the error has them (e.g. retryAfterSeconds, limit, items[]) |
error | no | The generic status name ("Conflict"). Only present on some errors |
timestamp | yes | ISO-8601 of when the error was generated |
path | yes | The request path, query string stripped |
Three consequences worth writing down:
erroris conditional. Code that readsbody.errorbreaks on the errors that omit it. Readcode.codeis never insidemessage. They are separate fields; matching on the message text is how integrations break on a copy-edit.- A 4xx never says "Internal server error". That string is reserved for
5xx. If you see it, the server really did fail — it is not a validation error in disguise. A 4xx whose call site supplied no message falls back to a generic phrase for its status ("Bad request", "Conflict", …) instead.
Anything outside these seven keys is dropped. The envelope is rebuilt from a fixed key set before the response is sent, so structured per-error context always travels inside details — never as a sibling key. When this documentation says an error "carries items[]", it means details.items.
HTTP status codes
| Status | Meaning | Common causes |
|---|---|---|
400 | Bad Request | Validation error — missing required field, malformed JSON |
401 | Unauthorized | Missing or invalid API key / customer token |
402 | Payment Required | Plan quota exhausted (orders, AI credits) |
403 | Forbidden | Authenticated but not allowed (wrong store, missing scope, suspended store) |
404 | Not Found | Resource doesn't exist or belongs to a different store |
409 | Conflict | State conflict, duplicate unique field, idempotency-key reuse |
413 | Payload Too Large | Request body exceeds the parser limit |
415 | Unsupported Media Type | Wrong Content-Type for the endpoint |
422 | Unprocessable Entity | Business rule violation |
429 | Too Many Requests | Rate limit exceeded — see Rate Limits |
500 | Internal Server Error | Unexpected error on our side — please report |
501 | Not Implemented | Deprecated stub endpoint that returns no data — use the documented replacement instead |
503 | Service Unavailable | Brief outage or maintenance window |
Error codes
These are the values the API actually puts in code. Adding a code is non-breaking; removing or renaming one is breaking and goes through the 90-day window in Versioning.
Validation (400)
VALIDATION_FAILED— one or more fields failed validation.messagelists the failing fields.MISSING_REQUIRED_FIELDS— a required field or header was omitted. Note the plural.STORE_ID_REQUIRED— the route needs astoreIdand none was resolvable from the credential or path.STORE_ID_MISMATCH— thestoreIdin the path is not the one the credential belongs to.INVALID_STOCK_VALUE— a stock quantity was negative or non-numeric.PLATFORMS_REQUIRED,PLATFORM_REQUIRED,RESOLUTION_REQUIRED,TYPE_REQUIRED— sync / conflict-resolution routes missing their discriminating field.DEVICE_AUTH_ALREADY_DECIDED— the device-flow code was already approved or denied, or an earlier approval attempt on it did not finish. Not retryable: start the connection again from the terminal for a fresh code.
Cart and checkout (400)
These five are the codes that carry structured recovery data in details. Read
details instead of parsing numbers or product names out of message.
CART_LINE_LIMIT_REACHED— the cart already holds the maximum number of distinct product lines (50 today) and the add would have created a 51st.details.maxLinesis the limit, so you never have to parse the number out of the sentence. Storefront traffic only —vc_*andstoreIdcallers; an admin API key is uncapped. It is checked only when a NEW line would be created, so a full cart can still have quantities changed and lines removed: render it as "your cart is full, remove something to add this", not a generic failure. Older integrations matched the message text ("A cart can hold at most 50 different items…") because there was no code. That match still works and the wording has not changed, so keep it as a fallback while you are talking to a backend that may predate the code — but switch oncodefrom now on.INSUFFICIENT_STOCK— not enough inventory. Cart add/update and inventory reservation reject a single line and senddetails.available+details.requested; checkout validates the whole cart and sendsdetails.items[]withproductId,variantId,available,requested. Branch on which is present.PRODUCT_UNAVAILABLE— a line's product is unpurchasable (deleted variant, or inventory tracking disabled). Carriesdetails.items[]withproductIdandvariantIdwhen more than one line is involved.PRICE_DRIFT— a cart line's snapshot price no longer matches the live price, socreateCheckoutrefused.details.items[]carriesitemId,oldUnitPrice,newUnitPrice,deltaanddirectionper affected line. Recover withPOST /cart/{id}/refresh-snapshotsor by removing those lines.MODIFIER_VALIDATION_FAILED— a cart payload'sselectionsfailed modifier-group validation.details.errors[]lists each issue with its owncode,message,modifierGroupIdandmodifierId. The envelopemessageis deliberately generic — the actionable detail is in that array. The per-issue codes are listed in Critical Rules → Modifier validation errors.
Note that OUT_OF_STOCK (below) is a different code from
INSUFFICIENT_STOCK and is raised by different routes — do not treat them as
aliases.
Gift-card refusals carry no code of their own. POST /checkout/{id}/gift-card
answers every rejected code — unknown, expired, spent, disabled, wrong currency —
with the same 400 / generic BAD_REQUEST and the same message, That gift card
code cannot be used on this order. There is no GIFT_CARD_EXPIRED,
GIFT_CARD_NOT_FOUND or INSUFFICIENT_BALANCE code, and the sameness is
deliberate: a gift-card code is bearer value, so a response that distinguished
"no such code" from "that code exists but is refused" would be an oracle for
discovering real codes. POST /gift-cards/balance follows the same rule without
erroring at all — an unknown, disabled or expired code returns
{ "balance": "0.00", "usable": false }. Show one message and let the shopper
re-enter the code.
Authentication and authorization (401 / 403)
UNAUTHORIZED— missing, malformed, revoked or expired credential. This one code covers every credential type; there is no separateINVALID_API_KEY,INVALID_CUSTOMER_TOKENorMISSING_AUTHORIZATION.SESSION_EXPIRED— a dashboard session expired.INSUFFICIENT_SCOPE— the API key is valid but its scope list does not cover this route. The most common 403 for integrators, and the oneSTORE_ACCESS_DENIEDused to be documented as.INSUFFICIENT_PERMISSION— the acting user lacks the store permission this action needs. Singular.STORE_SUSPENDED— the store is suspended; writes are refused.
Plan and quota (402 / 403)
PLAN_LIMIT_REACHED— a resource-count cap was hit (products, channels, …).PLAN_FEATURE_REQUIRED— the store's plan is too low for this feature.PLAN_ORDERS_EXHAUSTED— the plan's order allowance is used up; storefront checkout is blocked.PLAN_AI_CREDITS_EXHAUSTED— monthly or daily AI credits are depleted.
Not found (404)
RESOURCE_NOT_FOUND— generic.PRODUCT_NOT_FOUND,ORDER_NOT_FOUND,CUSTOMER_NOT_FOUND,COUPON_NOT_FOUND,CHECKOUT_NOT_FOUND— resource doesn't exist or isn't visible to this credential.IDEMPOTENCY_KEY_NOT_SUPPORTED— you sentIdempotency-Keyon aGET. See Idempotency.
Conflict and business rules (409 / 422)
CONFLICT— generic state conflict. There is noCONCURRENT_MODIFICATIONcode.IDEMPOTENCY_KEY_REUSED— the sameIdempotency-Keyarrived with a different request body.ALREADY_INSTALLED— the app is already installed on this store.COUPON_EXPIRED,COUPON_NOT_ACTIVE,COUPON_USAGE_LIMIT_REACHED,COUPON_PER_CUSTOMER_LIMIT_REACHED— coupon validation failures. There is noCOUPON_NOT_APPLICABLE.OUT_OF_STOCK— variant has no available inventory.RESERVATION_EXPIRED— cart reservation timed out; refresh and re-add items.ORDER_INVALID_STATE— the order is not in a state that allows this transition.PAYMENT_ALREADY_CAPTURED— capture was already taken for this payment.CARD_DECLINED— the payment processor rejected the charge. There is noPAYMENT_DECLINED.
A duplicate customer email surfaces as a 409 with CONFLICT or VALIDATION_FAILED depending on the route — there is no DUPLICATE_EMAIL code.
Rate limiting (429)
RATE_LIMITED— slow down. Readdetails.retryAfterSecondsand theRetry-Afterheader. There is noRATE_LIMIT_EXCEEDED.- Repeated failed authentication from one IP also returns
429after 50 failures in 5 minutes — fix the credential rather than retrying. See Rate Limits → Failed-authentication throttling.
Server and provider (5xx)
INTERNAL_SERVER_ERROR— unexpected failure on our side.PROVIDER_NOT_READY,PROVIDER_NOT_CONFIGURED— a payment or shipping provider is missing or incompletely configured.PAYMENT_PROCESSING_ERROR,REFUND_PROCESSING_ERROR— the provider call failed.DOMAIN_REGISTRATION_FAILED,DOMAIN_VERIFICATION_FAILED— custom-domain operations.MEDIA_UPLOAD_FAILED,MEDIA_REPLACE_FAILED— media pipeline failures.AI_TOOL_FAILED,AI_PROCESSING_FAILED— AI assistant failures.IDEMPOTENCY_LOCK_UNAVAILABLE(503) — we could not take the once-only lock for yourIdempotency-Key, so the request was refused rather than run unguarded. Nothing was processed — onPOST /v1/checkout/{checkoutId}/complete, the only route that can return it today, that means no order and no charge. Retry with the same key after a short backoff. ⛔ Do not treat it likeIDEMPOTENCY_KEY_REUSED: that code is a409telling you to generate a new key, and doing that here sends a second, unprotected attempt at the same payment.SERVICE_TEMPORARILY_UNAVAILABLE(503) — a transient internal dependency was unreachable and the request was refused rather than run without its safety guarantee. Nothing was processed. Retry the identical request after a short backoff; changing the request will not help. Distinct fromPROVIDER_NOT_READY, which points at a payment provider — this one does not, and a shopper hitting it on a paid download should simply try again.
Not implemented (501)
NOT_IMPLEMENTED— a deprecated stub endpoint. Today that isPOST /v1/syncandGET /v1/sync/{jobId}; use the per-resource replacements (e.g.POST /v1/coupons/{id}/sync).
Status-derived fallbacks
Not every error originates from the catalog above. An exception thrown without an explicit code gets one derived from its HTTP status, so these appear on the wire too and belong in a default: branch rather than being treated as unknown:
BAD_REQUEST (400), FORBIDDEN (403), NOT_FOUND (404), PAYLOAD_TOO_LARGE (413), UNSUPPORTED_MEDIA_TYPE (415), UNPROCESSABLE_ENTITY (422), and ERROR for anything else.
Codes that are not from this API
INVALID_JSON is returned by the marketplace-app framework (@brainerce/integration-shared) when an app's own webhook receiver is sent an unparseable body. You will see it if you are building a marketplace app; you will not see it in a response from api.brainerce.com.
Idempotency
Idempotency-Key is opt-in per route, not blanket coverage of every mutation. Most /v1/* mutations honour it, thirteen do not, and checkout completion requires it. Read Idempotency before relying on a retry being safe — a key sent to a route that does not support it is accepted and ignored, with nothing in the response to tell you.
When in doubt
If you hit an error you don't understand:
- Look up the value of the
codefield in this catalog. - Check the Critical Rules for known integration gotchas.
- Reach out at [email protected] — quote the endpoint you called plus the
timestampandpathfrom the error body. There is no request-id response header to quote (neitherX-Request-IdnorX-Brainerce-Request-Idis set by the API); those two body fields are what let us find the request in our logs.