Troubleshooting
The problems people run into most, and what to do. Your browser's developer tools (the Console and Network tabs) show nearly all of them.
TOKEN_INVALID (401)
The Storefront API refused the token: it's missing, mistyped, or revoked.
- Copy the token again from Tokens on your shop's Headless page. It's
odpk_(public) orodpv_(preview) followed by 32 letters and digits, with no spaces. - Check it hasn't been revoked. A revoked token stops working within 60 seconds.
- Check you're calling the right API: a token from one Organization Depot environment (a local or
test backend) isn't known to another.
apiUrl/api-urlshould behttps://api.organizationdepot.com/apifor your live shop. - With your own
fetch(thefetchoption, or a proxy), make sure it passes theX-OD-Storefront-Tokenheader through.
The web components explain a refused token in the console: "The Storefront API refused the token on
<od-store> (TOKEN_INVALID)…". The elements show "This shop isn't available right now."
CORS errors
The Storefront API accepts requests from any origin, so a CORS error in the console almost always means the request didn't reach it, or carried something it doesn't accept:
- The wrong
apiUrl. It'shttps://api.organizationdepot.com/api, with/apiat the end. Another address answers without the Storefront API's CORS headers, and the browser reports that as a CORS error. - Credentials. A custom
fetchwithcredentials: "include"fails: the API never allows credentials. The script sends none. - Extra headers. Only
Content-TypeandX-OD-Storefront-Tokenare allowed; anything else your code (or a proxy) adds fails the preflight. - Something in between. An ad blocker, privacy extension, corporate proxy or captive portal can block or replace the response. Try a private window without extensions.
- The scripts themselves. Their URLs answer with
Access-Control-Allow-Origin: *. A CORS error loadingcomponents.jsusually means a mistyped URL: a missing file is a real404, without the CORS header.
In the Network tab, look at the failed request's status and response headers to see which it is.
429 RATE_LIMITED
Too many requests from one buyer (one token and client address): about 120 reads or 60 writes a
minute. The error's retryAfter (the Retry-After header) says how many seconds to wait.
- Look for a loop: code that re-reads the cart or products on a timer, on every scroll, or in a
render loop. Listen to
cart:changeinstead of polling the cart. - A page that loads products one by one (
products.get()per card) should use oneproducts.list()instead. - A page that reads every product to build its tag links should call
tags.list()once instead. - On a server, all your visitors share the server's address: cache the catalog.
- The web components show "Too many requests right now. Try again in 20 seconds." and recover by themselves.
The client doesn't retry a 429 for you.
Nothing renders
Web components
- Load
components.jswith<script type="module" src="…">. Withouttype="module"nothing happens. - Open the page through a web server. Browsers don't run module scripts from
file://. - Add
<od-store token="odpk_…">(or yourodpv_…preview token while you develop) to every page that uses the elements. Without one, each element waits; after 3 seconds the console says "found no<od-store token="…">on the page". - A Content-Security-Policy may be blocking the script, the API or the photos: the console names the directive. Add the three sources.
- An unknown handle shows "This product isn't available." Check
handleagainst the product's slug. - Browsers older than 2023 (Safari before 16.4, say) don't support what the elements use.
storefront.js
OrgDepot is not defined: loadstorefront.jswith a plain<script src>(nottype="module"), before the code that uses it. Code in a module script after it is fine.await is only valid in async functions: top-levelawaitneeds a module script (<script type="module">) or anasyncfunction.- A blocked request shows as
NETWORK_ERROR(status 0) with the browser's reason incause: often a Content-Security-Policyconnect-srcmissinghttps://api.organizationdepot.com.
The Storefront
Problems that come from the shop itself: its sale, its carts and its checkout.
NOT_FOUND on everything
Every call answering 404 NOT_FOUND (the console says "isn't published" for the web components)
means the storefront isn't published. Publish it in Organization Depot. A single product being
null from products.get() means that handle isn't a product for sale: check the slug, and that
the product is active.
Photos don't load
Product photos come from https://api.organizationdepot.com. A Content-Security-Policy needs it in
img-src.
The cart is empty after checkout
That's by design. Once the buyer pays, the cart is CONVERTED: it's an order now. The next
cart.get() returns null, and the next addLine() starts a new cart. The web components do the
same, including when the buyer comes back with the Back button.
If your page keeps the cart in its own state, update it from cart:change, which fires with null
when the client finds the cart is over.
CART_CONVERTED
An edit (updateLine, removeLine, a discount code) hit a cart that was paid for, perhaps in
another tab, or that expired. The client has already forgotten it. Show the empty cart; the buyer's
next addLine() starts a new one. addLine(), setAnswers() and setContact() don't throw this:
they start the new cart themselves.
CART_LOCKED
The cart is mid-checkout. The client releases it and retries once by itself, so you only see this error when the cart is locked again straight away. Ask the buyer to try again.
Checkout opens the wrong address
- The platform address instead of your domain: your checkout domain isn't connected or isn't
live yet. Check the domain wizard on the Storefront page.
shop.checkoutDomainshows the host in use, andshop.urlthe full address checkout starts from. - "Continue shopping" or the confirmation goes somewhere else: set Your website on the Headless page. The checkout only ever links to that address.
- Our storefront shows your products as well as your site: choose Checkout only on the Headless page.
INVALID (400)
fieldiscode,quantity,lines[n].quantity,emailorname: the buyer typed something the API refuses, anderror.messagetells them what to fix. Show it.- Any other
field(first,after,lines,lines[n].variantId), or none: the page sent a request the API doesn't take, anderror.messagesays what, for you. Check the call against the HTTP reference and show the buyer your own words.
CUSTOMIZATION_INVALID
A personalization answer doesn't fit the product's form. error.field is the field's key and
error.message says what's wrong, in words for the buyer: show it on that field. The web
components do this for you.
SALE_CLOSED
The shop's sale isn't open (shop.sale.availableForSale is false): browsing works, starting a
cart doesn't. shop.sale.reason says why, and error.message is a sentence fit for the buyer:
reason |
Message | What to check in Organization Depot |
|---|---|---|
NOT_OPEN_YET |
"This shop isn't taking orders yet." | The shop is still a draft, or the sale opens later |
PAUSED |
"This shop isn't taking orders right now." | The shop is paused |
ENDED |
"This sale has ended." | The sale's closing time has passed |
Enable Add to cart only when shop.sale.availableForSale || shop.preview, so buyers don't hit
this, and show "Sold out" only from a variant's availableForSale, which is about stock.
Building your site while the shop is closed? Use a preview token
in your local settings: shop.preview is then true, and its carts work whatever the sale says
and pay only in test mode. The web components and starters let Add to cart through for it.
Preview carts pay only with test cards
That's what they're for. A cart made with a preview token (odpv_…) has preview: true; the
hosted checkout shows its real totals and takes only
Stripe's test cards, places no order and moves no money.
If buyers see "Preview cart" or "Test mode" on your live site, it's using a preview token: put a
public token (odpk_…) in its settings instead. The stored cart is kept per token, so buyers then
start on a new, real cart.