The storefront API
Read the shop, products and collections, build a cart, check out with Stripe or on invoice and show the order, from your own website. Endpoints, fields, CORS, caching and rate limits.
Reference13 min read
The storefront API lets your own website show products with live prices and stock, run a cart and take orders, without sending shoppers to the hosted shop. It needs no key: it returns what the hosted shop shows anyway, and payments go straight from the shopper's browser to the shop's own Stripe account. An API key is optional: it counts your requests, and a secret key also reads draft products for a preview.
Every address starts with /commerce/api/public/ and the shop address from Settings:
https://app.bifrostorbit.com/commerce/api/public/northwildsAnswers are JSON. A successful answer has the result in data; an error has error, with a code and a message you can show to shoppers as it is. Validation errors (422) add issues, one per field.
Amounts are whole minor units in the currency given: 27900 with EUR is €279.00.
Languages and currencies
The catalogue endpoints take ?locale=, such as nb, and the product endpoints ?currency=, such as NOK. Without them, or with one the shop doesn't offer, you get the main language and currency. Each product says which language it is in with locale. Text that isn't translated comes in the main language. Prices are the shop's own price in that currency, never a conversion; a variant without a price in it has "price": null and can't be bought in it.
Which websites can call it
https://northwilds.com, and the hosted shop. Others get 403.| Endpoints | Who can call them from a browser |
|---|---|
| The shop, products and collections | Any website. |
| Carts, checkout, orders and customer accounts | Only the shop's Website and Allowed websites in Settings, such as https://northwilds.com, and the hosted shop. Others get 403. |
| Anything with a customer token | The same allowed websites only. |
Calls from a server, without an Origin header, are always allowed. Only the scheme, host and port count, so https://www.northwilds.com is a different website from https://northwilds.com. Allowed headers are Content-Type, Authorization, X-Api-Key and X-Customer-Token. Answers carry Vary: Origin, and preflight answers can be cached for 10 minutes.
The shop
GET/commerce/api/public/northwilds
Returns slug, name, logoUrl, colour, currency (the main one), currencies (main first), locales (main first), defaultLocale, pricesIncludeTax, shopUrl (the hosted shop), termsUrl and returnsDays. Cacheable for 60 seconds.
Products
GET/commerce/api/public/northwilds/products
Lists active products, by title. Query parameters, all optional:
idsslugstagwinter.collectionquerytent.limitlocale, currency| Parameter | Means |
|---|---|
ids | Up to 100 product ids, separated by commas. The answer keeps your order. |
slugs | Up to 100 product addresses, separated by commas. |
tag | Only products with this tag, such as winter. |
collection | Only products in this collection, by address or id, in the collection's order. |
query | Products whose title contains this, such as tent. |
limit | How many, 100 unless you say, at most 250. |
locale, currency | As above. |
GET/commerce/api/public/northwilds/products/fjell-gtx-boot
One product, by address or id.
Each product has id, slug, title, summary, description (HTML), vendor, productType, tags, options (name, values), variants, images (url, alt), specs (label, value), price (min, max, currency, compareAt), available (any variant can be bought), status, seo (title, description, filled from the title and summary when empty), url (the product on the hosted shop), locale, updatedAt, stockSyncedAt (when stock last came from MAM, or null), metafields (your custom fields by key, only those shown to websites) and bundle (components: productId, variantId, quantity, or null when it isn't a bundle).
Each variant has id, title (such as 42 / Brown), options, sku, price, compareAtPrice (only in the main currency), available, quantity (units in stock, or null when stock isn't tracked), requiresShipping and digital (a download, nothing posted), plans (subscription plans: id, name, interval of week, month or year, intervalCount and price), metafields, volume and quantityRule:
{ "id": "v3f9a1c2b7e", "title": "42 / Brown", "options": ["42", "Brown"], "sku": "FJELL-GTX-42-BRN", "price": 24900, "compareAtPrice": null, "available": true, "quantity": 7, "requiresShipping": true, "digital": false, "plans": [], "metafields": { "leather": "Full-grain" }, "volume": [{ "min": 2, "percent": 5, "price": 23655 }], "quantityRule": null }volumelists the volume prices: fromminitems of the product, across its variants, each costsprice. It is empty for a price from a MAM account.quantityRuleis{ "min", "max", "step" }ornull: the fewest, the most (ornull) and the pack size one order can have. Step your quantity picker bystep.
A bundle's quantity is how many whole bundles its products make, so it sells out when one of them does.
A product with giftCard: true is a gift card. Its code is emailed once the order is paid, and it needs no delivery. See Gift cards.
Product answers can be cached for 30 seconds.
For a signed-in customer
Send the customer's token in X-Customer-Token and products, collections and carts come with that customer's prices and, for a MAM account, live stock per branch. Variants then also have listPrice, priceSource and branches, and products pricing. Those answers are private and never cached. See Customer accounts and personal prices.
Collections
GET/commerce/api/public/northwilds/collections
Active collections by title, each with id, slug, title, description, image, productCount and url. ids= asks for up to 100 chosen collections. Cacheable for 60 seconds.
GET/commerce/api/public/northwilds/collections/tents
One collection, by address or id, with products in the collection's order. Takes locale, currency and limit (100 unless you say). Cacheable for 30 seconds.
Carts
A cart lives in Commerce Orbit and is known by its token, such as c_Q2x…. Anyone with the token can read and change the cart, so keep it in the shopper's browser, not in a link others see.
POST/commerce/api/public/northwilds/carts starts a cart and answers 201 with it. The body can hold any of the update fields below, and source, up to 60 characters, to say where the order came from; it defaults to the calling website.
const response = await fetch("https://app.bifrostorbit.com/commerce/api/public/northwilds/carts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ currency: "NOK", locale: "nb", add: [{ variantId: "v3f9a1c2b7e", quantity: 1 }] }),});const { data: cart } = await response.json();localStorage.setItem("cart", cart.token);GET/commerce/api/public/northwilds/carts/{token} reads it, priced.
PATCH/commerce/api/public/northwilds/carts/{token} changes it and answers with the cart. Send only what changes; null clears a field.
addvariantId, quantity, optional productId and planId (one of the variant's plans, to subscribe). Up to 50. Adds to a line already there, up to 999; a planId given here, or null, changes that line's plan.linesquantity of 0 removes a line. Send each line's planId again to keep it a subscription. Up to 100.currency, localeemail, name, phonevatNumberDE123456789. It can make the order reverse charged.discountCodeWINTER15.giftCards["7KQ2-MNB4-XW8P-3HTR"]. Replaces the ones there; [] removes them all. Spaces, hyphens and small letters don't matter.countryNO, for shipping choices and VAT before there is an address.shippingAddressname, company, line1, line2, postalCode, city, region, country (two letters), phone.shipping{ "kind": "rate", "rateId": "…" } or { "kind": "pickup", "locationId": "…" }, from shippingChoices.note| Field | Means |
|---|---|
add | Lines to add: variantId, quantity, optional productId and planId (one of the variant's plans, to subscribe). Up to 50. Adds to a line already there, up to 999; a planId given here, or null, changes that line's plan. |
lines | Replaces every line. A quantity of 0 removes a line. Send each line's planId again to keep it a subscription. Up to 100. |
currency, locale | One of the shop's currencies and languages. |
email, name, phone | The shopper. An email that belongs to a customer in a company gives the cart that company's prices and terms. |
vatNumber | A business buyer's VAT number, such as DE123456789. It can make the order reverse charged. |
discountCode | A discount code, such as WINTER15. |
giftCards | Gift card codes, up to 5, such as ["7KQ2-MNB4-XW8P-3HTR"]. Replaces the ones there; [] removes them all. Spaces, hyphens and small letters don't matter. |
country | Two letters, such as NO, for shipping choices and VAT before there is an address. |
shippingAddress | name, company, line1, line2, postalCode, city, region, country (two letters), phone. |
shipping | { "kind": "rate", "rateId": "…" } or { "kind": "pickup", "locationId": "…" }, from shippingChoices. |
note | A note from the shopper, up to 1,000 characters. |
Lines whose product isn't active are dropped. Changing a cart while its checkout is waiting for payment starts the checkout over.
The cart
The cart answer has token, currency, currencies, locale, lines, count (items), email, name, phone, vatNumber, company (name, terms: card or invoice, termsDays, onStop, or null), discountCode, discountProblem, offers, giftCards, giftCardCodes, giftCardProblems, badGiftCards, needsShipping, shippingAddress, country, shipping, shippingChoices, totals, prices, problems and orderId.
- Each line has
variantId,productId,slug,title,variantTitle,sku,image,quantity,unitPrice(what the shopper pays each),listPrice(before a company price or volume price),total,available,problem,planIdandplanName(for a subscription, elsenull),digital,quantityRule,volumePercent(the volume tier reached, or 0) andgiftCard. - Each shipping choice has
kind,id,name,price,free,minDaysandmaxDays. offerslists every discount used:id,code(nullfor an automatic offer),title,kind,automatic,amountandfreeShipping. Show an automatic offer by itstitle.giftCardslists the usable gift cards withcode,last4,amount(what it pays) andbalance(what stays on it).giftCardProblemssays why a code can't be used, such as "The gift card ending 3HTR has expired", and stops checkout;badGiftCardsare those codes, to remove.needsShippingisfalsewhen nothing in the cart is posted: every line is a download or a gift card. Then the cart needs no shipping choice or address.totalshassubtotal,discountTotal,shippingTotal,taxTotal,total,discount(the code'scodeandamount),discounts(asoffers),giftCardTotal,amountDue(what is left to pay by card) andtax:mode(domestic,oss,reverseChargeorexport),country,pricesIncludeTaxandlines(VAT byrate, withbaseandtax).totalsisnullwhile the cart can't be priced.problemslists what stops checkout, such as "Only 2 of Tind 2 tent are left" or "Gas canister is sold in multiples of 6".discountProblemsays why a code doesn't apply, such as "That code has expired"; it doesn't stop checkout.pricesislistfor the shop's or a price list's prices,accountwhen every line with a part number has the MAM account's price, andfallbackwhen MAM didn't answer for some lines.- With a signed-in customer,
customerhas theirnameandemail, andaccounttheir MAM account:number,onStop,creditLimit,balance,overCredit(this order would take the account over its limit) andbranch. Lines then havepriceSourceandbranchesas on products, andpricesCheckedAtsays when MAM answered.
Check out
POST/commerce/api/public/northwilds/carts/{token}/checkout with { "method": "card" } or { "method": "invoice" }.
The cart needs lines, an email, a name, a shipping choice and an address unless it is collection or only downloads, and no problems. Checkout creates the order and takes the stock. It answers with orderId, reference (such as NW1042), status and payment:
"status": "pay"withpayment:clientSecret,publishableKey,amountandcurrency. Show Stripe's Payment Element with them. See Checkout with Stripe."status": "placed"withpayment: null: an order on invoice, or one gift cards paid in full, placed straight away.
With gift cards, payment.amount is amountDue: the gift cards are held at checkout and only the rest goes to Stripe. Gift cards can't be used with invoice. If the shopper doesn't pay, the held amount goes back on the cards.
For a customer with a MAM account, checkout asks MAM for the account's prices again. If a price changed since the cart showed it, checkout answers 409 with the code price_changed and a message naming each change; read the cart again, show the new total and check out again. An account on stop can't check out on invoice. Orders go to MAM under the customer's account.
A cart with a subscription line is paid by card only, and every subscription in it needs the same interval. Checkout then starts a Stripe subscription on your account: payment looks the same, and the amount is the first period of the subscriptions plus everything else in the cart. Each later period Stripe charges the subscriptions (and their delivery), and Commerce Orbit adds a paid order for it. See Subscriptions.
Calling checkout again before the order is paid cancels the earlier unpaid order and makes a new one. Once an order is paid or on invoice, the cart can't be checked out again.
The order
GET/commerce/api/public/northwilds/orders/NW1042?email=ingrid@northwilds.com
The order for a thank-you or order page. It needs the email the order was placed with; any other email gets 404. If a card payment is still waiting, Commerce Orbit asks Stripe first, so the answer is current when the shopper comes back from paying.
It has reference, status (open or cancelled), paymentStatus (pending, paid, failed, invoiced, partiallyRefunded or refunded), fulfilment (unfulfilled, partial or fulfilled), placedAt, currency, lines (title, variantTitle, quantity, total, image), subtotal, discountTotal, shippingTotal, taxTotal, total, pricesIncludeTax, taxMode, shipping (the rate's or location's name), tracking (carrier, number, url for each parcel), email and downloads, one for each file bought: filename, title, url, downloads (used), limit, expiresAt and usable. A download url works for 7 days; read the order again for a fresh one.
GET/commerce/api/public/northwilds/orders/NW1042/pdf?email=ingrid@northwilds.com downloads the receipt, or the invoice for an order on invoice, once it is paid or placed. Before that it answers 409.
Returns
GET/commerce/api/public/northwilds/returns?reference=NW1042&email=ingrid@northwilds.com
What can be returned from an order: orderReference, open (whether a return can be asked for now), until (the end of the return window), reason (why not, when open is false), lines (lineId, title, variantTitle, image and left, how many can still come back) and returns, the returns already asked for. For a signed-in customer, leave out email.
POST/commerce/api/public/northwilds/returns asks for a return:
{ "reference": "NW1042", "email": "ingrid@northwilds.com", "lines": [{ "lineId": "l1", "quantity": 1, "reason": "tooSmall" }], "note": "The boots are half a size too small."}reason is one of tooSmall, tooBig, notAsDescribed, damaged, wrongItem, changedMind or other. It answers 201 with the return, or 409 when the return window has closed or nothing on the order can come back. The shop's team gets a notification and the customer an email.
GET/commerce/api/public/northwilds/returns/R1001?email=ingrid@northwilds.com reads a return: reference, orderReference, status (requested, approved, rejected, received, refunded or cancelled), lines, note, decisionNote (the shop's message), label (carrier, trackingNumber, trackingUrl, labelUrl), refundAmount, currency and createdAt.
Gift card balance
POST/commerce/api/public/northwilds/gift-cards with { "code": "7KQ2-MNB4-XW8P-3HTR" } answers with last4, balance, currency, expiresAt and usable, or 404 for a code that doesn't exist. It allows a few tries a minute.
Rate limits
Limits count per IP address. Over them, the answer is 429 with "Too many tries. Wait a minute and try again."
| Endpoints | Limit |
|---|---|
| The shop, products and collections | 120 requests a minute per shop |
| Start a cart | 10 at once, then 5 a minute per shop |
| Check out | 20 a minute per cart |
| Send a sign-in code | 5 at once, then 1 a minute per address and per IP address |
| Sign in | 10 at once, then 2 a minute |
| Personal prices | 30 a minute per customer |
| The order and its PDF | 20 a minute per order |
| Read returns | 20 a minute per order or return |
| Ask for a return | 10 at once, then 5 a minute per order |
If your server calls on behalf of many shoppers, all its calls share its address, so cache catalogue answers on your side.
Try it
The API playground runs these endpoints with the shop's own products, and GraphQL reads the same catalogue in one request: see GraphQL.
Was this page helpful?