Skip to content
Coritan Docs

Build a checkout with the Store API

Use a publishable key to list products, fill a cart, take payment and show orders on your own site.

View as Markdown

The Store API is what your storefront calls to sell the goods in your organization's store. It lists the catalogue, fills carts, takes payment, places orders and shows shoppers their orders. You set up and run the store with the Commerce API.

Every route is under https://api.coritan.com/api/v1/orgs/{org_slug}/store. The API reference lists them under Commerce Store API, with the product routes under Catalog & Services and the shopper's account routes under Customers.

  • Set up the store with the Commerce API: a region, a stock location, shipping, and published products with prices. Set up the store explains each part.
  • Create a publishable key, as Create publishable keys and sales channels shows. Build with a test key, whose token starts with pk_test_. A live key works once we switch the store to live.
  • Set the store's storefront_url, because the order emails link to pages on it, and list your storefront's address in allowed_origins.
  • We send order emails with your organization's email settings. Choose how your email leaves before you take orders.
  • To let shoppers sign in, add customer sign-in to your storefront. Shoppers can also check out as guests, unless you turn off the store's guest_checkout setting.

Send the key in the x-publishable-api-key header of every request. It names your store and cannot change it, so it is safe to put in the browser.

Shell
curl "https://api.coritan.com/api/v1/orgs/acme/store" \
  -H "x-publishable-api-key: $PUBLISHABLE_KEY"

The answer is the store as your storefront needs it: its name, whether the key is in livemode, the currencies and locales with their defaults, the default_region_id, the key's sales_channels, the storefront_url and support_email, the terms_url and privacy_url, and whether guest_checkout is on. The store also holds turnstile, with enabled and the widget's site_key: when enabled is true, show the Cloudflare Turnstile challenge with that key on your sign-up form and send its answer as turnstile_token, as Register a customer shows.

  • A test key reaches test carts and orders only, and a live key reaches live ones. A cart or order of the other mode answers 404.
  • A key sells the products of its sales channels. The catalogue routes and new carts take a sales_channel_id to narrow to one of them.
  • When the shopper is signed in, also send their customer token as Authorization: Bearer $CUSTOMER_TOKEN. It prices products for the shopper's customer groups, makes the carts they change theirs, and opens their account and order routes.
  • While the store lists no allowed_origins, a browser on any site can call the Store API. Once it lists some, only those sites can. A change to the list takes up to 30 seconds to apply.

GET /store/products lists the published products in the key's sales channels, newest first.

Parameter Takes
q Text to find in the title, subtitle, handle or tags
collection_id or collection_handle A published collection. A smart collection follows its rules.
category_id or category_handle A category, with every category under it
tag One tag, in any case
id A product ID, repeated for more, up to 100
order -created_at (the default), created_at, title or -title
limit, offset Up to 100 a page; 20 by default
Shell
curl "https://api.coritan.com/api/v1/orgs/acme/store/products?collection_handle=clothing&country_code=DE" \
  -H "x-publishable-api-key: $PUBLISHABLE_KEY"

Every answer is priced for one place: the region_id you send, else the region that sells to country_code, else currency_code, else the store's default region. The answer's region and currency_code say which it used. Each variant has:

  • calculated_price, with the amount the shopper pays, the original_amount before any price list, the merchant's compare_at_amount, and is_discounted. It is null when the variant has no price there.
  • purchasable, which is true when the variant has a price and can be ordered.
  • availability: in_stock, low_stock (5 or fewer left), backorder, out_of_stock, or unlimited for a variant that does not track stock. Stock counts never leave our servers.

GET /store/products/{id_or_handle} answers one product by its handle or ID, priced the same way.

For menus and pickers:

  • GET /store/collections lists the published collections, and GET /store/collections/{handle} answers one. Its products come from GET /store/products?collection_handle=....
  • GET /store/categories answers the categories as a tree, and GET /store/categories/{handle} answers one with its children and parent. A category that is inactive or internal is hidden, with everything under it.
  • GET /store/regions lists the regions with their countries, currency_code, tax_inclusive and payment_providers, for a country picker. GET /store/regions/{region_id} answers one.

