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

Source: https://docs.bifrostorbit.com/commerce/developers/storefront-api/

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](https://docs.bifrostorbit.com/commerce/developers/api-keys/) 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/northwilds
```

Answers 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

| 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:

| 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](https://docs.bifrostorbit.com/commerce/everyone/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`:

```json
{ "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 }
```

- `volume` lists the volume prices: from `min` items of the product, across its variants, each costs `price`. It is empty for a price from a MAM account.
- `quantityRule` is `{ "min", "max", "step" }` or `null`: the fewest, the most (or `null`) and the pack size one order can have. Step your quantity picker by `step`.

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](https://docs.bifrostorbit.com/commerce/everyone/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](https://docs.bifrostorbit.com/commerce/developers/customer-accounts/).

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

```js
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.

| 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`, `planId` and `planName` (for a subscription, else `null`), `digital`, `quantityRule`, `volumePercent` (the volume tier reached, or 0) and `giftCard`.
- Each shipping choice has `kind`, `id`, `name`, `price`, `free`, `minDays` and `maxDays`.
- `offers` lists every discount used: `id`, `code` (`null` for an automatic offer), `title`, `kind`, `automatic`, `amount` and `freeShipping`. Show an automatic offer by its `title`.
- `giftCards` lists the usable gift cards with `code`, `last4`, `amount` (what it pays) and `balance` (what stays on it). `giftCardProblems` says why a code can't be used, such as "The gift card ending 3HTR has expired", and stops checkout; `badGiftCards` are those codes, to remove.
- `needsShipping` is `false` when nothing in the cart is posted: every line is a download or a gift card. Then the cart needs no shipping choice or address.
- `totals` has `subtotal`, `discountTotal`, `shippingTotal`, `taxTotal`, `total`, `discount` (the code's `code` and `amount`), `discounts` (as `offers`), `giftCardTotal`, `amountDue` (what is left to pay by card) and `tax`: `mode` (`domestic`, `oss`, `reverseCharge` or `export`), `country`, `pricesIncludeTax` and `lines` (VAT by `rate`, with `base` and `tax`). `totals` is `null` while the cart can't be priced.
- `problems` lists what stops checkout, such as "Only 2 of Tind 2 tent are left" or "Gas canister is sold in multiples of 6". `discountProblem` says why a code doesn't apply, such as "That code has expired"; it doesn't stop checkout.
- `prices` is `list` for the shop's or a price list's prices, `account` when every line with a part number has the MAM account's price, and `fallback` when MAM didn't answer for some lines.
- With a signed-in customer, `customer` has their `name` and `email`, and `account` their MAM account: `number`, `onStop`, `creditLimit`, `balance`, `overCredit` (this order would take the account over its limit) and `branch`. Lines then have `priceSource` and `branches` as on products, and `pricesCheckedAt` says 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"` with `payment`: `clientSecret`, `publishableKey`, `amount` and `currency`. Show Stripe's Payment Element with them. See [Checkout with Stripe](https://docs.bifrostorbit.com/commerce/developers/checkout-with-stripe/).
- `"status": "placed"` with `payment: 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](https://docs.bifrostorbit.com/commerce/everyone/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:

```json
{
  "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](https://docs.bifrostorbit.com/commerce/developers/api-playground/) runs these endpoints with the shop's own products, and GraphQL reads the same catalogue in one request: see [GraphQL](https://docs.bifrostorbit.com/commerce/developers/graphql/).
