Organization DepotDevelopers v1.3.0API 2026-12
All pages

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, and organization (name, logoUrl, verified);

  • sale: { opensAt?, closesAt?, timezone, availableForSale, reason? }. availableForSale is the one thing that says whether buyers can order. When it's false, browsing works but starting a cart fails with SALE_CLOSED (except with a preview token), and reason says why:

    reason When SALE_CLOSED message
    NOT_OPEN_YET The shop isn't live yet, or the sale hasn't opened "This shop isn't taking orders yet."
    PAUSED You paused the shop "This shop isn't taking orders right now."
    ENDED The 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 by reason;

  • preview: true when the token is a preview token, whose carts work whatever the sale says and pay only in test mode; false for a public token;

  • branding: your shop's branding tokens (color.brand, font.body, …), which the web components apply for you;

  • websiteUrl and mode: what you set on the Headless page;

  • checkoutDomain: the host buyers pay on, and url: your hosted shop's address on it (https://papaswoodworks.storefront.organizationdepot.com/papas-woodworks, or https://shop.papaswoodworks.com on 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 its id as 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() returns null for 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, and products.list({ tag: "Hoodies" }) returns those products. A tag matches ignoring case and surrounding spaces. productCount counts 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. options are the choices a buyer makes (Wood: Walnut, Maple), each value with an optional group (sizes in "Youth" and "Adult") and image. variants are the combinations you sell, each with its own id, price, availableForSale and selectedOptions. quantityAvailable is 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 availableForSale is true when you don't track its stock or some is left; a product's is true when any variant's is. Neither changes when the sale closes, so show "Sold out" from them and nothing else, and check shop.sale.availableForSale (or shop.preview) for the sale.
  • Prices. priceRange has 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 is PURCHASE_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 images and a featuredImage. An option value can have its own image (the board in maple), and a variant has the image of 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 on undefined. small suits thumbnails and cart lines, medium product cards and pages, original zooming.
  • altText is 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 in img-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 TEXT answer that misses a shop's own length or format rule fails with CUSTOMIZATION_INVALID: error.field is the key and error.message explains, 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 customizations and a customizationSummary ([{ 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 answers and answersSummary show 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.

  1. cart.checkoutUrl is 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.checkoutDomain tells you which host it is.
  2. cart.checkout() reads the cart and sends the browser there. (On a server, redirect the buyer to checkoutUrl yourself.)
  3. 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.
  4. Tax, fees and the total are worked out there. The cart on your site has a subtotal only.
  5. Questions: checkout asks any required checkout question the cart hasn't answered.
  6. After paying, the buyer sees the confirmation and gets the receipt email, and the order appears in Organization Depot with its personalization.

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).