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, with one exception. 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. The exception is a flat set of figures on exactly three codes (CHECKOUT_TOTAL_CHANGED, TAX_ADDRESS_REGION_REQUIRED, TAX_NO_RATE_FOR_DESTINATION), described under Cart and checkout above. 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, a guest cart's proof of ownership missing) |
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.- A plain
400(no dedicatedcode) with the message<name> is required and must be a non-empty string:salesChannelIdmissing, empty or not a string on apublish-sales-channel/unpublish-sales-channelroute (product, coupon, customer, category, brand, tag), andemailmissing, empty or not a string onGET /v1/customers/by-email. A publish call never falls back to "some channel of the store". 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)
The codes below that list fields carry structured recovery data in details (or, for three of them, flat fields). Read
those 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. On a sales-channel storefront the number is what that channel may sell: the shared pool minus the units the merchant reserved for other channels. On a channel that holds no stock before payment (reservationStrategyON_PAYMENT, the default),createCheckoutandcreatePaymentIntentboth run the check (the second one right before the charge, because stock can move while the buyer fills in the form) and answer with the samedetails.items[], so handle it on both calls. Channels that reserve at cart or checkout time hold the units already.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.CHECKOUT_OUT_OF_SYNC— HTTP 409 fromcreatePaymentIntent: the cart changed after the checkout was created, so itssubtotal/totalno longer match itslineItems.GET /checkout/{id}re-derives the totals from the lines; re-read it, show the shopper the new total, then create the intent again.CHECKOUT_CART_CHANGED— HTTP 400 fromcompleteCheckout: the cart lines no longer sum to the checkout's quotedsubtotal(a line was added, removed or its quantity changed after the checkout was created), so no order was created. A payment that was already captured is not lost: the payment worker finds the same difference, fails the checkout and flags the payment for refund. Re-read the checkout (GET /checkout/{id}re-derives the totals), show the shopper the new total, then complete it again.CHECKOUT_TOTAL_CHANGED— HTTP 409 fromcreatePaymentIntentwhen the request carriedexpectedTotal: tax was re-verified right before the charge and the total is no longer that number. Nothing was charged and no payment was created. The body is flat (previousTotal,newTotal,previousTax,newTax,currency, in major units of the store currency). Show the shoppernewTotal, then retry withexpectedTotal = newTotalonce they accept.TAX_ADDRESS_REGION_REQUIRED— HTTP 400 fromcreatePaymentIntent: the store charges tax by state or province in the buyer's country (US, Canada, Australia) and the shipping address has noregion. Nothing was charged and no payment was created. The body is flat and carries the buyer'scountry(ISO alpha-2). Ask for the state or province, update the shipping address withregion, then create the intent again.TAX_NO_RATE_FOR_DESTINATION— HTTP 400 fromcreatePaymentIntent(and fromcompleteCheckoutfor an order that needs no payment): the store owner chose not to take orders to a country with no tax rate, and the buyer's country has none in force. Nothing was charged and no payment was created. The body is flat and carries the buyer'scountry(ISO alpha-2). Retrying the same address cannot succeed: tell the buyer this store cannot deliver there and ask for another delivery address. A store that did not choose this (the default) never answers it, and neither does a country the merchant marked "not collected", a store with tax off, or a checkout with no address.MODIFIER_PRICE_FLOOR_VIOLATED— an issue code, not a response code. Add to cart and update cart line answer HTTP 400MODIFIER_VALIDATION_FAILED(below) when the paid and downsell modifiers would take the line's unit price below0, anddetails.errorsholds one entry with thiscodeand a generic message on purpose. Let the shopper remove a downsell and retry.CART_LOCKED_FOR_PAYMENT— HTTP 409 on a cart write (add, update, remove, clear, coupon, refresh-snapshots, merge) while a payment for that cart is in flight: an intent is being created, or the buyer has paid and the order is not created yet. A plain pending checkout with no money moving does not lock the cart. Wait for the payment to finish, or start over if the shopper abandoned it. See Checkout errors.MODIFIER_VALIDATION_FAILED— a cart payload'sselectionsfailed modifier-group validation.details.errors[]lists each issue with its owncode,message,groupIdandmodifierId. 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.
Cart not found and cart ownership (404 / 403)
Cart routes used to answer every missing thing with the same 404 NOT_FOUND
("Cart not found", "Cart item not found", ...), so a storefront could not tell a
vanished cart from a vanished line or an unknown product, and threw a perfectly good
cart away to recover from the wrong one. They now carry a code. The status and the
message wording are unchanged, so code that matched them keeps working; switch on
code from now on.
CART_NOT_FOUND(404) — the cart id does not exist for this store, or the cart is no longerACTIVE(finished, merged, or older than the 30-day revival window). Resolve the cart again; a cart an idle sweep markedABANDONEDis handed back to its owner asACTIVEby a read. Start a new cart only if that also 404s.CART_ITEM_NOT_FOUND(404) — the cart exists but has no such line (already removed, possibly from another tab). Show the current cart.PRODUCT_NOT_FOUND(404) — the product exists in no catalogue this caller can see. Cart add uses this code too, so it is not only a product-page error. The cart is fine: show "unavailable" and keep it.VARIANT_NOT_FOUND(404) — the product has no such variant. The cart is fine.CART_ACCESS_DENIED(403) — the caller did not prove it holds this cart. What it means: a write to a guest cart must carry that cart's session token, and a write to a customer's cart must carry that customer's Bearer token; this request carried neither, a wrong token, or another customer's. One message covers every cause on purpose, so the response cannot be used to learn whether a cart belongs to a customer. When it can happen: only on a store that enforces the proof. New stores enforce it from the start; Brainerce moves existing stores over one by one (until then a missing or wrong proof is counted but allowed), so send the proof now rather than waiting to be switched. How to fix it: send the cart's ownsessionTokenas thecartTokenquery parameter (or theX-Cart-Sessionheader) on the routes listed below; the SDK does it for you from 3.11.0. A browser that was only handed acartId(a cart your server created through the/v1API) holds no token and cannot write to that cart. Never retry it blindly: the same request fails the same way. It is a 403 and never a 401 on purpose, because a storefront proxy clears its auth cookies on any 401: do not clear the customer session or log the shopper out because of it.
The proof. Routes that take it (sales-channel /api/vc/{salesChannelId}, and the
same cart, bundle, bump and POST /checkout routes on the storeId surface):
POST /cart/{cartId}/items, PATCH and DELETE /cart/{cartId}/items/{itemId},
DELETE /cart/{cartId} (on storeId, DELETE /cart/{cartId}/items), POST and
DELETE /cart/{cartId}/coupon, POST /cart/{cartId}/recalculate,
POST /cart/{cartId}/refresh-snapshots, the bundle and order-bump routes under
/cart/{cartId}/, POST /reservation/extend, POST /reservation/release,
POST /checkout and the checkout coupon routes. Reads never need it:
GET /cart/{cartId} and GET /cart/session/{token} answer without proof. The token goes
in the query string (?cartToken=<sessionToken>) because it survives a
same-origin proxy that forwards the query and drops headers; for that reason, redact
cartToken from CDN, proxy and APM logs, and forward the query string through any
proxy of your own. See Core, Cart ownership proof.
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, andcreateCheckout()is refused too, so a suspended store opens no checkout.CART_ACCESS_DENIED— a write to a guest cart without that cart's proof of ownership (thecartTokenquery parameter), on a store that enforces it. A 403 and never a 401, so it must not log the shopper out. See Cart not found and cart ownership.REVIEWS_DISABLED— the merchant has switched product reviews off, so every storefront review route (list, submit, edit, delete, photo upload) answers 403 with this code. Render no reviews UI when you see it; any other failure on a reviews call is a real error.getStoreInfo().reviewsEnabledtells you the same thing before you make the call.getMyProductReviewis the exception: it answers 200 witheligible: false, reason: 'reviews_disabled'rather than a 403.
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 (a Free store at 10 orders in the cycle). It is a 402 oncreateCheckout(), and again oncreatePaymentIntent()/startPayment()for a checkout that was opened earlier, so it is refused before any charge exists. Do not show itsmessage: it is written for the merchant ("must upgrade"). Show a plain "this store is not accepting orders right now" instead. Paid plans never return it (Pro and Growth bill overage; Enterprise is unlimited). A 402 here is not a declined card: branch onerr.code, never on the status alone.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.PRODUCT_NOT_FOUNDis also what a cart add answers for an unknown product.CART_NOT_FOUND,CART_ITEM_NOT_FOUND,VARIANT_NOT_FOUND— the cart, a line in it, or a product's variant is missing. Each means a different recovery; see Cart not found and cart ownership.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_NOT_FOUND,COUPON_EXPIRED,COUPON_NOT_ACTIVE,COUPON_USAGE_LIMIT_REACHED,COUPON_PER_CUSTOMER_LIMIT_REACHED,COUPON_MINIMUM_NOT_MET— coupon validation failures. Applying a code to a cart (applyCoupon) returns one of these on every refusal, so branch oncodeand translate it yourself; themessageis English.COUPON_NOT_FOUNDalso covers a code that exists but is not published to this storefront's channel.COUPON_NOT_ACTIVEcovers a coupon that has not started yet. 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.COUPON_CODE_EXISTS(409) — creating or renaming a coupon to a code another coupon of the store already holds. Codes are case-insensitive and unique per store.COUPON_FIELD_NOT_NULLABLE(400) — a coupon update sentnullfor a field that cannot be cleared; omit the field to leave it unchanged.messagenames the fields.COUPON_BULK_LIMIT(400) — a bulk coupon update or delete named more than 500 ids. Split the call.API_KEY_TIER_EXCEEDS_PLAN,API_KEY_RATE_LIMIT_EXCEEDS_PLAN(400) — an API key asked for a rate-limit tier, or arateLimitOverride, above what the store's billing plan allows.API_KEY_LIMIT(400) — the store already has 100 active keys; revoke one first.CUSTOMER_ADDRESS_LIMIT,CUSTOMER_TAGS_LIMIT,CUSTOMER_TAG_TOO_LONG(400) — a customer already has 100 saved addresses, or the tag list is longer than 100 entries or a tag is longer than 100 characters.PROMO_PERCENT_OVER_100,PROMO_DISCOUNT_INVALID(400) — a bundle offer or order bump with aPERCENTAGEdiscount above 100, or a discount value outside 0 to 9999999999.99.DISCOUNT_RULE_BULK_LIMIT(400) — a bulk discount rule delete named more than 500 ids.WEBHOOK_SUBSCRIPTION_LIMIT(400) — the store already has 200 webhook endpoints.INVALID_ORDER_STATE_FOR_FULFILL(400) — fulfilling an order that is no longerPENDING,PROCESSING,PAIDorON_HOLD(another request moved it first). Sending no tracking field at all on an order that is already shipped also lands here.INVALID_ORDER_STATE_FOR_TRACKING_EDIT(400) — correcting the tracking details of an order that is no longer in a state whose tracking can be edited.GIFT_CARD_REFUND_EXCEEDS_TENDER(400) — a refund would return more to a gift card than the card paid on that order; nothing was refunded. Refund the remaining amount only.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.
Checkout, payment, coupon and stock refusals (400 / 503)
These codes come from the storefront checkout path. None of them carries extra structured data on the wire (the envelope keeps only the seven keys above), so the message is the only context besides details where stated.
code | Status | When it fires | What to do |
|---|---|---|---|
PAYMENT_REQUIRED | 400 | completeCheckout on a checkout whose payment is not captured and whose amount due is above zero (the /v1 surface has no payment routes, so every /v1 completion of a priced checkout answers this) | Take the payment on the storefront (/vc) first, then complete |
PAYMENTS_PAUSED | 503 | The store's payments are paused. details.reason carries the pause reason when there is one | Tell the shopper the store cannot take payments right now; retry later |
CHECKOUT_LOCKED | 400 | A change to a checkout (address, shipping method, gift card or other tender) while its payment is PAYMENT_PENDING or PAYMENT_PROCESSING | Wait for the payment to finish or fail; do not edit the checkout meanwhile |
CHECKOUT_ITEMS_MISSING | 400 | completeCheckout for a partial checkout (selectedItemIds) none of whose lines are on the cart any more. No order is created | Create a new checkout from the current cart |
PICKUP_POINT_REQUIRED | 400 | createPaymentIntent or completeCheckout with a shipping rate that delivers to a carrier pickup point and no pickup point chosen | Let the shopper choose a pickup point, then retry |
PICKUP_POINT_NOT_APPLICABLE | 400 | A pickup point was sent for a shipping rate that does not deliver to pickup points | Select the pickup-point delivery option first |
DISCOUNT_LIMIT_EXCEEDED | 400 | An automatic discount rule reached its total usage cap, its budget, or this customer's per-customer cap at checkout | Re-read the cart (the rule no longer applies) and show the new total |
COUPON_REGION_RESTRICTED | 400 | applyCoupon: the coupon is limited to regions and the cart's region is not one of them | Show "not available in your region" |
COUPON_NO_ELIGIBLE_ITEMS | 400 | applyCoupon: no cart line qualifies for the coupon's product or category targeting | Show "this coupon does not apply to any items in your cart" |
COUPON_FIRST_ORDER_ONLY | 400 | The coupon is first-order only and the identified customer (email or account) already has an order | Show "valid on a first order only" |
COUPON_NOT_REDEEMABLE_BY_CUSTOMER | 400 | A loyalty reward, referral welcome or birthday gift coupon is redeemed by a customer other than the one it was issued to, or a newsletter-benefit coupon bound to one email address is used with a different email | Show "this code was issued to a different customer" |
PRODUCT_NOT_AVAILABLE | 400 | POST /orders with a product that does not exist on, or is not published to, this sales channel. The ids are not returned | Remove the line and refresh the catalogue |
KIT_EMPTY | 400 | A kit product with no components is ordered (POST /orders) or reserved. Adding such a kit to a cart answers PRODUCT_UNAVAILABLE | Treat the product as unavailable |
KIT_COMPONENT_UNAVAILABLE | 400 | A kit contains a component that is no longer for sale. details.components[] lists the components | Treat the kit as unavailable |
INVENTORY_BUSY | 400 | A kit sale could not take the per-product lock because another order is updating the same item | Retry after a short backoff |
RESERVATION_CONFLICT | 400 | A reservation's quantity was changed concurrently by another request while this one was adjusting it | Retry the request |
The /v1 admin surface adds two inventory refusals: HAS_RESERVED_STOCK (400, changing a variant's tracking mode away from TRACKED while units are reserved for pending orders) and QUANTITY_REQUIRED (400, switching a variant to TRACKED without sending a stock quantity).
The storefront bot stream (POST /api/storefront-bot/{id}/chat) reports failures as events inside the stream rather than as an HTTP error: PROVIDER_UNAVAILABLE (the model call failed, retryable: true) and TURN_IN_PROGRESS (the same turn id is already being processed, retryable: true).
Rate limiting (429)
-
RATE_LIMITED— slow down. Readdetails.retryAfterSecondsand theRetry-Afterheader. There is noRATE_LIMIT_EXCEEDED. -
Repeated failed customer sign-ins from one IP address on one store (100 within an hour) return
429from the login route for that address only. See Rate Limits → Customer sign-in failures. -
Repeated failed authentication from one IP also returns
429(code: "RATE_LIMITED", nodetails) once it has made more than 50 failed attempts in 5 minutes — fix the credential rather than retrying. See Rate Limits → Failed-authentication throttling. -
Three per-account lockouts are separate from the IP limits and carry no
details: after 10 failed sign-ins for one email on one store within 10 minutes the login route locks that email for 10 minutes and answers401(not429) withInvalid email or password, exactly as for a wrong password, so a lock cannot be told apart from a typo (a password reset ends it early); after 5 wrong email-verification codes within an hourPOST /customers/verify-emailanswers429for 30 minutes; and a fourth verification email within an hour is refused (403for a saved account,429for a pending registration). Details: Customer accounts.
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, sixteen do not (plus the /v1/installations/* routes), 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.