Every refusal message, and what actually caused it
A message-to-cause table for every string the storefront refuses with at checkout, on downloads and on the admin order doors, including the ones that look identical but are not.
The storefront refuses with specific, fixed strings. A support agent who can map the string to its cause resolves the ticket in one reply; one who cannot will guess, and several of these messages have a cause that is not the obvious one.
This platform answers nearly everything with HTTP 200 and puts the real code in
the response body. Do not classify a failure by its status code — read the
message. The one place a code genuinely discriminates is the download file
streamer, where 403 and 404 mean different things; even there the code is in the
body.
At checkout
There are two order doors, and they word the same refusals differently. The
storefront's Place Order button calls POST /api/ecommerce/cart/checkout, whose
messages name the offending product or currency. The single-item door
POST /api/ecommerce/order runs the same gates in the same order but names
nothing — its messages are the bare phrase. No storefront screen posts to it, so
a ticket quoting the bare phrase came from an API integration rather than the
shop. Where the two differ below, the cart wording is given first and the
single-item wording second.
The whole checkout is one transaction, so any one of them rolls back every line — a three-product cart that fails on line two leaves the customer with nothing bought and nothing charged.
| Message | What actually happened | What you do |
|---|---|---|
Insufficient balance in <CURRENCY> wallet / Insufficient balance |
See below — this one is ambiguous | Check whether the customer holds that wallet at all |
Insufficient inventory for <product> / Insufficient inventory |
The pre-check. The product is a PHYSICAL item and inventoryQuantity is below the requested quantity |
Restock, or unpublish the product |
Product inventory changed during checkout |
The conditional stock decrement matched no row — someone else took the last unit between the pre-check and the write. Identical on both doors | Nothing to fix. See below |
Product is not available: <product> / Product is not available |
The product's status is off |
Republish it, or remove it from wherever the customer found the link |
Product not found: <id> / Product not found |
The product row is gone or soft-deleted, and the customer's cart still holds its id | Tell them to clear the cart; it lives in their browser |
No items to checkout / Invalid item in cart |
Cart door only. Empty cart, or a quantity that is zero, negative or not a whole number | A client-side problem, not a store one |
Invalid quantity |
Single-item door only. A quantity that is zero, negative or not a whole number | The same problem, from the other door |
Discount not found or not applicable to this product |
The code is misspelled, inactive, expired, or belongs to a different product. Identical on both doors | See Discount codes |
Discount usage limit reached |
The code has hit its maxUses across all customers. Identical on both doors |
Raise the cap or issue a new code |
You have already used this discount |
One use per customer, enforced. Identical on both doors | Working as designed |
Discount already applied in this checkout |
Cart door only — the same code was attached to two lines of one cart. The single-item door takes one product, so it has no such check | Working as designed |
Too many order attempts. Please wait before placing another order. |
A throttle, not a failure — 5 checkouts per 15 minutes, and both doors share it | See Rate limits |
Insufficient balance in <CURRENCY> wallet is two different problems
This is the most-misdiagnosed message in the addon, because it fires in two cases that look identical to the customer: the wallet is short, or the customer has no wallet of that type and currency at all.
Every product carries a walletType (FIAT, SPOT or ECO) and a currency.
Checkout groups the cart's lines by that pair, looks for a wallet matching it
exactly, and refuses if the wallet is missing or its balance is below the
group's aggregated total. Both paths produce the same sentence. The single-item
door does the same for its one product and refuses with the bare
Insufficient balance, which names neither the currency nor the reason.
So before you tell a customer to top up:
-
Open the product in
/admin/ecommerce/productand note its Wallet Type and Currency. -
Open the customer's wallets in the admin panel and check whether a wallet of exactly that type and currency exists. A customer with 500 USDT in a
SPOTwallet cannot buy a product priced in USD onFIAT. -
If the wallet is missing, the customer has to fund one through the platform's normal deposit flow. There is no card checkout and nothing in the store creates a wallet for them.
A catalogue that mixes wallet types is the usual root cause. If most of your support load is this message, price the catalogue in one currency on one wallet type and the problem disappears.
Product inventory changed during checkout is a race, not a fault
The stock decrement is conditional — it only writes when the row still holds at least the ordered quantity. When two customers check out the last unit at the same moment, the second one's decrement matches nothing and the checkout is refused.
Two things to know:
- The whole checkout rolls back, not just that line. A customer buying three things loses all three because one was oversold. They see one error and an unchanged cart.
- It is the correct outcome. The alternative is negative stock. Nothing is broken; the product was genuinely sold out a fraction of a second earlier.
If it happens often, you are running too close to zero. Keep a buffer, or publish more stock than a single unit at a time.
The KYC refusal, which fires before any product is loaded
Checkout's second gate — immediately after the throttle and before a single
product is read — is the KYC feature order_ecommerce, shown as Marketplace
Purchases in the level builder at Users → Compliance & Verification →
Verification Levels (/admin/crm/kyc/level).
| Message | Meaning |
|---|---|
KYC verification is required to place an order. |
The customer has no approved application |
Your verification level does not include this feature (place an order). Complete a higher verification level to continue. |
They are verified, but their level does not carry Marketplace Purchases |
Because it runs first, a customer blocked here will never see an inventory or balance message, and the order they were trying to place is irrelevant to the diagnosis.
Two switches decide whether this gate runs at all, and both live under Admin → System → Settings → Features → Verification:
| Key | Shown as | Default |
|---|---|---|
kycStatus |
KYC Verification | "true" — on |
kycFeatureEnforcement |
Enforce KYC Feature Access | "false" — off |
With either off, the gate is silent regardless of what the levels say. On a
default install that means kycFeatureEnforcement is the one deciding: KYC
itself ships switched on, per-feature enforcement does not, so nobody is refused
at checkout until an operator turns the second switch on. Turning it on activates
every per-level feature switch across the whole platform at once, so review the
levels first — a customer whose level does not list Marketplace Purchases
loses checkout the moment you save.
If a customer reports being blocked and you cannot reproduce it, check both.
Platform settings are stored as text, and the values read as on are "true",
"1" and "on" — anything else, including an empty value, is off.
On downloads
Both download routes — the metadata call GET /api/ecommerce/download/{orderItemId} and the file streamer
GET /api/ecommerce/download/{orderItemId}/file — apply the same checks in the
same order.
| Message | Code | Cause |
|---|---|---|
Order item not found or access denied |
404 | The order item does not exist, or it belongs to someone else. The ownership check and the existence check answer identically, on purpose |
Order must be completed before downloading |
403 | The order is not COMPLETED. A DOWNLOADABLE order is created COMPLETED at checkout, so this normally means somebody moved it, or the item is on a physical order |
This product is not downloadable |
400 | The product's type is PHYSICAL |
Nothing has been attached to this purchase yet |
404 | See below — the operator step that has not been done |
Access denied |
403 | The stored file path resolves outside the uploads root |
Download file not found on server |
404 | The path is inside the uploads root and the file is not there |
Download limit exceeded. Please try again later. |
429 | A throttle — 10 per hour, and both routes share it. See Rate limits |
Nothing has been attached to this purchase yet
This is the single most common support ticket on a new store, and it is not a bug. Digital delivery is a manual, per-order-item step:
- Nothing is ever attached to the product row. There is no field on a product for a file or a licence key.
- After a digital order completes and pays you, an operator opens
/admin/ecommerce/order/<id>and fills in the download options — a licence key, a download link, or both. - Until that happens the buyer has a paid,
COMPLETEDorder with nothing behind it, and this is the message they get.
The full procedure is on Digital delivery. If you sell digital goods, treat "attach the file" as part of fulfilment, not as setup.
The two routes disagree slightly on what counts as "nothing". The metadata endpoint only says this when the item has neither a file path nor a licence key — a key-only delivery is valid and comes back as a key. The file streamer says it whenever there is no file path, so a key-only item hitting the streamer gets the same sentence even though the purchase was delivered correctly.
403 versus 404 on the file streamer
Both come from resolving the stored path against the uploads root, and the distinction tells you which mistake was made:
- 403
Access denied— the path escaped the root. That is a path typed by hand, or one pointing somewhere outsidefrontend/public/uploads. Re-enter it through the admin form. - 404
Download file not found on server— the path is legitimate and the file is not on disk. The usual cause is an upload that never landed, or a deployment that replaced the uploads directory. Re-upload the artefact.
They used to be reported as a single 500, which is why the distinction is worth naming: they need opposite fixes.
One more, on the streamer only:
This purchase is delivered from an external link — use the download url from /api/ecommerce/download/{id}
The file streamer serves files hosted on your own box. When you enter an
http:// or https:// download link instead, the metadata endpoint hands the
buyer that URL directly and the streamer refuses. Reaching it with an external
link means the caller built the path by hand — the storefront does not do this.
On the admin order doors
| Message | Where | Meaning |
|---|---|---|
Order <id> has been paid for and not refunded. Cancel or reject it first — that refunds the buyer and restores the stock — then delete it. |
Delete, single | The guard. Delete removes the row and nothing else; cancelling is what returns the money |
<n> of these orders have been paid for and not refunded (<ids>, …). Cancel or reject them first, then delete them. |
Delete, bulk | The same guard, naming the first three. Nothing in the batch is deleted |
Cannot transition from COMPLETED to CANCELLED. Pass allowRefundFromCompleted=true to deliberately refund a COMPLETED order (return-after-delivery flow). |
Status change | See below |
Cannot transition from <A> to <B>. |
Status change | CANCELLED and REJECTED are terminal, and so is COMPLETED without the flag |
Order <id> status is not PENDING |
Bulk status change | Every order in a bulk status change must currently be PENDING. Refused before anything is written |
No payment was found for order <id>, so it cannot be refunded. |
Cancel / reject | The status change did not happen. See below |
The allowRefundFromCompleted 400
COMPLETED is terminal by default. Refunding from it is possible, but it needs
an explicit allowRefundFromCompleted=true on
PUT /api/admin/ecommerce/order/{id}/status — and the admin screens do not
send it. If you meet this 400 in the panel, there is no button that will get
past it; the transition is an API-level choice.
It was made explicit so that a refund can never be the accidental side effect of
a careless status flip. Mark orders COMPLETED when they have actually shipped,
not when you start picking them.
No payment was found for order <id> means nothing changed
The payment is looked up before the status is written, specifically so that an order can never be left cancelled with the buyer's money still gone. When you see this, the order is still in the state you found it and no money has moved.
It means the order genuinely has no purchase transaction — typically a row created outside checkout, or one whose transaction was deleted. Confirm before doing anything else; the queries are on Where store money lands in the platform ledger.
Quick triage
| Symptom | Look here first |
|---|---|
| Every customer is refused at checkout, wording mentions order attempts | Redis. Checkout's throttle is fail-closed — Rate limits |
| One customer refused with a balance message | Whether they hold that exact wallet type and currency, not their balance |
| A paid digital order delivers nothing | The order item's download options, not the product |
| Downloads 404 for everyone after a deployment | The uploads directory was replaced |
| A cancel "did nothing" | Read the message — a missing payment stops the write entirely |
The general Troubleshooting page covers the non-message-specific symptoms — invisible products, missing menus, stuck orders.