Catalogue answers to requests without a shopper's token can be cached for up to 30 seconds, so a change to a product can take that long to show.

Create the cart when the shopper adds their first item. Send their country, and the cart takes the region that sells there, with its currency:

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/store/carts" \
  -H "x-publishable-api-key: $PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"country_code": "DE", "email": "alex@example.com", "items": [{"variant_id": 1841, "quantity": 2}]}'

The answer is 201 with the whole cart. Keep its id, which starts with cart_, in the shopper's browser. Every change answers the whole cart again, priced from the catalogue as it is at that moment.

  • Without country_code or region_id, the cart takes the store's default region. A country the store does not sell to answers 422 with country_not_served.
  • POST /store/carts/{cart_id}/line-items adds a variant_id with a quantity. The same variant with the same metadata joins its existing line. A line holds up to 999 units, and a cart up to 100 lines.
  • PATCH /store/carts/{cart_id}/line-items/{line_id} sets a line's quantity, and 0 removes the line. DELETE on the same path removes it too.
  • PATCH /store/carts/{cart_id} sets the email, the shipping_address and billing_address, a note of up to 2,000 characters, the locale and metadata. A new country_code or region_id moves the cart to that region and prices every line in its currency.
  • Adding an item holds no stock: we reserve it when the order is placed. Asking for more than there is answers 409 with insufficient_inventory, and available gives the units left for each variant.

Anyone who holds a guest cart's ID can reach it. When a signed-in shopper changes a cart, it becomes theirs, and anyone else then gets 403 with cart_forbidden. Only an open cart changes, and a cart that nobody touches for 30 days becomes abandoned.

An address takes first_name, last_name, company, address_1, address_2, city, province, postal_code, country_code and phone. To ship and tax an order, it needs the country_code, a first or last name, address_1, city, and a postal_code where the country uses them. A Canadian address also needs its province, as a code such as ON or a name such as Ontario, because the tax depends on it. The shipping country must be one of the cart's region.

To sell a gift card, put its recipient in the line's metadata: recipient_email, recipient_name, sender_name and a message of up to 500 characters. We issue one card for each unit, and email it to the recipient once the order is placed, or to the shopper when the line names nobody.

  • POST /store/carts/{cart_id}/promotions with a code applies a discount code. A code that does not apply answers 422 with promotion_not_applicable, a reason such as expired, min_subtotal or first_order_only, and a message you can show. A cart holds up to 5 codes. Remove one with DELETE /store/carts/{cart_id}/promotions?code=WELCOME10. Automatic promotions apply without a code.
  • POST /store/carts/{cart_id}/gift-cards with a code applies a gift card, up to 5 a cart. A wrong code answers 422 with gift_card_invalid, and a card that cannot pay answers 422 with gift_card_not_applicable and a reason: disabled, expired, exhausted or currency_mismatch. The cart shows each card's last4 and the amount it applied.

Each cart, and each IP address, gets 10 gift card attempts in 10 minutes, right or wrong. When the cart changes, codes and cards that no longer apply leave it.

Once the cart has a shipping address, GET /store/carts/{cart_id}/shipping-options lists the options that ship its items there, priced for them. Each option has its id, name, profile_id, amount and currency_code, whether it is_pickup, and its delivery_min_days and delivery_max_days.

Items in different shipping profiles need one option for each profile. Choose one with POST /store/carts/{cart_id}/shipping-methods and its option_id: it replaces the method that profile had. A cart with nothing to ship, such as one that holds only gift cards, needs a billing address instead of shipping.

The cart's totals hold what the shopper pays: the subtotal, discount_total, shipping_total, tax_total and gift_card_total, and the total that is left to pay after gift cards. Each line has its own totals. Until the cart has an address, tax_estimated is true and the tax is an estimate for the region's first country.

We work out the tax from Coritan's own rates. Each tax in a line's tax_lines, or a shipping method's, has its code, name, rate (a fraction, such as 0.19) and amount, and the jurisdiction it is owed to: the country_code, and the subdivision_code of a state or province that charges it.

The cart's blockers list what stops checkout, and completable is true when the list is empty.

