API ReferenceError Catalog

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"
}
FieldAlways present?What it is
statusCodeyesThe HTTP status, repeated in the body
codeyesThe stable, machine-readable code. Switch on this.
messageyesHuman-readable prose. Wording can change at any time — never parse it
detailsnoStructured extras when the error has them (e.g. retryAfterSeconds, limit, items[])
errornoThe generic status name ("Conflict"). Only present on some errors
timestampyesISO-8601 of when the error was generated
pathyesThe request path, query string stripped

Three consequences worth writing down:

  • error is conditional. Code that reads body.error breaks on the errors that omit it. Read code.
  • code is never inside message. 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

StatusMeaningCommon causes
400Bad RequestValidation error — missing required field, malformed JSON
401UnauthorizedMissing or invalid API key / customer token
402Payment RequiredPlan quota exhausted (orders, AI credits)
403ForbiddenAuthenticated but not allowed (wrong store, missing scope, suspended store, a guest cart's proof of ownership missing)
404Not FoundResource doesn't exist or belongs to a different store
409ConflictState conflict, duplicate unique field, idempotency-key reuse
413Payload Too LargeRequest body exceeds the parser limit
415Unsupported Media TypeWrong Content-Type for the endpoint
422Unprocessable EntityBusiness rule violation
429Too Many RequestsRate limit exceeded — see Rate Limits
500Internal Server ErrorUnexpected error on our side — please report
501Not ImplementedDeprecated stub endpoint that returns no data — use the documented replacement instead
503Service UnavailableBrief 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. message lists the failing fields.
  • MISSING_REQUIRED_FIELDS — a required field or header was omitted. Note the plural.
  • A plain 400 (no dedicated code) with the message <name> is required and must be a non-empty string: salesChannelId missing, empty or not a string on a publish-sales-channel / unpublish-sales-channel route (product, coupon, customer, category, brand, tag), and email missing, empty or not a string on GET /v1/customers/by-email. A publish call never falls back to "some channel of the store".
  • STORE_ID_REQUIRED — the route needs a storeId and none was resolvable from the credential or path.
  • STORE_ID_MISMATCH — the storeId in 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.maxLines is the limit, so you never have to parse the number out of the sentence. Storefront traffic only — vc_* and storeId callers; 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 on code from now on.
  • INSUFFICIENT_STOCK — not enough inventory. Cart add/update and inventory reservation reject a single line and send details.available + details.requested; checkout validates the whole cart and sends details.items[] with productId, 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 (reservationStrategy ON_PAYMENT, the default), createCheckout and createPaymentIntent both 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 same details.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). Carries details.items[] with productId and variantId when more than one line is involved.
  • PRICE_DRIFT — a cart line's snapshot price no longer matches the live price, so createCheckout refused. details.items[] carries itemId, oldUnitPrice, newUnitPrice, delta and direction per affected line. Recover with POST /cart/{id}/refresh-snapshots or by removing those lines.
  • CHECKOUT_OUT_OF_SYNC — HTTP 409 from createPaymentIntent: the cart changed after the checkout was created, so its subtotal / total no longer match its lineItems. 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 from completeCheckout: the cart lines no longer sum to the checkout's quoted subtotal (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 from createPaymentIntent when the request carried expectedTotal: 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 shopper newTotal, then retry with expectedTotal = newTotal once they accept.
  • TAX_ADDRESS_REGION_REQUIRED — HTTP 400 from createPaymentIntent: the store charges tax by state or province in the buyer's country (US, Canada, Australia) and the shipping address has no region. Nothing was charged and no payment was created. The body is flat and carries the buyer's country (ISO alpha-2). Ask for the state or province, update the shipping address with region, then create the intent again.
  • TAX_NO_RATE_FOR_DESTINATION — HTTP 400 from createPaymentIntent (and from completeCheckout for 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's country (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 400 MODIFIER_VALIDATION_FAILED (below) when the paid and downsell modifiers would take the line's unit price below 0, and details.errors holds one entry with this code and 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's selections failed modifier-group validation. details.errors[] lists each issue with its own code, message, groupId and modifierId. The envelope message is 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 longer ACTIVE (finished, merged, or older than the 30-day revival window). Resolve the cart again; a cart an idle sweep marked ABANDONED is handed back to its owner as ACTIVE by 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 own sessionToken as the cartToken query parameter (or the X-Cart-Session header) on the routes listed below; the SDK does it for you from 3.11.0. A browser that was only handed a cartId (a cart your server created through the /v1 API) 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 separate INVALID_API_KEY, INVALID_CUSTOMER_TOKEN or MISSING_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 one STORE_ACCESS_DENIED used 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, and createCheckout() 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 (the cartToken query 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().reviewsEnabled tells you the same thing before you make the call. getMyProductReview is the exception: it answers 200 with eligible: 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 on createCheckout(), and again on createPaymentIntent() / startPayment() for a checkout that was opened earlier, so it is refused before any charge exists. Do not show its message: 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 on err.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_FOUND is 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 sent Idempotency-Key on a GET. See Idempotency.

Conflict and business rules (409 / 422)

  • CONFLICT — generic state conflict. There is no CONCURRENT_MODIFICATION code.
  • IDEMPOTENCY_KEY_REUSED — the same Idempotency-Key arrived 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 on code and translate it yourself; the message is English. COUPON_NOT_FOUND also covers a code that exists but is not published to this storefront's channel. COUPON_NOT_ACTIVE covers a coupon that has not started yet. There is no COUPON_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 sent null for a field that cannot be cleared; omit the field to leave it unchanged. message names 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 a rateLimitOverride, 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 a PERCENTAGE discount 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 longer PENDING, PROCESSING, PAID or ON_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 no PAYMENT_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.

codeStatusWhen it firesWhat to do
PAYMENT_REQUIRED400completeCheckout 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_PAUSED503The store's payments are paused. details.reason carries the pause reason when there is oneTell the shopper the store cannot take payments right now; retry later
CHECKOUT_LOCKED400A change to a checkout (address, shipping method, gift card or other tender) while its payment is PAYMENT_PENDING or PAYMENT_PROCESSINGWait for the payment to finish or fail; do not edit the checkout meanwhile
CHECKOUT_ITEMS_MISSING400completeCheckout for a partial checkout (selectedItemIds) none of whose lines are on the cart any more. No order is createdCreate a new checkout from the current cart
PICKUP_POINT_REQUIRED400createPaymentIntent or completeCheckout with a shipping rate that delivers to a carrier pickup point and no pickup point chosenLet the shopper choose a pickup point, then retry
PICKUP_POINT_NOT_APPLICABLE400A pickup point was sent for a shipping rate that does not deliver to pickup pointsSelect the pickup-point delivery option first
DISCOUNT_LIMIT_EXCEEDED400An automatic discount rule reached its total usage cap, its budget, or this customer's per-customer cap at checkoutRe-read the cart (the rule no longer applies) and show the new total
COUPON_REGION_RESTRICTED400applyCoupon: the coupon is limited to regions and the cart's region is not one of themShow "not available in your region"
COUPON_NO_ELIGIBLE_ITEMS400applyCoupon: no cart line qualifies for the coupon's product or category targetingShow "this coupon does not apply to any items in your cart"
COUPON_FIRST_ORDER_ONLY400The coupon is first-order only and the identified customer (email or account) already has an orderShow "valid on a first order only"
COUPON_NOT_REDEEMABLE_BY_CUSTOMER400A 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 emailShow "this code was issued to a different customer"
PRODUCT_NOT_AVAILABLE400POST /orders with a product that does not exist on, or is not published to, this sales channel. The ids are not returnedRemove the line and refresh the catalogue
KIT_EMPTY400A kit product with no components is ordered (POST /orders) or reserved. Adding such a kit to a cart answers PRODUCT_UNAVAILABLETreat the product as unavailable
KIT_COMPONENT_UNAVAILABLE400A kit contains a component that is no longer for sale. details.components[] lists the componentsTreat the kit as unavailable
INVENTORY_BUSY400A kit sale could not take the per-product lock because another order is updating the same itemRetry after a short backoff
RESERVATION_CONFLICT400A reservation's quantity was changed concurrently by another request while this one was adjusting itRetry 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. Read details.retryAfterSeconds and the Retry-After header. There is no RATE_LIMIT_EXCEEDED.

  • Repeated failed customer sign-ins from one IP address on one store (100 within an hour) return 429 from 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", no details) 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 answers 401 (not 429) with Invalid 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 hour POST /customers/verify-email answers 429 for 30 minutes; and a fourth verification email within an hour is refused (403 for a saved account, 429 for 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 your Idempotency-Key, so the request was refused rather than run unguarded. Nothing was processed — on POST /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 like IDEMPOTENCY_KEY_REUSED: that code is a 409 telling 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 from PROVIDER_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 is POST /v1/sync and GET /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:

  1. Look up the value of the code field in this catalog.
  2. Check the Critical Rules for known integration gotchas.
  3. Reach out at [email protected] — quote the endpoint you called plus the timestamp and path from the error body. There is no request-id response header to quote (neither X-Request-Id nor X-Brainerce-Request-Id is set by the API); those two body fields are what let us find the request in our logs.