Concepts
How the pieces behave: the shop and its products, personalization, the cart, and the hand-off to
checkout. The same rules apply whether you use the web components, storefront.js or the HTTP
API.
A storefront is read with a token from your shop's Headless page: a public token (odpk_…) on
your live site, and a preview token (odpv_…) while you build. Tokens explains
both.
The shop
store.shop.get() (or GET /shop) describes the storefront the token names:
-
title,currencyCode, andorganization(name,logoUrl,verified); -
sale:{ opensAt?, closesAt?, timezone, availableForSale, reason? }.availableForSaleis the one thing that says whether buyers can order. When it'sfalse, browsing works but starting a cart fails withSALE_CLOSED(except with a preview token), andreasonsays why:reasonWhen SALE_CLOSEDmessageNOT_OPEN_YETThe shop isn't live yet, or the sale hasn't opened "This shop isn't taking orders yet." PAUSEDYou paused the shop "This shop isn't taking orders right now." ENDEDThe sale has closed, or the shop was retired "This sale has ended." Enable Add to cart when
shop.sale.availableForSale || shop.preview, and otherwise label it byreason; -
preview:truewhen the token is a preview token, whose carts work whatever the sale says and pay only in test mode;falsefor a public token; -
branding: your shop's branding tokens (color.brand,font.body, …), which the web components apply for you; -
websiteUrlandmode: what you set on the Headless page; -
checkoutDomain: the host buyers pay on, andurl: your hosted shop's address on it (https://papaswoodworks.storefront.organizationdepot.com/papas-woodworks, orhttps://shop.papaswoodworks.comon your own domain); -
checkoutQuestions: the questions asked once per order, if any (see Checkout questions).
A storefront that isn't published answers 404 NOT_FOUND to everything, the same as its hosted
pages would.
Products, options and variants
- Handle. A product is named by its
handle, the slug from its address on your hosted storefront (walnut-end-grain-board). A product without a slug uses itsidas its handle.products.get(handle)accepts either. - Only products for sale. The API shows active products only; a product you withdraw
disappears from it, and
products.get()returnsnullfor it. - Order and paging.
products.list()returns your shop's display order, 20 at a time by default and at most 100, with a cursor for the next page (pageInfo.endCursor). - Tags. Tags group your products.
tags.list()lists every tag with how many products carry it, in your display order, andproducts.list({ tag: "Hoodies" })returns those products. A tag matches ignoring case and surrounding spaces.productCountcounts every product the API lists with that tag, sold-out ones included, so it always equals what the filter returns. A tag only on a draft or withdrawn product isn't listed. - Options and variants.
optionsare the choices a buyer makes (Wood: Walnut, Maple), each value with an optionalgroup(sizes in "Youth" and "Adult") andimage.variantsare the combinations you sell, each with its ownid,price,availableForSaleandselectedOptions.quantityAvailableis there only when you track stock.variantFor(product, { Wood: "Walnut" })finds the variant for a choice. A product without options has one variant. - Availability is stock. A variant's
availableForSaleistruewhen you don't track its stock or some is left; a product's istruewhen any variant's is. Neither changes when the sale closes, so show "Sold out" from them and nothing else, and checkshop.sale.availableForSale(orshop.preview) for the sale. - Prices.
priceRangehas the lowest and highest variant price, for "From $34.00". - Limits.
purchaseLimit, when set, is the most of that product one order may hold; going over isPURCHASE_LIMIT.
Product details
properties are a product's details as labelled text, in order, ready to show as a list:
product.properties; // [{ label: "Wood", value: "Black walnut" }, { label: "Size", value: "16 × 10 in" }]
They're set per product in Organization Depot.
Images
Every photo is an Image:
{
url: "https://api.organizationdepot.com/api/media/v2/…/variants/ORIGINAL",
altText: "Walnut end-grain board",
sizes: { small: "…/SMALL", medium: "…/MEDIUM", original: "…/ORIGINAL" },
}
- A product has
imagesand afeaturedImage. An option value can have its ownimage(the board in maple), and a variant has theimageof its photo option's value. Show the variant's image, else the chosen value's, else the featured one, and the photo follows the buyer's choice. imageUrl(image, "medium")picks a size and is safe onundefined.smallsuits thumbnails and cart lines,mediumproduct cards and pages,originalzooming.altTextis the product's title when nothing better exists.- Photos are served from the API's host (
/media/v2/…), so a Content-Security-Policy needs it inimg-src; see Security.
Personalization
A product can ask for answers per item, such as an engraving or a player's name and number. Its
customizationForm is a form (the same form JSON Organization Depot uses everywhere), and
customizationFields(form) turns it into plain fields to render. For an engraving field whose
form says "helpText": "Up to 30 characters":
OrgDepot.customizationFields(product.customizationForm);
// [{ key: "engraving", label: "Engraving", type: "TEXT", required: true, maxLength: 30,
// description: "Up to 30 characters" }]
The descriptor's description is the field's help text, to show under the input. The form JSON
is in the HTTP reference.
type is TEXT, NUMBER, SELECT (with options: [{ value, label }]) or CHECKBOX. Send each
answer under its key in the line's customizations: a string for TEXT and SELECT, a number
for NUMBER, a boolean for CHECKBOX.
await store.cart.addLine({ variantId, quantity: 1, customizations: { engraving: "J & J" } });
- The server checks every answer against the product's form. A missing required answer, a
value that's too long or out of range, a choice that isn't offered, or a
TEXTanswer that misses a shop's own length or format rule fails withCUSTOMIZATION_INVALID:error.fieldis the key anderror.messageexplains, in words fit to show the buyer. Check in the page first for a quick answer, but the server is the rule. - Same answers, same line. Adding a variant with the same customizations as a line already in the cart adds to that line's quantity. Different customizations make a separate line.
- In the cart, each line has its
customizationsand acustomizationSummary([{ label: "Engraving", value: "J & J" }]) ready to show.
Checkout questions
A shop can also ask questions once per order, such as a pickup time. They're
shop.checkoutQuestions, a form in the same format; customizationFields() reads it too.
- Send the answers with
cart.setAnswers({ pickup: "Saturday" }). It replaces all the cart's answers. - You may skip them. The hosted checkout asks any required question the cart hasn't answered.
- The cart's
answersandanswersSummaryshow what was given.
The cart lifecycle
A cart is a buyer's basket in your shop. The client keeps track of it for you, so no method takes a cart id.
status |
Means |
|---|---|
OPEN |
Being filled |
LOCKED |
The buyer is on the checkout page; an edit releases it first |
CONVERTED |
Paid for: it's an order now, so a new cart starts |
ABANDONED |
Expired; treated like CONVERTED |
Where the cart id lives
The cart id is kept in the browser's localStorage under od:cart:{token}, so the cart survives
page loads and is shared by your site's tabs: one cart per storefront per browser.
The cart id is the key to the buyer's cart. Anyone with it can read and change that cart. Don't
log it, put it in a URL, or send it to analytics. When localStorage is blocked or full, or with
cartStorage: "none", the id is kept in memory for as long as the client lives (until the page is
left).
The first add creates the cart
addLine, addLines, setAnswers and setContact start a cart when there's none.
updateLine, removeLine and the discount-code calls need a cart, and throw NO_CART without
one.
Converted carts start over
After the buyer pays, the stored cart is CONVERTED. The next cart.get() returns null and
forgets it, and the next addLine creates a new cart. The same happens for an ABANDONED cart and
for one the API no longer knows.
When an edit fails because the cart is over (paid in another tab, say), the client checks the cart
first, then forgets it. addLine(s), setAnswers and setContact then start a new cart with the
same change; the others throw (CART_CONVERTED or NOT_FOUND), so your page can show the empty
cart.
Locked carts are released
While the buyer is on the checkout page, the cart is LOCKED, so its price can't change under
them. If they come back to your site and change the cart, the edit gets CART_LOCKED; the client
then releases the cart (back to OPEN) and retries the edit once. The checkout prices the cart
again when they return to it.
One call at a time
Cart calls run in order, one after another, so two quick clicks on Add to cart never create two carts.
Listening for changes
store.on("cart:change", listener) hears every change the client makes: the new cart after each
edit, and null on clear() or when the stored cart turns out to be over. A plain cart.get()
that finds the cart unchanged doesn't fire it. The web components listen to the same event, so your
code and the elements stay in sync.
The checkout handoff
Payment always happens on the hosted checkout, never on your page. Card details, 3-D Secure, Apple Pay and Google Pay stay there, on your shop's checkout domain and in its branding.
cart.checkoutUrlis your hosted shop's address (shop.url) followed by/c/{cartId}. That's your own domain (https://shop.your-club.com/c/…) once it's connected and live, and otherwise your shop's platform address, where each of your organization's shops has its own path:https://{your organization}.storefront.organizationdepot.com/{your shop}/c/….shop.checkoutDomaintells you which host it is.cart.checkout()reads the cart and sends the browser there. (On a server, redirect the buyer tocheckoutUrlyourself.)- The checkout page stores the cart, removes its id from the address bar at once (so it isn't left in the browser's history), and shows the one-page checkout.
- Tax, fees and the total are worked out there. The cart on your site has a subtotal only.
- Questions: checkout asks any required checkout question the cart hasn't answered.
- After paying, the buyer sees the confirmation and gets the receipt email, and the order appears in Organization Depot with its personalization.
Back links
When Your website is set on the Headless page, the checkout's "Continue shopping", its header's
shop link and the confirmation's back link all go there. There's no ?return_to= parameter: only
the address you configured is used, so the checkout can't be made to send buyers anywhere else.
Hosted storefront or Checkout only
- Hosted storefront (the default): our full storefront stays on your checkout domain, and your website can sell too.
- Checkout only: our domain shows checkout and the confirmation, nothing else. Its home and product pages send buyers to your website, so they never find a second shop. Checkout only needs Your website to be set.
Money
Money is { amount: "38.00", currencyCode: "USD" }: the amount is a decimal string in major units,
so "38.00" is $38, not 38 cents. Strings keep amounts exact; don't add them as floating-point
numbers.
formatMoney(money) formats one for the buyer's locale ($38.00).