Blocker What to do
empty Add an item.
region_unavailable The cart's region was disabled. Move the cart to another region.
item_unavailable An item is no longer for sale. Remove it.
insufficient_inventory An item has less stock than the cart asks for. Lower the quantity.
price_unavailable An item has no price in the cart's currency. Remove it.
email_required Set the cart's email.
customer_required The store does not take guest orders. Sign the shopper in.
shipping_address_required Add a complete shipping address.
billing_address_required Nothing in the cart ships, so add a complete billing address.
shipping_method_required Choose a shipping option for each profile.
tax_unavailable We cannot work out the tax for the address. A Canadian address needs its province. For an address elsewhere, ask support.

Starting a payment or completing the cart while something blocks it answers 409 with cart_not_ready and the blockers. There, the list can also hold seller_unavailable, which means no Coritan company sells to the address.

  1. List what can take the payment with GET /store/carts/{cart_id}/payment-providers. Each provider has an id, a display_name and the config its browser script needs. The answer also has the total and payment_required, which is false when gift cards pay for everything: then go straight to Complete the order.

  2. Start a payment session with the provider the shopper picks. Send accept_terms: true once the shopper has accepted your terms.

    Shell
    curl -X POST "https://api.coritan.com/api/v1/orgs/acme/store/carts/cart_01j8z4a7b2c9d3e6f5g8h1k0m4/payment-sessions" \
      -H "x-publishable-api-key: $PUBLISHABLE_KEY" \
      -H "Content-Type: application/json" \
      -d '{"provider": "stripe", "accept_terms": true}'
    
  3. Let the shopper pay in the browser with the payment_session from the answer:

    • stripe: mount Stripe's Payment Element with config.publishable_key and the session's client_secret, and confirm the payment with Stripe.js.
    • paypal: send the shopper to the session's approve_url, or show PayPal's buttons with config.client_id and the paypal_order_id. PayPal brings the shopper back to the return_url you sent, or to the cancel_url when they stop. Both must be on your storefront's origin or in allowed_origins, and both default to <storefront_url>/checkout.
    • manual: a test payment that moves no money. Only test carts offer it, and it is authorized at once.

Starting a session again with the same provider for the same amount answers the same session, and a session with another provider replaces it. When the cart changes after the payment started, start the session again. The shopper pays the Coritan company that sells to their address, which also issues the invoice.

POST /store/carts/{cart_id}/complete checks the payment with the provider and places the order. Send an Idempotency-Key header: a value of up to 128 characters that you make once for each order the shopper places.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/store/carts/cart_01j8z4a7b2c9d3e6f5g8h1k0m4/complete" \
  -H "x-publishable-api-key: $PUBLISHABLE_KEY" \
  -H "Idempotency-Key: 5f0c2a6e-1d3b-4c7e-9a41-2b8f6d0e7c19" \
  -H "Content-Type: application/json" \
  -d '{"accept_terms": true}'

The answer is 200 with the order and the access_token for its page. We email the shopper a confirmation that links to <storefront_url>/orders/{order_id}?token=<access_token>, a page your storefront serves.

  • A retry with the same key for the same cart, within 24 hours, answers what the first request answered. A cart that already has its order answers that order again, with or without the key.
  • The same key for another cart answers 409 with idempotency_key_reused, and a retry while the first request still runs answers 409 with idempotency_in_progress.
  • We price the cart again at this step. When its total no longer matches the payment, the answer is 409 with payment_session_stale: start the payment again.
  • When the money moved but the order cannot be placed, for example because another shopper bought the last unit, we refund the payment. The answer is 409 with the reason and payment_refunded: true.
  • When the shopper closes the page after paying, we place the order once the payment provider reports the payment.
  • A guest's first order adds a customer with that email to your organization, without a password. Their later orders join the same customer.
Answer What to do
409 with payment_required Start a payment session first.
409 with payment_requires_action The shopper has not finished paying, for example approving in PayPal. Let them finish, then complete the cart again.
409 with payment_failed The payment was declined. Let the shopper pay again.
409 with payment_processing The payment is still clearing. We place the order as soon as it clears.
409 with cart_completing Another request is placing this order. Complete the cart again in a moment to get the order.
503 with completion_pending The payment went through and we are placing the order. Try again in a minute.
503 with payment_provider_unavailable The payment provider did not answer. Try again in a moment.
401 with sign_in_required The store takes no guest orders. Sign the shopper in.
422 with terms_not_accepted Show the terms at terms_url, then send accept_terms: true.

