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.
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.
Before you begin
Section titled Before you begin- 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 tolive. - Set the store's
storefront_url, because the order emails link to pages on it, and list your storefront's address inallowed_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_checkoutsetting.
Send the publishable key
Section titled Send the publishable keySend 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.
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_idto 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.
Show the catalogue
Section titled Show the catalogueGET /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 |
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 theamountthe shopper pays, theoriginal_amountbefore any price list, the merchant'scompare_at_amount, andis_discounted. It isnullwhen the variant has no price there.purchasable, which istruewhen the variant has a price and can be ordered.availability:in_stock,low_stock(5 or fewer left),backorder,out_of_stock, orunlimitedfor 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/collectionslists the published collections, andGET /store/collections/{handle}answers one. Its products come fromGET /store/products?collection_handle=....GET /store/categoriesanswers the categories as a tree, andGET /store/categories/{handle}answers one with itschildrenandparent. A category that is inactive or internal is hidden, with everything under it.GET /store/regionslists the regions with theircountries,currency_code,tax_inclusiveandpayment_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.
Fill a cart
Section titled Fill a cartCreate the cart when the shopper adds their first item. Send their country, and the cart takes the region that sells there, with its currency:
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_codeorregion_id, the cart takes the store's default region. A country the store does not sell to answers422withcountry_not_served. POST /store/carts/{cart_id}/line-itemsadds avariant_idwith aquantity. The same variant with the samemetadatajoins 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'squantity, and0removes the line.DELETEon the same path removes it too.PATCH /store/carts/{cart_id}sets theemail, theshipping_addressandbilling_address, anoteof up to 2,000 characters, thelocaleandmetadata. A newcountry_codeorregion_idmoves 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
409withinsufficient_inventory, andavailablegives 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.
Apply codes and gift cards
Section titled Apply codes and gift cardsPOST /store/carts/{cart_id}/promotionswith acodeapplies a discount code. A code that does not apply answers422withpromotion_not_applicable, areasonsuch asexpired,min_subtotalorfirst_order_only, and amessageyou can show. A cart holds up to 5 codes. Remove one withDELETE /store/carts/{cart_id}/promotions?code=WELCOME10. Automatic promotions apply without a code.POST /store/carts/{cart_id}/gift-cardswith acodeapplies a gift card, up to 5 a cart. A wrong code answers422withgift_card_invalid, and a card that cannot pay answers422withgift_card_not_applicableand areason:disabled,expired,exhaustedorcurrency_mismatch. The cart shows each card'slast4and the amount itapplied.
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.
Choose shipping
Section titled Choose shippingOnce 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.
Check the cart before payment
Section titled Check the cart before paymentThe 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.
Take payment
Section titled Take paymentList what can take the payment with
GET /store/carts/{cart_id}/payment-providers. Each provider has anid, adisplay_nameand theconfigits browser script needs. The answer also has thetotalandpayment_required, which isfalsewhen gift cards pay for everything: then go straight to Complete the order.Start a payment session with the provider the shopper picks. Send
accept_terms: trueonce the shopper has accepted your terms.Shellcurl -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}'Let the shopper pay in the browser with the
payment_sessionfrom the answer:stripe: mount Stripe's Payment Element withconfig.publishable_keyand the session'sclient_secret, and confirm the payment with Stripe.js.paypal: send the shopper to the session'sapprove_url, or show PayPal's buttons withconfig.client_idand thepaypal_order_id. PayPal brings the shopper back to thereturn_urlyou sent, or to thecancel_urlwhen they stop. Both must be on your storefront's origin or inallowed_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.
Complete the order
Section titled Complete the orderPOST /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.
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
409withidempotency_key_reused, and a retry while the first request still runs answers409withidempotency_in_progress. - We price the cart again at this step. When its total no longer matches the payment, the answer is
409withpayment_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
409with the reason andpayment_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.
Show the shopper's orders
Section titled Show the shopper's ordersGET /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.
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}/documentsanswers 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/orderslists 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/lookupwith anemail, and optionally anorder_numbersuch as#1001, emails the links to up to 10 orders placed with that address while their tokens still work. The answer is always202withsent: 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 returnA 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:
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, and0turns 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-Keyheader, 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}/returnslists every return on the order, oldest first, including the ones you open yourself. Show each one'sstatus, theinstructionsandlabel_urlyou add when you open or approve it, and therejection_reasonwhen you decline it.- Once we create the order that sends an exchange's new items, the return's
exchange_order_idnames it, andexchange_order_access_tokenis itstokenforGET /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
timelineshows 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. |
Manage the shopper's account
Section titled Manage the shopper's accountThese routes need the shopper's token, and reach only their own records:
GET /store/customers/meanswers the shopper'semail, name,phone,accepts_marketingandvat_id.PATCHchangesfirst_name,last_name,phone,accepts_marketingandvat_id. Only the shopper can turn marketing on.GET /store/customers/me/addresseslists their saved addresses, up to 50.POSTadds one, which needsaddress_1,cityandcountry_code. The first address becomes the default for shipping and billing, andis_default_shippingandis_default_billingmove the defaults.PATCHandDELETEon/store/customers/me/addresses/{address_id}change and delete one.
Errors
Section titled ErrorsAn 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.
{"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. |
Result
Section titled ResultWith 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.
Troubleshooting
Section titled Troubleshooting401withinvalid_publishable_key- The key was revoked or mistyped, or belongs to another organization. List the keys with
GET /commerce/publishable-keys, or create one. 403withstore_not_live- The store is still in
testmode. Use a test key until we switch the store tolive. - The browser reports a CORS error
- The store lists
allowed_origins, and your site is not among them. Add its origin withPATCH /commerce/store. The change applies within 30 seconds. 422withcountry_not_servedorcountry_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 acountry_code, or add the country to a region. 409withprice_unavailable- The variant has no price in the cart's currency. Give it one for that currency or region with the Commerce API.
409withcart_not_ready- Something in
blockersstops checkout. Check the cart before payment says what each one needs. 409withpayment_already_authorized- The cart is already paid for. Complete it.
409withcart_not_open- The cart is completed or abandoned. Completing a completed cart again answers its order; otherwise create a new cart.
422withinvalidonreturn_urlorcancel_url- PayPal's return addresses must be on your storefront. Set the store's
storefront_url, or add your site's origin toallowed_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.