A cart gets 20 payment sessions and 20 completion attempts in 10 minutes, and an IP address gets 60 of each. Beyond that, the answer is 429 with rate_limited and a Retry-After header.

GET /store/orders/{order_id} answers an order to its signed-in shopper, or to anyone who sends its token: the access_token from completion, which every order email carries. A token works for 90 days after the order was placed, or for the store's order_access_days.

Shell
curl "https://api.coritan.com/api/v1/orgs/acme/store/orders/order_01j8z3k4m5n6p7q8r9s0t1v2w3?token=$ORDER_TOKEN" \
  -H "x-publishable-api-key: $PUBLISHABLE_KEY"

The order has its items, totals, shipping_methods and refunds, the fulfillments with their tracking, its documents, a timeline of what the shopper may see, such as shipments and the notes you share, and sold_by, the Coritan company that sold it.

  • GET /store/orders/{order_id}/documents answers the invoice and any credit notes as data, with the issuer, the buyer, the lines and the tax, for your storefront to show.
  • GET /store/customers/me/orders lists a signed-in shopper's orders in the key's mode, newest first: 20 a page by default, and up to 100.
  • POST /store/orders/lookup with an email, and optionally an order_number such as #1001, emails the links to up to 10 orders placed with that address while their tokens still work. The answer is always 202 with sent: true, so it never shows whether an address has ordered. An IP address can look up 10 times an hour, and each email address 3 times.

Let the shopper ask for a return

Section titled Let the shopper ask for a return

A shopper can ask to send back units that have shipped, or to exchange them for other variants. The request waits as requested for you to approve or decline it with the Commerce API, as Handle returns, exchanges and claims explains.

GET /store/return-reasons lists the reasons the shopper can pick, ordered by the rank you give them, each with its code, label and description. It leaves out the reasons you disabled.

The return routes of an order answer its signed-in shopper, or anyone who sends its token, as GET /store/orders/{order_id} does. Send the request with the items that go back:

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/store/orders/order_01j8z3k4m5n6p7q8r9s0t1v2w3/returns?token=$ORDER_TOKEN" \
  -H "x-publishable-api-key: $PUBLISHABLE_KEY" \
  -H "Idempotency-Key: 2c7e9b14-8f3a-4d6b-a1e5-7b0d3c9f6a28" \
  -H "Content-Type: application/json" \
  -d '{"items": [{"order_item_id": 5521, "quantity": 1, "reason_code": "too_small"}], "note": "The shoes are half a size too small."}'
Field Takes
items Up to 100 of the order's items, each with its id as order_item_id and the quantity that goes back. Each can add a reason_code from the store's reasons and a note of up to 500 characters.
exchange_items For an exchange, up to 50 variants the shopper wants instead, each a variant_id and a quantity.
note A note of up to 2,000 characters.

The answer is 201 with the return, which is requested. Its kind is exchange when it names new items, and return otherwise. refund_amount is what we expect to refund. It can fall when you approve the return with a fee, or when fewer units come back.

  • The shopper can ask for units that have shipped and are not already returned, refunded or in another return. Gift cards never come back.
  • The request must come within the store's returns window. The window is 30 days unless you set the store's returns_window_days, and 0 turns shopper requests off. It counts from when the item was delivered, or shipped when no delivery is recorded. When the item arrived in more than one shipment, the latest one counts.
  • We price the new items in the order's currency and region, for the shopper's customer groups, and tax them at the order's address. They must be for sale in the order's sales channel and in stock. They can cost no more than the units that go back are worth, because the Store API takes no payment for a difference.
  • Send an Idempotency-Key header, as when you complete a cart. A retry with the same key for the same order, within 24 hours, answers what the first request answered.
  • GET /store/orders/{order_id}/returns lists every return on the order, oldest first, including the ones you open yourself. Show each one's status, the instructions and label_url you add when you open or approve it, and the rejection_reason when you decline it.
  • Once we create the order that sends an exchange's new items, the return's exchange_order_id names it, and exchange_order_access_token is its token for GET /store/orders/{order_id}.
  • We email the shopper when a return is refunded, as for any refund. The other steps of a return send no email, so show them on your order page. The order's timeline shows them to the shopper too.
Answer What to do
409 with item_not_returnable Ask for no more than returnable_quantity units of the item in order_item_id.
409 with return_window_closed The window closed at closed_at. You can still open the return yourself with the Commerce API.
409 with exchange_payment_required The new items cost difference_amount more than what goes back. Let the shopper pick items that cost less, or ask for a refund. You can also open the exchange yourself and waive the difference.
409 with insufficient_inventory A new item has less stock than the shopper asks for, and available gives the units left.
409 with order_canceled or exchange_order The order was cancelled, or it sends the new items of an exchange. Neither takes a return, so show the shopper the store's support_email.
422 with invalid on items or exchange_items An order_item_id is not one of the order's items, a reason_code is not one of the store's reasons, or a new item is a gift card or is not for sale in the order's sales channel.
429 with rate_limited The order had 10 return requests in the last hour. Retry-After says when to try again.

These routes need the shopper's token, and reach only their own records:

  • GET /store/customers/me answers the shopper's email, name, phone, accepts_marketing and vat_id. PATCH changes first_name, last_name, phone, accepts_marketing and vat_id. Only the shopper can turn marketing on.
  • GET /store/customers/me/addresses lists their saved addresses, up to 50. POST adds one, which needs address_1, city and country_code. The first address becomes the default for shipping and billing, and is_default_shipping and is_default_billing move the defaults. PATCH and DELETE on /store/customers/me/addresses/{address_id} change and delete one.

An error answers with a status and a detail object that holds a machine-readable error code and a message you can show the shopper. Some errors add fields, such as blockers or available.

JSON
{"detail": {"error": "cart_not_ready", "message": "The cart cannot be completed yet.", "blockers": ["shipping_method_required"]}}
Status error Meaning
401 publishable_key_required, invalid_publishable_key The key is missing, revoked or for another store.
403 store_not_live A live key, while the store is still in test mode.
403 cart_forbidden The cart belongs to another shopper.
404 commerce_not_enabled The organization has no store, or Coritan disabled it.
404 not_found The thing does not exist in this store or this mode, or the order is not the shopper's.
409 cart_not_open The cart already has its order, or was abandoned.
422 invalid or a specific code The input is wrong; field names it when it can.
429 rate_limited Too many attempts. Retry-After says when to try again.
503 store_unavailable Coritan suspended the store, so it takes no orders.

With a test key, your storefront lists the catalogue, fills a cart, takes a test payment and places a test order. The order appears in the Commerce API's GET /commerce/orders?livemode=false, and the shopper receives a confirmation whose subject starts with [TEST]. Once the store is live, the same code with a live key takes real orders.

401 with invalid_publishable_key
The key was revoked or mistyped, or belongs to another organization. List the keys with GET /commerce/publishable-keys, or create one.
403 with store_not_live
The store is still in test mode. Use a test key until we switch the store to live.
The browser reports a CORS error
The store lists allowed_origins, and your site is not among them. Add its origin with PATCH /commerce/store. The change applies within 30 seconds.
422 with country_not_served or country_not_in_region
The store has no region for that country, or the address is outside the cart's region. Move the cart with PATCH /store/carts/{cart_id} and a country_code, or add the country to a region.
409 with price_unavailable
The variant has no price in the cart's currency. Give it one for that currency or region with the Commerce API.
409 with cart_not_ready
Something in blockers stops checkout. Check the cart before payment says what each one needs.
409 with payment_already_authorized
The cart is already paid for. Complete it.
409 with cart_not_open
The cart is completed or abandoned. Completing a completed cart again answers its order; otherwise create a new cart.
422 with invalid on return_url or cancel_url
PayPal's return addresses must be on your storefront. Set the store's storefront_url, or add your site's origin to allowed_origins.
An order link answers 404
The token is wrong, or the order is older than the store's order_access_days. A signed-in shopper can still read their own orders.

API operations on this page

MethodPathWhat it does
GET/api/v1/orgs/{org_slug}/storeStore info
GET/api/v1/orgs/{org_slug}/store/return-reasonsList return reasons
POST/api/v1/orgs/{org_slug}/store/cartsA cart in the key's mode and sales channel, in a region and its currency
GET/api/v1/orgs/{org_slug}/store/carts/{cart_id}Get cart
PATCH/api/v1/orgs/{org_slug}/store/carts/{cart_id}Email, addresses, region or country (re-prices every line), note, locale, metadata
POST/api/v1/orgs/{org_slug}/store/carts/{cart_id}/completePlace the cart's order: {"order", "accesstoken"}
POST/api/v1/orgs/{org_slug}/store/carts/{cart_id}/gift-cardsApply a gift card by its code: 10 attempts per 10 minutes per cart and per client IP
DELETE/api/v1/orgs/{org_slug}/store/carts/{cart_id}/gift-cards/{gift_card_id}Remove gift card
POST/api/v1/orgs/{org_slug}/store/carts/{cart_id}/line-itemsAdd a variant; the same variant with the same metadata merges into its line
PATCH/api/v1/orgs/{org_slug}/store/carts/{cart_id}/line-items/{line_id}Set a line's quantity; 0 removes it
DELETE/api/v1/orgs/{org_slug}/store/carts/{cart_id}/line-items/{line_id}Delete line item
GET/api/v1/orgs/{org_slug}/store/carts/{cart_id}/payment-providersThe providers that can take this cart's payment, and whether it needs one
POST/api/v1/orgs/{org_slug}/store/carts/{cart_id}/payment-sessionsStart the payment with provider, or refresh the one already started for the same amount
POST/api/v1/orgs/{org_slug}/store/carts/{cart_id}/promotionsApply a code
DELETE/api/v1/orgs/{org_slug}/store/carts/{cart_id}/promotionsRemove a code, named in the body {"code"} or as ?code=
POST/api/v1/orgs/{org_slug}/store/carts/{cart_id}/shipping-methodsChoose an option; it replaces the method its shipping profile had
GET/api/v1/orgs/{org_slug}/store/carts/{cart_id}/shipping-optionsThe options the cart's address and items allow, priced for them, per shipping profile
GET/api/v1/orgs/{org_slug}/store/categoriesThe active, public categories as a tree
GET/api/v1/orgs/{org_slug}/store/categories/{handle}Get category
GET/api/v1/orgs/{org_slug}/store/collectionsList collections
GET/api/v1/orgs/{org_slug}/store/collections/{handle}A published collection; its products are /store/products?collectionhandle=
GET/api/v1/orgs/{org_slug}/store/customers/meGet me
PATCH/api/v1/orgs/{org_slug}/store/customers/meName, phone, marketing consent (the shopper alone turns it on) and VAT id
GET/api/v1/orgs/{org_slug}/store/customers/me/addressesList my addresses
POST/api/v1/orgs/{org_slug}/store/customers/me/addressesAdd my address
PATCH/api/v1/orgs/{org_slug}/store/customers/me/addresses/{address_id}Update my address
DELETE/api/v1/orgs/{org_slug}/store/customers/me/addresses/{address_id}Delete my address
GET/api/v1/orgs/{org_slug}/store/customers/me/ordersThe shopper's orders in the key's mode, newest first
POST/api/v1/orgs/{org_slug}/store/orders/lookupLook up orders
GET/api/v1/orgs/{org_slug}/store/orders/{order_id}Get order
GET/api/v1/orgs/{org_slug}/store/orders/{order_id}/documentsThe invoice and credit notes, as issued by the seller of record
GET/api/v1/orgs/{org_slug}/store/orders/{order_id}/returnsThe order's returns, oldest first
POST/api/v1/orgs/{org_slug}/store/orders/{order_id}/returnsRequest return
GET/api/v1/orgs/{org_slug}/store/productsPublished products in the key's sales channels
GET/api/v1/orgs/{org_slug}/store/products/{id_or_handle}One published product by handle (or by id), priced like the list
GET/api/v1/orgs/{org_slug}/store/regionsThe regions the store sells in, to pick a country and currency from
GET/api/v1/orgs/{org_slug}/store/regions/{region_id}Get region