Skip to content
Coritan Docs

Sell with the Commerce API

Run a store with Coritan as the merchant of record: products, regions, prices, orders, refunds and fulfilment from your systems.

View as Markdown

The Commerce API is the back office of your organization's store for goods. Your own systems use it to run the catalogue, prices, stock, shipping, promotions, orders, refunds and fulfilment. Shoppers buy through the Store API, which your storefront calls with a publishable key.

Coritan is the store's merchant of record. A Coritan company is the seller on every order: it takes the payment, issues the invoice and credit notes in its own name and accounts for the tax. Your balance records the part of each sale that is yours. The one exception is an order paid through a payment page, which your MAEF parent site takes and your business sells itself.

Every route is under https://api.coritan.com/api/v1/orgs/{org_slug}/commerce/. The API reference lists them under Commerce, with the product routes under Catalog & Services and the customer routes under Customers. This page covers the main tasks, and the reference has every field. The organization's Commerce tab does the same tasks in the dashboard (Run your store from the Commerce tab).

  • Ask support to turn commerce on for your organization. We create the store in test mode, with one sales channel called Web store. Until then, every route answers 404 with commerce_not_enabled.
  • Get a credential: an organization API key with the scopes your system needs, or a member's access token. Authenticate explains both.
  • Have your company's legal details and its owners' names ready for the merchant profile. The store takes real orders only after we approve it.

Send one of these with every request:

  • An organization API key in the X-API-Key header, for your own systems. Create it in the dashboard or through the API with the scopes it needs, as Create organization API keys shows. A key with no scopes reaches nothing, so every route here refuses it.
  • A member's token in Authorization: Bearer, for a person: their coritan.com access token, or the token of their staff console session. Their role decides what the token can do.
Shell
curl "https://api.coritan.com/api/v1/orgs/acme/commerce/store" \
  -H "X-API-Key: $ORG_API_KEY"

When a request carries X-API-Key, we read the key and ignore any Authorization header. A key is never asked for a step-up. A person's token is, to refund, cancel or delete an order, as Refund or cancel an order describes.

Each route needs one scope from a key, or a role from a member. A :write scope includes the :read scope of its area. commerce:* grants every scope, email marketing's included, and an area with :*, such as commerce.orders:*, grants every scope in that area. The email marketing scopes start with marketing. and have their own routes under /marketing, which Set up email marketing for your store covers. marketing:* grants every one of them.

Scope What it allows Lowest role
commerce.store:read Reading the store, its publishable keys and its event log. An event shows an email address only to a caller who can also read the customer or order it came from (Receive commerce webhooks). Read only
commerce.store:write Changing the store's settings, publishable keys, sales channels, regions, shipping profiles, zones and options, and fulfilment providers. Admin
commerce.catalog:read Reading products, tags, collections, categories, price lists, regions, shipping, stock locations, inventory items, sales channels, the store's media and imports. Read only
commerce.catalog:write Changing products with their options, variants, prices and images; collections, categories and price lists; stock locations, inventory items and the locations each sales channel sells from; importing from Shopify. Admin
commerce.media:write Uploading the store's images and PDFs, changing their alt text, deleting them and importing a WordPress media library. Listing them takes commerce.catalog:read. Admin
commerce.inventory:write Setting stock levels. Tier 3 support
commerce.customers:read Reading customers, customer groups and gift cards. Tier 1 support
commerce.customers:write Changing customers and customer groups. Tier 3 support
commerce.orders:read Reading orders with their documents, fulfilments and returns, the return reasons, and fulfilment providers with their deliveries and events. Tier 1 support
commerce.orders:write Changing an order and adding notes to it. Tier 2 support
commerce.orders:edit Editing the items of a placed order. Not part of commerce.orders:write. Tier 3 support
commerce.orders:delete Deleting an order from the platform entirely. Not part of commerce.orders:write. Admin
commerce.refunds:write Refunding and cancelling orders, and refunding returns. Billing
commerce.promotions:write Reading and changing promotions, and issuing, changing and adjusting gift cards. Admin
commerce.fulfillment:write Creating fulfilments, marking them dispatched, shipped, delivered or cancelled, and sending a fulfilment provider's events. Tier 3 support
commerce.returns:write Opening, approving, declining, receiving and cancelling returns, and changing the return reasons. Reading them takes commerce.orders:read. Tier 2 support
commerce.finance:read Reading the balance, the ledger, disputes, payouts and tax reports, and the payment routing stats. Billing
commerce.disputes:write Changing a dispute's evidence, sending it and accepting a dispute. Billing
commerce.payments:read Reading the store's payment pages and what they reported, and the payment routing stats. Billing
commerce.payments:write Adding, changing, testing and deleting payment pages. Admin
commerce.checkout:read Reading hosted checkout pages, what their payment site sent back and the domains that serve them, and drawing a preview of a style. Billing
commerce.checkout:write Creating and cancelling hosted checkout pages, and adding, verifying and removing their domains. Admin
commerce.storefront:forward No Merchant API route takes it. It lets a store's own server pass its visitors' addresses and browsers on the Store API, sign-in, chat and contact routes (Server-side storefronts: pass the visitor's address). A key with only this scope reaches nothing else. None: keys only
marketing.contacts:read Reading the email marketing audience: contacts, lists, custom fields, tags and segments, and exporting contacts (Email marketing). Tier 1 support
marketing.contacts:write Changing contacts, lists, fields, tags and segments, importing contacts and bringing in leads and customers. Reading imports takes this scope too. Tier 3 support
marketing.campaigns:read Reading campaigns and their reports, saved and starter designs and signup forms, and checking a campaign. Tier 1 support
marketing.campaigns:write Writing campaigns, designs and signup forms, and sending test emails. Tier 3 support
marketing.campaigns:send Scheduling and sending a campaign, pausing, resuming and cancelling it, and resending it to non-openers. Not part of marketing.campaigns:write. Admin
marketing.automations:read Reading automations, their runs and stats. Tier 1 support
marketing.automations:write Changing automations, sending a test of one, and reading the secret that signs its webhook steps. Tier 3 support
marketing.settings:read Reading the sending setup: the domain, its DNS records, the limits and any pause. Tier 3 support
marketing.settings:write Setting up the sending domain, checking its records, changing the sender, the postal address and the limits, and pausing and resuming sending. Admin

The lowest role applies to a member's token. The roles form a ladder, and each role can do what the roles below it can: Read only, then Tier 1, Tier 2 and Tier 3 support, then Billing, then Admin and Owner. Organization roles and permissions describes them. The merchant profile takes an owner's or admin's token, and no key can reach it, because it holds the identities of your company's owners. Asking for a payout also takes an owner's or admin's token, and no key can ask for one.

GET /commerce/store answers the store with its settings and its status, the merchant review's status under merchant, can_go_live, and the store's sales_channels.

Status What it means
test The status a new store starts in. It takes test orders, paid with test payments through test publishable keys.
live The store also takes real orders, through live publishable keys. It needs an approved merchant profile, and Coritan makes the switch.
suspended Shoppers cannot buy: the Store API answers 503 with store_unavailable. This API keeps working.
disabled Every route answers 404 with commerce_not_enabled, except reading the store and the merchant profile.

Test and live business stay apart. Orders, gift cards and events carry livemode, which is false for test ones, and test orders never reach your balance.

Coritan sells your goods in its own name, so we check who you are before the store goes live. The profile routes take an owner's or admin's token.

  1. Read the profile with GET /commerce/merchant-profile. The first read creates it as a draft, filled in from your organization's details. The answer has profile, missing_for_submission with what review still needs, and editable.

  2. Fill it in with PUT /commerce/merchant-profile. Send the fields you change; the others keep their values.

    Shell
    curl -X PUT "https://api.coritan.com/api/v1/orgs/acme/commerce/merchant-profile" \
      -H "Authorization: Bearer $CORITAN_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "legal_name": "Acme Goods Ltd",
        "country_code": "GB",
        "support_email": "support@example.com",
        "product_description": "Printed T-shirts and mugs.",
        "address": {"line1": "1 Example Street", "city": "London", "postal_code": "EC1A 1AA", "country_code": "GB"},
        "ship_from": {"line1": "Unit 4, Example Park", "city": "Leeds", "postal_code": "LS1 1AA", "country_code": "GB"},
        "return_address": {"line1": "Unit 4, Example Park", "city": "Leeds", "postal_code": "LS1 1AA", "country_code": "GB"},
        "beneficial_owners": [{"name": "Alex Example", "date_of_birth": "1985-04-12", "country_code": "GB", "ownership_percent": 100}]
      }'
    
  3. Send it to review with POST /commerce/merchant-profile/submit. When something is missing, the answer is 422 with the missing fields in missing.

Review needs legal_name, country_code, support_email, product_description, at least one entry in beneficial_owners, and three addresses: address for the company, ship_from and return_address, each with at least line1, city and country_code. An address also takes company, first_name, last_name, line2, province, postal_code and phone. Each owner takes a name, a date_of_birth, a country_code and an ownership_percent from 0 to 100, and we keep the first 10. The profile also takes trading_name, legal_form, registration_number, tax_id, website, support_phone, up to 20 product_categories, expected_monthly_volume and volume_currency.

proposed_statement_descriptor is the name you want on shoppers' card statements: 5 to 22 letters, digits, spaces, dots or hyphens, or 422 naming the field. It is a proposal. Payments send terms.statement_descriptor, which we set when we approve the profile: your proposal unless we choose another. Approval then sets proposed_statement_descriptor back to null. Proposing the descriptor already in force leaves it null too.

Status What it means
draft You are filling it in. Changing a submitted profile brings it back here, and so does Coritan reopening a rejected or suspended one.
submitted Waiting for review.
approved Coritan can switch the store to live.
restricted Review stopped short of approval. Read review_notes, change the profile and submit it again.
rejected Review turned it down. You can change it and submit it again.
suspended Coritan suspended the merchant, which also suspends a live store.

An approved or suspended profile is locked: changing it answers 409 with profile_locked, and support makes any change. After review, the profile's terms show the reserve and the payout delay Coritan set, and review_notes hold what the reviewer wrote. terms.platform_cut shows the platform cut, the same for every organization: its tiers (each a percent and the monthly volume it applies below, in currency; the last has no below), this month's volume so far, the rate_percent it puts the month at, and exempt for an organization Coritan runs. There is no fee to set.

A shopper can check out once the store has a region for where they live, a stock location, shipping, products with prices and a publishable key for your storefront. Money is always a whole number in the currency's smallest unit: 2500 in EUR is €25.00.

PATCH /commerce/store changes the store's name, default_currency and supported_currencies, default_locale and supported_locales (language tags such as en-GB), allowed_origins, storefront_url, support_email, and the keys under settings.

Shell
curl -X PATCH "https://api.coritan.com/api/v1/orgs/acme/commerce/store" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"default_currency": "EUR", "allowed_origins": ["https://shop.example.com"], "storefront_url": "https://shop.example.com", "settings": {"terms_url": "https://shop.example.com/terms", "terms_version": "2026-09"}}'

allowed_origins lists the sites whose browsers may call the Store API, each as a scheme and host, up to 20. The default currency is always among the supported ones.

Setting What it does
guest_checkout true by default. false makes shoppers sign in before they pay.
order_access_days For how many days after an order is placed its access token still opens it. Defaults to 90.
unpaid_minutes How long a shopper has to finish a payment, from 15 to 1440 minutes, counted from when it started or from when the shopper came back to it. Defaults to 60. Then we cancel the payment, and the checkout is unpaid.
completed_when shipped, the default, or delivered: when an order's display_status becomes completed.
terms_url, terms_version When terms_url is set, shoppers must accept your terms to pay, and each order records the version they accepted (up to 40 characters).
privacy_url Your privacy policy, which the Store API passes to your storefront.
auto_dispatch false stops us sending fulfilments to any provider on our own.
notify_shipped true unless you set it to false. false stops the shipping email the shopper gets for each fulfilment that ships.
notify_delivered false unless you set it to true. true sends the shopper one delivery email for each fulfilment that is delivered, however it was marked, and a fulfilment that was never shipped through us then sends the delivery email and no shipping email. Turn the delivery email on or off describes it.
notify_new_referral false unless you set it to true. true emails an affiliate once for each referral that earns them commission, when the referral becomes unpaid. It is a setting of its own and not a key of settings.affiliates, so saving the program leaves it alone. Turn the affiliate commission email on or off describes it.
release_delay_minutes The least time every fulfilment provider waits after an order is placed, up to 10,080 (a week).
risk_high_value The amount above which a first order counts as high value, in minor units. Defaults to 50000.
returns_window_days For how many days after an item is delivered a shopper can ask to return or exchange it, counted from when it shipped if no delivery is recorded. Defaults to 30, and 0 stops shoppers from asking.
refund_policy, shipping_policy Your refund and shipping policies as text, up to 5,000 characters each. Dispute evidence quotes them.
payout_schedule How often we create payouts: daily, weekly or monthly. Defaults to weekly.
payment_routing Gives a share of new carts other ways to pay, as Route carts between ways to pay describes. A new value replaces the old one whole.

Setting a key to null removes it, which brings back its default. A key the store does not know answers 422.

A region is a group of countries that share a currency, tax handling and payment methods. A cart belongs to one region and is priced in its currency.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/regions" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Europe", "currency_code": "EUR", "countries": ["DE", "FR", "NL"], "payment_providers": ["stripe", "paypal"]}'
  • payment_providers says whether shoppers in the region pay with stripe, paypal or both, and defaults to ["stripe"]. PayPal takes only currencies with two decimal places. A test cart can also pay with manual, a test payment that moves no money. Once our staff turn payment pages on for the store, external offers a payment page, which hands the cart to your MAEF parent site.
  • Prices exclude tax unless you set tax_inclusive to true. We work out the tax on each cart from Coritan's own rates for the shopper's address, and charge none where Coritan does not collect tax. The region's automatic_taxes field changes nothing.
  • A country belongs to one region at most, and only where Coritan sells. A country we do not sell into yet answers 409 with market_not_enabled.
  • The first enabled region becomes the store's default. Send is_default: true to move the default. The default region cannot be disabled or deleted.
  • A region's currency joins the store's supported currencies. Deleting a region also deletes the prices set for it.

A store has up to 50 regions.

A stock location is a place that holds your goods, such as a warehouse. Stock is counted per location.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/stock-locations" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Leeds warehouse", "address": {"line1": "Unit 4, Example Park", "city": "Leeds", "postal_code": "LS1 1AA", "country_code": "GB"}, "is_default": true}'

Set fulfillment_provider_id on a location that a fulfilment provider runs. A store has up to 100 locations. A sales channel sells from every enabled location until you choose some with PUT /commerce/sales-channels/{channel_id}/stock-locations.

Shipping has three parts:

  • A shipping profile groups products that ship the same way. The store's default profile covers every product that names no other, and you cannot delete it. Add a custom or gift_card profile with POST /commerce/shipping-profiles, up to 50.
  • A shipping zone is where you ship: a list of up to 300 places in geo, each a country_code, optionally narrowed by province_code and postal_prefix. A store has up to 200 zones.
  • A shipping option is what the shopper chooses, with its price, for one zone and one profile. A store has up to 500 options.
Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/shipping-zones" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Western Europe", "geo": [{"country_code": "DE"}, {"country_code": "FR"}, {"country_code": "NL"}]}'

curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/shipping-options" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"zone_id": 12, "name": "Standard", "currency_code": "EUR", "price_type": "free_over", "amount": 490, "free_over_amount": 5000, "delivery_min_days": 2, "delivery_max_days": 4}'

An option without profile_id belongs to the default profile. Its price_type is one of these, worked out from the items of its profile after discounts:

price_type Price
flat amount.
free_over amount, or nothing once the subtotal reaches free_over_amount.
weight_tiers The first of tiers whose up_to, in grams, covers the weight.
subtotal_tiers The first of tiers whose up_to, in minor units, covers the subtotal.

tiers holds up to 50 {up_to, amount} entries in ascending order, and only the last may leave up_to as null. An option no tier covers is not offered. min_subtotal and max_subtotal limit the subtotals an option is offered for, and provider_id and provider_code tell a fulfilment provider which service to use. At checkout, the shopper picks one option for each profile among the items that ship, from a zone that matches the address and in the cart's currency.

Create publishable keys and sales channels

Section titled Create publishable keys and sales channels

Your storefront sends a publishable key to the Store API. It names the store and is safe to put in a browser.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/publishable-keys" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label": "Web shop", "livemode": false}'

The answer is 201 with the key, whose token starts with pk_test_, or with pk_live_ when livemode is true. A live key works once the store is live. GET /commerce/publishable-keys shows every key with its token, and DELETE /commerce/publishable-keys/{key_id} revokes one; a revoked key can keep working for up to 15 seconds.

A sales channel is a place you sell, such as your website or a marketplace. A key sells the products of the channels in its sales_channel_ids, or of the default channel when it names none. A store has up to 50 channels. The default channel cannot be disabled or deleted, and PATCH /commerce/sales-channels/{channel_id} with is_default: true moves the default.

A product has options, such as size and colour, and variants, one for each combination you sell. Each variant has its own SKU, prices and stock.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/products" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Logo T-shirt",
    "handle": "logo-t-shirt",
    "status": "published",
    "options": [{"title": "Size", "values": ["S", "M", "L"]}],
    "variants": [
      {"title": "S", "sku": "TSHIRT-S", "options": {"Size": "S"}, "weight_g": 180, "prices": [{"currency_code": "EUR", "amount": 2500}]},
      {"title": "M", "sku": "TSHIRT-M", "options": {"Size": "M"}, "weight_g": 190, "prices": [{"currency_code": "EUR", "amount": 2500}]},
      {"title": "L", "sku": "TSHIRT-L", "options": {"Size": "L"}, "weight_g": 200, "prices": [{"currency_code": "EUR", "amount": 2500}]}
    ],
    "tags": ["clothing"]
  }'

The answer is 201 with the whole product. Keep these rules in mind:

  • A product is a draft until you set status to published, and the Store API shows published products only. archived hides it.
  • It sells in the store's default channel unless you send sales_channel_ids.
  • A handle or a SKU belongs to one product. A taken handle answers 409 with handle_taken, and a taken SKU 409 with sku_taken. Without a handle, we make one from the title.
  • A variant tracks stock unless you set manage_inventory to false, and it cannot be ordered beyond its stock unless allow_backorder is true. A tracked variant with no stock set cannot be bought unless it allows backorders.
  • A price can be narrowed to a region_id, or to a quantity with min_quantity and max_quantity, and can carry a compare_at_amount to show as the old price.
  • A product has up to 10 options, 250 values an option, 500 variants, 250 tags and 250 images.

PATCH /commerce/products/{product_id} changes the fields you send. When you send options, variants, sales_channel_ids, collection_ids, category_ids or tags, the list replaces the product's: name each variant you keep by its id or sku, or we delete it. The options, variants and images also have routes of their own under the product. DELETE /commerce/products/{product_id} archives the product: orders keep its variants, and its handle and SKUs are free for new products.

GET /commerce/products lists products, most recently changed first, 50 a page and at most 200. Send fields to answer only some keys of each product, comma-separated, such as title,variants. id always comes too, and we load only what the keys name, so a list that leaves out variants, images and the other lists comes back sooner. Leaving out variants also skips prices and stock. A key a product does not have answers 422 with invalid_fields, and allowed lists the keys.

An image links to a picture on https: POST /commerce/products/{product_id}/images takes its url, or in its place the media_id of an image in the store's media. It also takes alt text, and optionally a variant_id, a rank and the image's width and height. An image from the store's media takes the file's width, height and alt text unless you send them, and a body with both url and media_id answers 422.

  • A collection is a set of products your storefront shows together. A manual collection holds the products you set with PUT /commerce/collections/{collection_id}/products. A smart collection holds every product that matches its rules, as {"match": "all", "conditions": [{"field": "tag", "op": "equals", "value": "clothing"}]}, where field is tag, product_type, vendor or title, op is equals or contains, and match is all or any.
  • A category sits in a tree through parent_id. The Store API hides a category, and everything under it, while its is_active is false or its is_internal is true.
  • Tags are words on the product. GET /commerce/product-tags lists the ones in use, most used first.

A price list changes what variants cost for a while or for some customers. POST /commerce/price-lists takes a title, a kind, a status of draft or active, optionally starts_at, ends_at and customer_group_ids, and prices that each name a variant_id.

  • In a sale list, the shopper pays the lowest of the variant's own price and every active sale list they qualify for.
  • An override list they qualify for replaces all of those.

The Store API shows what the price would be without any list as original_amount, which your storefront can strike through. PUT /commerce/price-lists/{price_list_id}/prices replaces a list's prices, up to 5,000.

POST /commerce/imports takes the CSV that Shopify's admin exports and queues it for import. Send the file as the multipart field file, as a text/csv body, or as JSON {"csv": "...", "filename": "..."}. Try it first with dry_run=true: a dry run goes through the same steps and saves nothing but its report. The same route imports customers, promotions and past orders from another platform as NDJSON files, with kind set to customers, promotions or orders; Imports gives each file's fields.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/imports?dry_run=true&currency_code=EUR" \
  -H "X-API-Key: $ORG_API_KEY" \
  -F "file=@products_export.csv"
  • mode=upsert, the default, updates the products whose handle the store already has, and mode=create_only skips them.
  • Prices are read in currency_code, which defaults to the store's default currency and must be one the store sells in.
  • Variant Inventory Qty becomes the stock at location_id, which defaults to the store's default location.
  • Shopify's active, draft and archived become published, draft and archived. A new product without a status is a draft.
  • A blank cell keeps the store's value. A blank Variant Inventory Tracker turns stock tracking off for the variant, as in Shopify.

A file is at most 10 MB and 5,000 rows, and must be UTF-8. We read it before we queue it, and a file we cannot import at all, such as one without a Handle column, answers 422 with the reason. Otherwise the answer is 202 with the queued import, and we import the products in batches of 50. A store runs one import at a time: while one is queued or running, another answers 409 with import_in_progress and that import's import_id. The route's earlier path, POST /commerce/imports/shopify-products, works the same way and also answers 202 with the queued import.

Follow the import with GET /commerce/imports/{import_id}:

  • status starts as queued, moves to running, and ends as completed or failed. A failed import says why in error.
  • progress counts the products_processed out of products_total.
  • report counts what changed: products_created, products_updated, products_skipped, variants_created, variants_updated, images_added, levels_set and categories_created. Its errors list up to 500 rows the import could not use, each with its spreadsheet row, its handle and a message, and error_count counts them all. The rest of the file still imports.

When an import fails part way, the products in the batches it finished stay imported, so send the file again to import the rest. GET /commerce/imports lists the store's imports newest first, without their errors: 20 a page by default and up to 100, narrowed by status.

Each variant that tracks stock has an inventory item, with a level at each location. A level has a stocked_quantity (on the shelf), a reserved_quantity (held for orders that have not shipped), an incoming_quantity, and an available_quantity, which is stocked less reserved. Shipping takes units off both stocked and reserved, and cancelling an order gives its reserved units back.

Set one item's level at one location with stocked_quantity, or change it by a delta:

Shell
curl -X PUT "https://api.coritan.com/api/v1/orgs/acme/commerce/inventory-items/301/levels/7" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"delta": -2}'

Set many at one location by SKU, up to 1,000 a request:

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/inventory/levels/batch" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"location_id": 7, "levels": [{"sku": "TSHIRT-S", "stocked_quantity": 40}, {"sku": "TSHIRT-M", "stocked_quantity": 55}]}'

The answer lists the levels it set and any unknown_skus. A SKU sent twice takes its last count, and stock never goes below zero. GET /commerce/inventory-items?low_stock=5 finds the items with 5 or fewer available.

Offer promotions and gift cards

Section titled Offer promotions and gift cards
Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/promotions" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code": "WELCOME10", "status": "active", "value_type": "percentage", "percentage": 10, "target": "order", "conditions": {"first_order_only": true}, "per_customer_limit": 1}'
  • value_type is percentage, fixed (an amount with its currency_code) or free_shipping, and target is order, items or shipping.
  • conditions can ask for a min_subtotal in the promotion's currency_code, items from product_ids or collection_ids, a shopper in one of customer_group_ids, or first_order_only.
  • conditions.include_shipping and conditions.free_shipping take shipping off as well, so one promotion can give 20% off and free shipping. Both are booleans, and we store each only when it is true.
    • include_shipping is for a percentage promotion whose target is order. The same percentage comes off the shipping that is left after the shipping promotions that applied before it.
    • free_shipping is for a percentage, fixed, buy_x_get_y or free_gift promotion whose target is order or items. The shipping that is left after the shipping promotions that applied before it costs nothing.
    • Setting both to true answers 422 with field set to conditions.free_shipping. include_shipping anywhere else answers 422 with field set to conditions.include_shipping, and free_shipping on a free_shipping promotion or a shipping target answers 422 with field set to conditions.free_shipping.
    • The promotion still applies on its own terms: an items promotion with no matching line does not apply, so it takes no shipping off. A single use code minted from it keeps both options. A PATCH that sends conditions replaces all of them, so leave an option out to turn it off.
    • The shipping discount shows on each shipping method of the cart and the order as its discount_total, and on the cart in totals.shipping_discount_total. The order's discount_total includes it. An order counts as one use, and the discount_total in the promotion's usage includes the shipping share.
  • usage_limit, per_customer_limit, starts_at and ends_at bound its use. A promotion that is not combinable applies only on its own.
  • Codes are unique in the store and match whatever the case. A new promotion is a draft until you make it active.
  • GET /commerce/promotions lists the store's promotions, newest first. q finds the codes that start with it and the descriptions that hold it. code finds the one promotion that has exactly that code, in any case, and never a longer code that only starts with it: code=summer10 finds SUMMER10 and not SUMMER100. It answers an empty list when the store has no such code. code combines with q, status, is_automatic, minted, before_id and limit, and a blank code finds nothing.
  • is_automatic: true makes a promotion without a code that applies to every cart it fits. Up to 50 can be active at once.

A promotion that an order used cannot be deleted: set its status to disabled instead.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/gift-cards" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"currency_code": "EUR", "amount": 5000, "recipient_email": "alex@example.com", "message": "Thank you for your help."}'

The answer holds the card and its code. We show the code this once and do not email it, so send it to the recipient yourself. POST /commerce/gift-cards/{gift_card_id}/adjust adds or takes away an amount with a note, and a card's balance cannot go below zero. PATCH sets a card's status to active or disabled. A card is exhausted when its balance runs out and expired after its expires_at. The gift cards that shoppers buy in your store are emailed to their recipients.

A customer here is one of your organization's customers who has ordered from the store or has a store profile. GET /commerce/customers searches them with q, and PATCH /commerce/customers/{customer_id} changes your note about them, their tags, tax_exempt, and their vat_id with vat_id_valid once you have checked it. You can set accepts_marketing to false only: shoppers opt in themselves. Setting it to false also puts the address on the store's suppression list.

A customer group collects customers for price lists and promotions. Create one with POST /commerce/customer-groups and set its members with PUT /commerce/customer-groups/{group_id}/customers, up to 1,000. Deleting a group keeps its customers.

A cart becomes an order when the shopper pays through the Store API. We email them a confirmation, and later a notice if you cancel or refund the order.

GET /commerce/orders lists orders newest first, with count for everything the filters match:

Filter Takes
display_status processing, completed, on_hold, refunded or canceled, the status the dashboard shows
status pending, completed, canceled, archived or requires_action
payment_status not_paid, awaiting, captured, partially_refunded, refunded, canceled or requires_action
fulfillment_status not_fulfilled, partially_fulfilled, fulfilled, partially_shipped, shipped, partially_delivered, delivered, partially_returned, returned or canceled
q An order number such as #1001, an order_ ID, or part of an email address
customer_id, email, livemode, risk_hold Exact values
placed_from, placed_to Dates; placed_to is not included
updated_from A date and time: orders changed at or after it, the moment itself included (Keep a copy of your orders)
payment_route default or rollout, the payment route the order was paid on, or none for orders paid with routing off
view, fields view=detail answers each order in full, and fields names the keys to answer (Keep a copy of your orders)
limit, offset Up to 200 a page, or 100 with view=detail; 50 by default
Shell
curl "https://api.coritan.com/api/v1/orgs/acme/commerce/orders?payment_status=captured&fulfillment_status=not_fulfilled&livemode=true" \
  -H "X-API-Key: $ORG_API_KEY"

Every order carries display_status, the one word to show for it. It is the first of these that holds: canceled; refunded, when all of the payment went back; on_hold, while a risk hold keeps it; completed, once everything has shipped, or has been delivered when the store's completed_when is delivered, or once it is paid when nothing in it ships; and processing for the rest. What each status means explains them. status keeps its own values.

Reach an order by its id or its order_ public ID. Order numbers start at 1001. GET /commerce/orders/{order_id} answers the order with its items, totals, payments, refunds, fulfilments, documents, timeline and risk, the Coritan company that sold it, and what is left to refund under refundable. Each item has product_id and product_handle, the product's current handle, which is null when the product was deleted. Its seller_of_record is coritan, or merchant for an order paid through a payment page, and its payment_route names the route it was paid on. It also lists the order's returns, each with its kind, status and any exchange_order_id, and for an exchange order, exchange_for names the return and the order it replaces. GET /commerce/orders/{order_id}/documents lists the invoice and credit notes as the Coritan company issued them.

A system that keeps its own copy of the store reads every order once, then asks only for what changed. Three parameters of GET /commerce/orders do this:

view
summary, the default, answers a short row for each order. detail answers each order in full, exactly as GET /commerce/orders/{order_id} does, with its items, payments, refunds, fulfilments, returns, documents and timeline. A page of detail holds up to 100 orders, and a larger limit answers 422 with maximum. Any other word answers 422 with invalid_view, and allowed lists the two words. The filters, the order, count and the paging are the same in both views.
fields
The keys of each order to answer, comma-separated, such as id,status,totals. id always comes too. With view=detail, we read only the parts of an order that the keys need, so a page with fewer keys comes back sooner. A key the view does not have answers 422 with invalid_fields, and allowed lists the keys. With summary the keys are id, public_id, display_id, status, display_status, payment_status, fulfillment_status, email, currency_code, total, refunded_total, item_count, livemode, placed_at, customer_id, risk_level, risk_hold, payment_route and created_at. With detail they are id, public_id, display_id, status, display_status, payment_status, fulfillment_status, email, customer_id, currency_code, locale, livemode, sales_channel_id, region_id, shipping_address, billing_address, note, promo_codes, totals, items, shipping_methods, fees, payments, refunds, refundable, fulfillments, returns, exchange_for, documents, timeline, risk, seller_of_record, seller_entity_key, payment_route, tax_provider, tax_scheme, terms, client, external_id, metadata, placed_at, canceled_at, completed_at, created_at and updated_at.
updated_from
A date and time. Only the orders whose updated_at is at or after it come back, and an order changed at that exact moment is included. A time without a zone is read as UTC. The updated_at of an order moves when the order itself changes: its status, payment, fulfilment status, refunds, email, address, note, metadata or items (an item edit). It does not move when an order only gains a note on its timeline, a fulfilment that has not shipped, or a return request, so read those with GET /commerce/orders/{order_id}. Only view=detail answers updated_at. It combines with every other filter, with fields and with the paging.

To keep a copy of the store:

  1. Note the time, then read every order in full, 100 at a time, until you hold count orders. Keep each one under its id.

    Shell
    since=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    offset=0
    while :; do
      page=$(curl -s "https://api.coritan.com/api/v1/orgs/acme/commerce/orders?view=detail&limit=100&offset=$offset" \
        -H "X-API-Key: $ORG_API_KEY")
      echo "$page" | jq -c '.orders[]'
      [ "$(echo "$page" | jq '.orders | length')" -lt 100 ] && break
      offset=$((offset + 100))
    done
    
  2. Later, ask for the orders that changed since that time. Go back one minute, so a clock that runs behind ours misses nothing. Replace each order you hold by its id. The answer has the same shape, so page it the same way.

    Shell
    curl -G "https://api.coritan.com/api/v1/orgs/acme/commerce/orders" \
      --data-urlencode "view=detail" \
      --data-urlencode "limit=100" \
      --data-urlencode "updated_from=2026-10-09T09:59:00Z" \
      -H "X-API-Key: $ORG_API_KEY"
    
  3. Note the time before each round, and use it, less a minute, as the next round's updated_from.

A page is cut after the filters, so an order that changes while you page can move the next page by one. The next round picks it up.

PATCH /commerce/orders/{order_id} corrects the email or the shipping_address, or changes the note (up to 2,000 characters) and metadata. The address keeps its country, and in the US and Canada its state or province, because the order was taxed for them. The address cannot change once part of the order has shipped, which answers 409 with already_shipped, or once a fulfilment has gone to its provider, which answers 409 with fulfillment_started: ask the provider to change it.

POST /commerce/orders/{order_id}/notes adds a note of up to 500 characters to the timeline, and customer_visible: true shows it on the shopper's order page.

Use an item edit when an order's contents change after it was paid, for example because your warehouse swapped a size, left an item out or changed a quantity, and the order here must match what shipped. POST /commerce/orders/{order_id}/edits takes commerce.orders:edit (Tier 3 support and up). A key needs that scope by name, because commerce.orders:write does not include it. A person does not need to confirm it is them first, because no money moves.

An edit names the lines to change. Each line is named once, and an edit holds at most 100 changes.

add
A list of {variant_id, quantity, unit_price}. The quantity is 1 to 999. We price the variant for the order's currency and region, unless you send a unit_price in minor units, such as 0 for a free replacement.
update
A list of {order_item_id, quantity, unit_price}. Send the quantity, the unit_price or both. A quantity of 0 removes the line.
remove
A list of order_item_id to take off the order.
reason, note
Up to 64 and 500 characters, kept with the edit.
adjust_payment
Only false, which is the default. An edit never charges the shopper or refunds them, and true answers 422 with payment_adjustment_unavailable. Settle a difference with a refund.
reserve_stock
true by default: we hold stock for the units you add. Send false when your stock counts come from your warehouse. Units taken off an order always release the stock held for them.
edit_sent_fulfillments
false by default. Send true when a fulfilment provider already holds the lines you change and already has the edited order. We change our record of the fulfilment and send nothing to the provider.
preview
true checks the edit, answers 200 with the edit it would make and writes nothing, so you can show the new totals before you save.
Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/orders/order_01j8z3k4m5n6p7q8r9s0t1v2w3/edits" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Idempotency-Key: edit-1043-1" \
  -H "Content-Type: application/json" \
  -d '{
    "remove": [5521],
    "add": [{"variant_id": 88, "quantity": 1}],
    "reason": "size_swap",
    "note": "The customer asked for the 10mg size."
  }'

The answer is 201 with the order and the edit. The edit has its id, number (1 for the first edit of the order), the actor, reason and note, and these:

  • items: one entry for each line it touched, with change (added, removed or changed), order_item_id, variant_id, sku, title, and before and after. Each side holds quantity, unit_price, subtotal, discount_total, tax_total and total.
  • totals: the order's before and after figures and their difference.
  • effects: the stock released and held, and the fulfilments we trimmed, cancelled or changed after they were sent.

The same record is on the order's timeline as order.edited, and commerce.order.edited goes to your webhooks. A retry with the same Idempotency-Key and body gets the first answer back and edits nothing more. An organization can make 60 edits a minute.

What an edit changes, and what it leaves alone:

  • Totals. We always work the order's subtotal, discount_total, tax_total and total out again from its lines, and GET /commerce/orders/{order_id}, the order list, the shopper's order history and updated_from show them. The payment is not touched: paid_total, refunded_total, shipping and store fees stay as they were, and we issue no new invoice or credit note. The invoice keeps what the shopper was charged.
  • Prices. A new quantity keeps the unit price and scales the line's discount and tax by units. A unit_price replaces the price, removes the line's discount and works the tax out again at the order's address. We do not work out promotions again, and the affiliate commission and ad conversions already sent do not change.
  • Removed lines. A removed line stays on the order with a quantity of 0 and no money, because the invoice, credit notes and fulfilments name it. The shopper's order, emails and the store's segments leave it out.
  • Refunds. A line is refundable up to what the shopper paid for it before the first edit. A removed line can still be refunded by item, and a line you added cannot, because nobody paid for it. Cancelling an edited order refunds what was paid.
  • Fulfilments. A fulfilment that has not been sent yet gives up the units you take off, and a fulfilment left empty is cancelled. One that a provider holds answers 409 with fulfillment_started for any line whose quantity you change, unless edit_sent_fulfillments is true. Changing only a price never touches a fulfilment.
  • Orders an import wrote. An import in upsert mode leaves an edited order as it is.

The edit refuses with these answers:

Status error Meaning
404 not_found The order is not in this organization.
422 unknown_variant The variant is not one this store sells in the order's sales channel, or belongs to another organization.
422 unknown_item order_item_id is not a line of this order.
422 price_unavailable The variant has no price in the order's currency. Send a unit_price.
422 gift_card_line A gift card line cannot be edited, and a gift card cannot be added.
422 payment_adjustment_unavailable adjust_payment was true.
422 invalid The edit is empty, changes nothing, names a line twice, or a value is out of range.
409 order_would_be_empty Every line would end with no units. Cancel or refund the order instead.
409 units_shipped The quantity is below the units that shipped plus those refunded before they shipped. minimum says how many.
409 units_in_return A return or claim holds units of the line.
409 fulfillment_started A fulfilment with its provider holds the line. fulfillment_ids names it.
409 insufficient_inventory There is not enough stock for the units you add.
409 tax_unavailable We cannot work out the tax on the changed lines for the order's address.
409 order_canceled, order_refunded, order_not_editable The order is cancelled, fully refunded, archived, waiting for payment or an exchange order.
409 refund_in_progress, order_disputed A refund waits for the payment provider, or a dispute is open. Edit the order when it ends.
429 rate_limited More than 60 edits in a minute.

We hold an order that shows two or more of these signals, or that the payment provider rates as high risk:

  • It is the first order from its email and above risk_high_value.
  • The billing and shipping countries differ.
  • The shopper's IP address is in another country than the shipping address.
  • The payment provider rates it as elevated risk.

A held order has risk_hold: true, and risk lists the reasons. Nothing on it can be fulfilled until you release it, a fulfilment provider receives nothing for it, and any gift cards it sold cannot be spent. When you have checked the order, release it:

Shell
curl -X PATCH "https://api.coritan.com/api/v1/orgs/acme/commerce/orders/order_01j8z3k4m5n6p7q8r9s0t1v2w3" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"risk_hold": false}'

To turn the order down instead, cancel it.

POST /commerce/orders/{order_id}/refunds refunds part or all of an order. Send an amount, which we spread over what is left, or items with each order_item_id and quantity, plus shipping: true to refund the shipping too. Add a reason of up to 64 characters and a note.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/orders/order_01j8z3k4m5n6p7q8r9s0t1v2w3/refunds" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Idempotency-Key: refund-1043-1" \
  -H "Content-Type: application/json" \
  -d '{"items": [{"order_item_id": 5521, "quantity": 1}], "reason": "damaged"}'

We refund the card or PayPal payment first and credit the rest to any gift cards the shopper paid with, and we issue a credit note. The answer is 201 with the refund and the order. On an order paid through a payment page, we move no money and issue no credit note: the refund is recorded, and you return the money at the MAEF parent site, as Refund an order paid through a parent describes.

A payment that reaches an order after it was placed, such as a shopper paying twice, is refunded on its own. The order's payments mark it with unmatched: true. Send its id as payment_id, with an amount or with none to refund all that is left of it. Such a refund takes no items or shipping. The payment the order was paid with answers 422 there, and an amount above what is left of that payment answers 422 with refund_too_large.

A refund takes an optional Idempotency-Key header of up to 128 characters. A retry with the same key and body within 24 hours gets the first answer back and refunds nothing more. The same key with another body, or for another order, answers 409 with idempotency_key_reused, and a retry while the first request runs answers 409 with idempotency_in_progress. A refund that fails keeps no key, so you can send it again with the same one.

A refund moves the stock of the units it names in items, and only those. An amount, or shipping on its own, moves no stock.

  • Without restock, the refunded units come first from those that have not shipped. They will never ship: we release their stock and take them out of any pending fulfilment we have not yet tried to send, and cancel one that is left empty. When a fulfilment that a provider already has still holds them, the order's timeline names it, so ask the provider to take them out. Any other refunded units have shipped, and their stock does not move.
  • Send restock: true when shipped units have come back to you. The refunded units then come first from the shipped units not yet returned: they go back in stock at the location_id you send, or where they shipped from, and count in the item's returned_quantity. Any others come from units that have not shipped, as without restock.

restock needs items, and location_id needs restock: true and must be one of the store's stock locations. Otherwise the answer is 422.

Units that an open return holds are refunded through the return, as Refund a return describes. Asking for more of an item in items than open returns leave answers 409 with units_in_return: return_id names a return that holds units of the item, and refundable_quantity says how many you can refund on the order. Refunding an exchange order, which sends an exchange's new items or a claim's replacements, answers 409 with exchange_order.

POST /commerce/orders/{order_id}/cancel refunds whatever is left, gives the stock back and ends the order. Once any of it has shipped, refund it instead. Cancelling an exchange order refunds nothing, and what its new items took becomes refundable again on the order they came from.

Warning

A refund or a cancellation sends money back to the shopper at once, and neither can be undone.

Refunds and cancellations share a limit of 30 a minute for the organization.

A person must have confirmed it is them in the last ten minutes to refund or cancel. With a coritan.com access token, send your password or a code to POST /orgs/{org_slug}/step-up and pass the token it answers in the X-Org-Step-Up header. A staff console session confirms in the staff console. Without that, the answer is 403 with reauth_required, and nothing is refunded and none of the limit or the Idempotency-Key is used, so send the same request again once you have confirmed. An API key is never asked.

DELETE /commerce/orders/{order_id}?confirm={number} removes the order from the platform entirely, for test, duplicate or imported orders. confirm is the order's number, 1006 or #1006; any other value answers 422. It takes commerce.orders:delete (Admin and up), and a person must have stepped up in the last ten minutes, as Refund or cancel an order describes (403 reauth_required).

Everything that exists because of the order goes with it: its items, shipping methods, fees, timeline, payments with their sessions, refunds, documents, ledger entries, fulfilments and their requests to a provider, returns, disputes, promotion uses, gift card movements, affiliate referrals, flow emails, integration deliveries, conversion marks and the cart it was checked out from. Held stock is released, each promotion's use count drops, and a gift card the order spent or was refunded to gets that amount back or off. Nobody is emailed, and no webhook is sent. The answer lists what went, by kind, in removed, and the organization's audit log keeps an order.deleted entry.

409 refuses an order whose deletion would lose money or a parcel:

Code Why
order_paid_out Money from the order is in a payout.
order_at_warehouse A fulfilment waits at its provider (requested, accepted or in exception). Cancel it there first. When the provider already cancelled or deleted the parcel without telling Coritan, which only reads shipments back, send provider_canceled=true and the parcel no longer refuses the delete.
order_has_payment A live order has a payment that is not fully refunded. Refund or cancel the order first.
order_disputed A live order has a dispute.
gift_card_in_use A gift card the order sold was spent on another order.

Test orders are refused only for the first two. Imported orders have no payments or ledger entries, so only a fulfilment waiting at a provider holds them.

Shell
curl -X DELETE "https://api.coritan.com/api/v1/orgs/acme/commerce/orders/order_01J9ZK?confirm=1006" \
  -H "X-API-Key: $ORG_API_KEY"

A checkout whose payment fails, or that the shopper does not finish paying within the store's unpaid_minutes, becomes no order. GET /commerce/checkouts?state=unpaid lists those, each with why it went unpaid (reason is failed or expired), what the provider said, and its unpaid checkout email under comeback. state=awaiting_payment, the default, lists the payments shoppers have started and not finished yet. Each row also says who the shopper is, with name, phone and country from the cart's addresses (the billing address first, else the shipping address), and carries the cart's metadata. Reading them takes commerce.orders:read, and Follow up unpaid checkouts has every field.

Shell
curl "https://api.coritan.com/api/v1/orgs/acme/commerce/checkouts?state=unpaid" \
  -H "X-API-Key: $ORG_API_KEY"

Money that arrives for an expired payment within 7 days still places the order, when the shopper started no newer payment and the cart still costs the same; the checkout then carries the order's ID in order_id for 30 days. Otherwise we refund it. Each payment that goes unpaid sends the commerce.checkout.unpaid webhook once.

A fulfilment is a set of an order's items that leave together from one stock location. Gift cards and items that need no shipping never go into one. You ship fulfilments yourself, or a fulfilment provider ships them, such as a 3PL warehouse that we send each fulfilment to.

When an order is placed, we create its fulfilments: one for each provider and location its items go to. An order on risk hold gets them when you release it. For each item, we choose the first of these that applies:

  1. The provider of the shipping option the shopper chose.
  2. The provider that runs the stock location holding the item.
  3. Your only enabled provider, when you have exactly one.
  4. You.

GET /commerce/orders/{order_id}/fulfillments lists an order's fulfilments with the requests we sent to each provider, and each item's unfulfilled_quantity.

Status What it means
pending No provider has it yet. You ship it, or it waits for the release delay or for you to dispatch it.
requested Its provider received it.
accepted Its provider took it on.
exception Something needs you. The fulfilment's exception field says what.
shipped It left the stock location.
delivered It arrived.
canceled, rejected You cancelled it, or its provider refused it. Its items are free for a new fulfilment.

A fulfilment that no webhook provider ships stays pending until you ship it:

  1. Find its id in GET /commerce/orders/{order_id}/fulfillments.

  2. Mark it shipped, with its tracking. To ship part of it, add items: those units become a fulfilment of their own, and the answer's remainder keeps the rest under the old ID.

    Shell
    curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/orders/order_01j8z3k4m5n6p7q8r9s0t1v2w3/fulfillments/88/ship" \
      -H "X-API-Key: $ORG_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"tracking": [{"carrier": "DHL", "number": "00340434161234567890", "url": "https://www.dhl.com/track?id=00340434161234567890"}]}'
    
  3. Mark it delivered with POST .../fulfillments/{fulfillment_id}/deliver. A fulfilment you never marked shipped is marked shipped first, without tracking.

Delivering a fulfilment sends the commerce.fulfillment.delivered event, makes the order completed when the store's completed_when is delivered, starts the returns window and, once every fulfilment of the order is delivered, brings its payouts forward. It also sends the shopper the delivery email when the store's notify_delivered is true, once for each fulfilment. A fulfilment answers 409 with already_delivered when it was delivered before.

You rarely need this route for FedEx. With the fedex integration on, we check each shipped fulfilment whose tracking names FedEx, and deliver it ourselves with FedEx's own delivery time. Every effect above follows, exactly as with this route. Read the integration with GET /commerce/integrations, where its tracking says when we last checked, how many fulfilments we checked and how many we delivered.

A shipment takes up to 20 tracking entries, each with a carrier and a number of up to 100 characters, and optionally a url. The Store API shows the shopper each shipment with its tracking.

POST .../fulfillments/{fulfillment_id}/cancel cancels a fulfilment that has not shipped, with an optional reason, and frees its items. When its provider may already have it, we send the provider a fulfillment.cancel. Cancelling a fulfilment refunds nothing.

To ship freed items, add a fulfilment for them. Name each order_item_id and quantity, whose IDs are in the order's items, and optionally the provider_id or location_id to use:

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/orders/order_01j8z3k4m5n6p7q8r9s0t1v2w3/fulfillments" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items": [{"order_item_id": 5521, "quantity": 1}], "provider_id": 3}'

A fulfilment takes only units that no other fulfilment holds, and asking for more answers 409 with quantity_exceeds_unfulfilled. A webhook provider whose auto_dispatch is on receives the new fulfilment at once, without the release delay.

A provider is manual or webhook. A manual provider is a team that ships by hand, such as your own warehouse: we send it nothing, and it reports what it ships with an organization API key. A webhook provider is an HTTPS endpoint at your 3PL. We send it each fulfilment to ship, and it sends back what happened.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/fulfillment-providers" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind": "webhook", "name": "Example 3PL", "endpoint_url": "https://3pl.example.com/coritan/orders", "config": {"release_delay_minutes": 60}}'

The answer holds secrets with the outbound_secret, which signs what we send, and the inbound_secret, which the 3PL signs its events with. We show them this once. POST .../rotate-secrets makes new ones, and the old ones stop working at once. POST .../test sends a signed ping and reports what the endpoint answered.

config key What it does
auto_dispatch true by default: we send each fulfilment once the release delay has passed. false waits for you to dispatch it.
release_delay_minutes How long after the order is placed we wait, from 0 to 10,080. It gives you time to change or cancel an order.
service_code_map Maps a shipping option's ID or provider code to the 3PL's service code, up to 200 entries.
field_map Renames the top-level keys of the body we send, for a 3PL that expects other names.
include_prices true by default. false leaves prices and totals out of the body.
returns false by default. true makes a webhook provider receive the goods that come back from returns of units it shipped. We send it return.requested and return.canceled, and it answers with return.received, as Have a 3PL receive the goods explains.

The endpoint must use https on a public host. A provider that is disabled receives nothing, and what waited for it goes out when you enable it again. You cannot delete a provider while a fulfilment that has not shipped uses it. A store has up to 20 providers.

Check what we send to a provider

Section titled Check what we send to a provider

Each request is a POST with a JSON body whose schema is coritan.fulfillment.v1 and whose type is fulfillment.create, fulfillment.cancel or ping, or for a provider that receives returns, return.requested or return.canceled. A fulfillment.create holds the order with its shipping address, shipping method and seller, the fulfillment with its location, and the items. A return.requested holds the order, the return with the location its goods go to, and the items, as Have a 3PL receive the goods describes. Every request carries these headers:

Header Holds
X-Coritan-Signature t=<unix seconds>,v1=<hex HMAC-SHA256>
X-Coritan-Event The type
X-Coritan-Delivery The request's ID
Idempotency-Key A key that stays the same when we retry the request

To check a request, compute the HMAC-SHA256 of <t>.<raw body> with the outbound secret, compare it with v1, and refuse a t that is too old:

Python
import hashlib
import hmac
import time


def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    parts = dict(item.split("=", 1) for item in header.split(","))
    timestamp = int(parts["t"])
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
    return abs(time.time() - timestamp) <= tolerance and hmac.compare_digest(expected, parts["v1"])

Answer with a 2xx status to accept the request. A JSON answer with a reference saves it as the fulfilment's provider_reference, or the return's for a return.requested. We retry after 408, 429, a 5xx, a timeout of 10 seconds or a failed connection, 1, 5, 30, 120 and 480 minutes later, and then give up. Any other answer fails at once, and we do not follow redirects. A fulfillment.create that fails puts the fulfilment in exception; fix the cause and send it again with POST .../fulfillments/{fulfillment_id}/dispatch. When a return.requested fails, we note it in the order's timeline, and you receive the goods yourself when they arrive. After five failures in a row we send the commerce.fulfillment.provider_failing webhook.

GET /commerce/fulfillment-providers/{provider_id}/deliveries lists what we sent and how the endpoint answered, and GET .../events lists what the provider sent us. Filter the deliveries by kind: create, cancel, ping, return_create or return_cancel.

The 3PL reports back with POST /api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}/events. It signs the raw body with the inbound secret in X-Coritan-Signature, in the same form as above and within 5 minutes, or sends an organization API key with commerce.fulfillment:write in X-API-Key.

JSON
{
  "id": "evt-3pl-10492",
  "type": "fulfillment.shipped",
  "data": {
    "fulfillment_id": 88,
    "tracking": [{"carrier": "DHL", "number": "00340434161234567890"}]
  }
}

id is the 3PL's own event ID, up to 128 characters. An event already processed answers 200 with status: duplicate and changes nothing, so the 3PL can send it again safely. One that failed can be sent again under the same ID.

type data What it does
fulfillment.accepted fulfillment_id or order_id, and reference The 3PL took the fulfilment.
fulfillment.rejected fulfillment_id, and reason The 3PL refused it, and its items are free again.
fulfillment.shipped fulfillment_id or order_id, tracking, and items by sku or order_item_id with quantity Marks it shipped. Without items, everything in it shipped.
fulfillment.delivered fulfillment_id or order_id Marks it delivered, with every effect of the deliver route, including the delivery email when the store turned it on.
fulfillment.exception fulfillment_id and message Marks a problem with it.
inventory.levels levels, each with sku, stocked_quantity and incoming_quantity, and location_id Sets stock at a location the provider runs, up to 1,000 SKUs.
return.received return_id, location_id, and items by sku or order_item_id with quantity and condition Receives a return we sent the provider. sellable units, the default, go back in stock at location_id, which must be a location the provider runs, or else where the return.requested said. damaged units do not. Without items, every unit comes back sellable. It refunds nothing.

order_id is the order's order_ public ID. The answer is 200 with status processed, ignored, failed or duplicate. A body that is not an event answers 422, and one over 256 KB answers 413.

The store's events reach your organization webhooks with names that start with commerce.. Subscribe a webhook to the exact names, or to * for everything. The delivery, retries and signature are the ones Receive organization webhooks describes.

JSON
{
  "id": "evt_48213",
  "type": "commerce.order.placed",
  "livemode": true,
  "created_at": "2026-09-26T14:03:11Z",
  "resource": {"type": "order", "id": "1742"},
  "data": {"order_id": 1742, "public_id": "order_01j8z3k4m5n6p7q8r9s0t1v2w3", "display_id": 1043, "livemode": true, "risk_hold": false, "display_status": "processing"}
}

An event can arrive more than once, so skip an id you have handled.

Event When data
commerce.product.created, .updated, .deleted A product changes. product_id, handle
commerce.inventory.updated Stock changes through the API, an import, a provider, a refund or a return. location_id, inventory_item_ids, skus
commerce.checkout.unpaid A checkout's payment fails, or expires after the store's unpaid_minutes. Sent once for each payment, and never for a cart that already has its order. cart_id, reason (failed or expired), email, amount, currency_code, provider, started_at, unpaid_at
commerce.order.placed A shopper completes a cart. order_id, public_id, display_id, livemode, risk_hold, display_status
commerce.order.updated An order's email, address, note or risk hold changes. The same, plus changes
commerce.order.edited An order's items are edited. The same as commerce.order.updated with changes of ["items"], plus edit_id, number, reason, totals_before, totals_after and items (each line's change, order_item_id, variant_id, sku, and its before and after quantity and unit_price). Read the order for the full lines.
commerce.order.canceled An order is cancelled. The same, plus reason, refund_id, refunded_amount, gift_card_amount, currency_code
commerce.payment.captured A payment is taken. order_id, payment_id, amount, currency_code, provider, livemode
commerce.payment.refunded Money goes back to the shopper. order_id, public_id, refund_id, amount, gift_card_amount, total, currency_code, reason, cancellation, livemode
commerce.payment.refund_required A MAEF parent took money through a payment page that could not pay for an order, so you must refund it at the parent. We refund nothing. cart, session_id, order_id, amount, currency_code, reference, reason, livemode, and email when the return named no payment of the store
commerce.checkout_page.paid, .failed The payment site reports that a payment on a hosted checkout page succeeded, or failed. checkout_page, reference, amount, currency_code, parent_order_id, email, livemode
commerce.checkout_page.refund_required The payment site took money on a hosted checkout page that cannot pay it, so you must refund it at the payment site. We refund nothing. The same, plus reason (canceled, second_payment, amount_mismatch or no_page). checkout_page is null for no_page
commerce.customer.created A guest order creates a customer. customer_id, email, is_guest
commerce.customer.updated A customer's store profile or groups change. customer_id, changed
commerce.gift_card.issued A gift card is sold or issued. The code is never in the event. id, last4, currency_code, amount, recipient_email, issuing_order_id, expires_at
commerce.fulfillment.created We or you create a fulfilment. order_id, fulfillment_id, provider_id, items
commerce.fulfillment.accepted The provider takes a fulfilment on. order_id, fulfillment_id, reference
commerce.fulfillment.rejected, .canceled The provider refuses a fulfilment, or it is cancelled. order_id, fulfillment_id, reason
commerce.fulfillment.shipped A fulfilment ships. order_id, fulfillment_id, tracking, items
commerce.fulfillment.delivered A fulfilment arrives: you deliver it, its provider reports it, or FedEx delivery tracking sees it. order_id, fulfillment_id
commerce.fulfillment.exception A fulfilment needs you, such as when its provider's endpoint refused it. order_id, fulfillment_id, message
commerce.fulfillment.provider_failing A provider's endpoint fails five times in a row. We send it again only after a request succeeds. provider_id, name, consecutive_failures, last_error
commerce.dispute.opened, .won, .lost A shopper's dispute opens, or ends in your favour or theirs. order_id, dispute_id, amount, currency_code
commerce.payout.created, .paid, .failed, .canceled A payout is created, completed, declined or cancelled. id, currency_code, amount, platform_fee, net_amount, status, payout_method, entry_count
commerce.return.requested, .approved, .rejected, .received, .refunded, .completed, .canceled A return, exchange or claim is asked for or opened, approved, declined, received, refunded, settled with nothing to refund, or cancelled. return_id, order_id, public_id, kind, claim_type, status, currency_code, livemode, and the fields each one adds
commerce.lead.created A new address signs up on the storefront's form or popup. lead_id, email, source, variant
commerce.marketing.contact.subscribed An email marketing contact becomes subscribed. contact_id, and list_ids and source when known
commerce.marketing.contact.unsubscribed An email marketing contact unsubscribes or is unsubscribed. contact_id, and source when known
commerce.marketing.campaign.sent A campaign finishes sending. campaign_id, recipients
commerce.marketing.email.bounced, .complained A marketing email bounces, or a reader reports it as spam. campaign_id when it belongs to one, contact_id, reason
commerce.affiliate.referral.created, .updated An order credits an affiliate, or its referral changes status. referral_id, affiliate_id, order_id, status, previous_status, commission, currency_code, basis, first_order
commerce.affiliate.claim.created An affiliate claims a reward. claim_id, affiliate_id, reward_number, variant_id
commerce.inventory.low_stock An item's stock falls to its low-stock threshold. The first event of a batch also lists every item the batch alerted. inventory_item_id, sku, available, threshold, items
commerce.exchange.created We create an exchange order to send an exchange's new items or a claim's replacements. return_id, kind, order_id, public_id, exchange_order_id, exchange_public_id, display_id, items, livemode

GET /commerce/events shows the same events with whether we delivered each one, filtered by event_type (without the commerce. prefix) and status (pending, dispatched or failed). Each event has its id, type, resource, livemode, status, attempts, error, created_at and dispatched_at, and data: the object a webhook for that event carries under data, so a system that missed a delivery reads the same facts here. The list is newest first, 50 a page and at most 200. Send next_before_id as before_id for the next page, and stop when it is null.

An event holds ids and small facts. The few that carry an email address show it in data only to a caller who can read the record it came from. commerce.customer.created, commerce.lead.created and commerce.gift_card.issued (its recipient_email) need commerce.customers:read. commerce.checkout.unpaid and commerce.payment.refund_required need commerce.orders:read, and the three commerce.checkout_page.* events need commerce.checkout:read. For any other caller, data leaves the address out and keeps the rest. The webhook itself always carries it.

Read the balance and the ledger

Section titled Read the balance and the ledger

GET /commerce/balance answers, for each currency your live orders were paid in:

Field What it is
pending Money from sales that is not yours to take yet.
available Money that is yours.
reserved Money held back as a reserve until its release date.
payable available less reserved, and never below zero.

A sale becomes available after the payout delay in your merchant profile's terms, 7 days unless we set another, or when it is delivered, if that comes first.

GET /commerce/ledger lists every movement, newest first, filtered by currency_code, status, entry_type, order_id, created_from and created_to. Each order books its sale and shipping, the tax that the Coritan company owes, and our platform cut as a commission row. The cut is on the money that came in: cut_basis is the cash the shopper paid less its share of the tax (a gift card sold counts, a gift card spent does not), cut_month is the month whose rate it pays, and the description names the rate. A refund books the reverse and returns the cut on the cash it refunds, with a negative cut_basis; money refunded to a gift card returns nothing. Once the month ends, one commission row per currency settles it at the month's final rate. What Stripe or PayPal charged for the payment follows as a processing_fee row, at cost. A dispute books chargeback rows, as Answer disputes on store orders describes. Test orders book to a separate ledger that you read with livemode=false, and never count towards the balance. An order paid through a payment page books nothing, because the money never reaches us.

We pay the payable amount out on your store's schedule. A payout marks the rows it pays paid_out and books a payout row against them, as Get store payouts and tax reports describes.

Most Commerce API routes read no Idempotency-Key, so a request that timed out may have succeeded. Before you send it again, check:

  • Create a product with an explicit handle. If the first request succeeded, the retry answers 409 with handle_taken and creates nothing.
  • Read the order before you retry a refund sent without an Idempotency-Key. A refund never exceeds what is left, and while an earlier one waits for the payment provider, a new one answers 409 with refund_in_progress.
  • Set stock with stocked_quantity: sending the same count twice gives the same result, where a repeated delta counts twice.
  • A provider's events carry their own id, so a repeated event changes nothing.

Refunding an order and every write on a return take an Idempotency-Key, as Refund or cancel an order and Handle returns, exchanges and claims explain.

Sending a dispute's evidence, accepting a dispute and asking for a payout take an Idempotency-Key, as Answer disputes on store orders and Ask for a payout explain.

In the Store API, completing a cart takes an Idempotency-Key, as Complete the order explains.

An error answers with a status and a detail object that holds a machine-readable error code and a message to show a person. Some errors add fields, such as field for the input that is wrong.

JSON
{"detail": {"error": "invalid", "message": "currency_code must be a currency code such as EUR.", "field": "currency_code"}}
Status error Meaning
403 scope_required, role_required, people_only, forbidden The credential cannot do this.
404 not_found, commerce_not_enabled The thing, or the store, does not exist for this organization.
409 conflict or a specific code, such as handle_taken The request clashes with the store's state.
422 invalid or a specific code The input is wrong; field names it.
429 rate_limited Too many refunds and cancellations, item edits, dispute answers, payout requests, media uploads or media imports. Retry-After says when to try again.
502 refund_failed, payment_provider_error The payment provider refused a refund, or a dispute's evidence or acceptance.

The store's media has errors of its own, such as 413 with quota_exceeded when the store is full. Upload and import your store's media lists them.

Errors from the organization API key itself, such as a key that is missing, revoked or used from the wrong address, are on Create organization API keys.

Your storefront can list the published products through the Store API with a test publishable key, and a test order placed there appears in GET /commerce/orders?livemode=false. Once we approve the merchant profile and switch the store to live, live keys take real orders, and each one reaches your ledger.

404 with commerce_not_enabled
Commerce is not turned on for the organization, or Coritan disabled the store. Ask support.
403 with scope_required
The key lacks the scope in the answer's scope. Keys cannot change, so create one with the scopes it needs and revoke the old one.
403 with role_required
The member's role is below the lowest role for the route, as the answer's permission shows. Scopes and roles lists them.
403 with people_only
The merchant profile routes and payout requests refuse every key. Call them with an owner's or admin's token.
409 with market_not_enabled
A country in the region is one where Coritan does not sell yet. Take it out of the region.
409 with country_in_other_region
The country is already in another region. Take it out of that region first.
409 with default_region or default_channel
You tried to disable or delete the default region or sales channel. Make another one the default first.
409 with handle_taken or sku_taken
Another product has that handle or SKU. Choose another, or change the product that has it.
409 with import_in_progress
The store runs one import at a time, and another is queued or running. Follow it with GET /commerce/imports/{import_id}, using the import_id in the answer, and send the file again once it has finished.
409 with order_on_hold
The order is on risk hold. Release the hold before you fulfil it.
409 with already_shipped
Part of the order has shipped, so it cannot be cancelled or have its address changed. Refund the items instead.
409 with fulfillment_started
A fulfilment provider already has the order. To change the address, ask the provider. To edit an item the provider already holds, send edit_sent_fulfillments: true once the provider has the edited order (Edit the items of an order).
409 with quantity_exceeds_unfulfilled
You named more units than there are: other fulfilments hold them, or the fulfilment you are shipping part of holds fewer. The answer's unfulfilled says how many there are. To move units between fulfilments, cancel one first.
409 with refund_in_progress
An earlier refund is still waiting for the payment provider. Read the order again in a few minutes.
409 with gift_card_used
The order sold a gift card that has been spent, so the order cannot be refunded in full. Refund the other items.
409 with promotion_in_use
An order used the promotion. Set its status to disabled instead of deleting it.
409 with limit_reached
The store has as many of these as it can hold. Delete one you no longer need.
422 listing missing
The merchant profile lacks what review needs. Fill in the fields in missing and submit it again.
429 with rate_limited
The organization made more than 30 refunds and cancellations in a minute, or went over the limit for dispute answers or payout requests. Wait for the time in Retry-After.
A fulfilment stays requested or goes to exception
Read GET /commerce/fulfillment-providers/{provider_id}/deliveries to see how the endpoint answered, fix the endpoint, then dispatch the fulfilment again.

API operations on this page

MethodPathWhat it does
GET/api/v1/orgs/{org_slug}/commerce/balanceGet balance
GET/api/v1/orgs/{org_slug}/commerce/categoriesList categories
POST/api/v1/orgs/{org_slug}/commerce/categoriesA category; isinternal keeps it out of the Store API
GET/api/v1/orgs/{org_slug}/commerce/categories/{category_id}Get category
PATCH/api/v1/orgs/{org_slug}/commerce/categories/{category_id}Moving a category under itself or one of its children is a 409
DELETE/api/v1/orgs/{org_slug}/commerce/categories/{category_id}A category with children answers 409; move or delete them first
GET/api/v1/orgs/{org_slug}/commerce/checkoutsThe store's checkouts: carts that started a payment, read through their latest payment
GET/api/v1/orgs/{org_slug}/commerce/collectionsList collections
POST/api/v1/orgs/{org_slug}/commerce/collectionsCreate collection
GET/api/v1/orgs/{org_slug}/commerce/collections/{collection_id}The collection and its product ids, in the collection's order
PATCH/api/v1/orgs/{org_slug}/commerce/collections/{collection_id}Turning a manual collection smart drops the products added by hand
DELETE/api/v1/orgs/{org_slug}/commerce/collections/{collection_id}The collection goes; its products stay
PUT/api/v1/orgs/{org_slug}/commerce/collections/{collection_id}/productsA manual collection's products become exactly these, in this order
GET/api/v1/orgs/{org_slug}/commerce/customer-groupsThe store's customer groups by name, each with its customer count
POST/api/v1/orgs/{org_slug}/commerce/customer-groupsCreate group
GET/api/v1/orgs/{org_slug}/commerce/customer-groups/{group_id}The group; its customers are GET /commerce/customers?groupid=
PATCH/api/v1/orgs/{org_slug}/commerce/customer-groups/{group_id}Update group
DELETE/api/v1/orgs/{org_slug}/commerce/customer-groups/{group_id}Delete group
PUT/api/v1/orgs/{org_slug}/commerce/customer-groups/{group_id}/customersSet group customers
GET/api/v1/orgs/{org_slug}/commerce/customersNewest first, with each customer's order count, spend and groups
GET/api/v1/orgs/{org_slug}/commerce/customers/{customer_id}The customer, their saved addresses, note, tags, groups and spend
PATCH/api/v1/orgs/{org_slug}/commerce/customers/{customer_id}Note, tags, tax exemption, VAT id and its check; acceptsmarketing takes only false
GET/api/v1/orgs/{org_slug}/commerce/eventsWhat the store announced, newest first: the log behind its webhooks
GET/api/v1/orgs/{org_slug}/commerce/fulfillment-providersThe store's fulfillment providers, oldest first
POST/api/v1/orgs/{org_slug}/commerce/fulfillment-providersA provider
GET/api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}Get provider
PATCH/api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}Rename, enable or disable, move the endpoint, change settings
DELETE/api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}Refused while a fulfillment that has not shipped uses it; disable it instead
GET/api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}/deliveriesRequests sent (or waiting to be sent) to the provider, newest first
GET/api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}/eventsEvents the provider sent, newest first, with what became of each
POST/api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}/eventsOne event from a fulfillment provider
POST/api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}/rotate-secretsNew outbound and inbound secrets, returned this once
POST/api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}/testSend a webhook provider a signed ping and report what the endpoint answered (delivery)
GET/api/v1/orgs/{org_slug}/commerce/gift-cardsNewest first
POST/api/v1/orgs/{org_slug}/commerce/gift-cardsIssue a card
GET/api/v1/orgs/{org_slug}/commerce/gift-cards/{gift_card_id}The card and its latest transactions, newest first
PATCH/api/v1/orgs/{org_slug}/commerce/gift-cards/{gift_card_id}Disable or re-enable a card, move its expiry, correct its recipient
POST/api/v1/orgs/{org_slug}/commerce/gift-cards/{gift_card_id}/adjustMove the balance by a signed amount; 409 giftcardbalancenegative below zero
GET/api/v1/orgs/{org_slug}/commerce/importsList imports
POST/api/v1/orgs/{org_slug}/commerce/importsQueue a file for import and answer 202 with it
POST/api/v1/orgs/{org_slug}/commerce/imports/shopify-productsQueue a file for import and answer 202 with it
GET/api/v1/orgs/{org_slug}/commerce/imports/{import_id}Get import
POST/api/v1/orgs/{org_slug}/commerce/inventory/levels/batchSet levels by sku
GET/api/v1/orgs/{org_slug}/commerce/inventory-itemsInventory items by SKU, with their levels
GET/api/v1/orgs/{org_slug}/commerce/inventory-items/{item_id}Get item
PATCH/api/v1/orgs/{org_slug}/commerce/inventory-items/{item_id}Update item
PUT/api/v1/orgs/{org_slug}/commerce/inventory-items/{item_id}/levels/{location_id}Set level
GET/api/v1/orgs/{org_slug}/commerce/ledgerEvery movement of the store's money, newest first
GET/api/v1/orgs/{org_slug}/commerce/merchant-profileThe merchant profile that Coritan reviews as Merchant of Record
PUT/api/v1/orgs/{org_slug}/commerce/merchant-profileChange the merchant profile
POST/api/v1/orgs/{org_slug}/commerce/merchant-profile/submitSend the profile to platform review
GET/api/v1/orgs/{org_slug}/commerce/ordersNewest first; count is every order the filters match
GET/api/v1/orgs/{org_slug}/commerce/orders/{order_id}The order with its items, payments, refunds, fulfillments, documents and timeline
PATCH/api/v1/orgs/{org_slug}/commerce/orders/{order_id}Update order
DELETE/api/v1/orgs/{org_slug}/commerce/orders/{order_id}Delete order
POST/api/v1/orgs/{org_slug}/commerce/orders/{order_id}/cancelRefund what is left, give the stock back and end the order
GET/api/v1/orgs/{org_slug}/commerce/orders/{order_id}/documentsThe invoice and credit notes, with the issuing entity's details as issued
POST/api/v1/orgs/{org_slug}/commerce/orders/{order_id}/editsEdit order items
GET/api/v1/orgs/{org_slug}/commerce/orders/{order_id}/fulfillmentsThe order's fulfillments with their 3PL requests, and each item's progress
POST/api/v1/orgs/{org_slug}/commerce/orders/{order_id}/fulfillmentsFulfill chosen units
POST/api/v1/orgs/{org_slug}/commerce/orders/{order_id}/fulfillments/{fulfillment_id}/cancelCancel before it ships (409 alreadyshipped after)
POST/api/v1/orgs/{org_slug}/commerce/orders/{order_id}/fulfillments/{fulfillment_id}/deliverDelivered
POST/api/v1/orgs/{org_slug}/commerce/orders/{order_id}/fulfillments/{fulfillment_id}/dispatchDispatch fulfillment
POST/api/v1/orgs/{org_slug}/commerce/orders/{order_id}/fulfillments/{fulfillment_id}/shipShipped, with tracking
POST/api/v1/orgs/{org_slug}/commerce/orders/{order_id}/notesA note on the timeline; customervisible shows it on the shopper's order page
POST/api/v1/orgs/{org_slug}/commerce/orders/{order_id}/refundsRefund an amount, or items, shipping and fees, never more than is left
GET/api/v1/orgs/{org_slug}/commerce/price-listsList price lists
POST/api/v1/orgs/{org_slug}/commerce/price-listsCreate price list
GET/api/v1/orgs/{org_slug}/commerce/price-lists/{price_list_id}Get price list
PATCH/api/v1/orgs/{org_slug}/commerce/price-lists/{price_list_id}Fields change when named; prices replaces the list's prices
DELETE/api/v1/orgs/{org_slug}/commerce/price-lists/{price_list_id}The list and its prices go; base prices are untouched
PUT/api/v1/orgs/{org_slug}/commerce/price-lists/{price_list_id}/pricesReplace price list prices
GET/api/v1/orgs/{org_slug}/commerce/product-tagsThe tags on live products, most used first: what a tag picker suggests
GET/api/v1/orgs/{org_slug}/commerce/productsProducts, most recently changed first
POST/api/v1/orgs/{org_slug}/commerce/productsCreate product
GET/api/v1/orgs/{org_slug}/commerce/products/{product_id}Get product
PATCH/api/v1/orgs/{org_slug}/commerce/products/{product_id}Fields change when named
DELETE/api/v1/orgs/{org_slug}/commerce/products/{product_id}Archived and gone from every list and the Store API
POST/api/v1/orgs/{org_slug}/commerce/products/{product_id}/imagesAn https image, given by its url or by mediaid, an image in the store's media
PATCH/api/v1/orgs/{org_slug}/commerce/products/{product_id}/images/{image_id}Change an image: its url or mediaid, alt text, variant, rank or size
DELETE/api/v1/orgs/{org_slug}/commerce/products/{product_id}/images/{image_id}If it was the thumbnail, the next image takes its place
POST/api/v1/orgs/{org_slug}/commerce/products/{product_id}/optionsAn option such as Size
PATCH/api/v1/orgs/{org_slug}/commerce/products/{product_id}/options/{option_id}Update option
DELETE/api/v1/orgs/{org_slug}/commerce/products/{product_id}/options/{option_id}Delete option
POST/api/v1/orgs/{org_slug}/commerce/products/{product_id}/variantsCreate variant
PATCH/api/v1/orgs/{org_slug}/commerce/products/{product_id}/variants/{variant_id}Fields change when named; prices replaces the variant's base prices
DELETE/api/v1/orgs/{org_slug}/commerce/products/{product_id}/variants/{variant_id}Gone from the catalog; orders keep naming it, and its SKU is free
PUT/api/v1/orgs/{org_slug}/commerce/products/{product_id}/variants/{variant_id}/pricesReplace variant prices
GET/api/v1/orgs/{org_slug}/commerce/promotionsNewest first
POST/api/v1/orgs/{org_slug}/commerce/promotionsA code (or an automatic promotion)
GET/api/v1/orgs/{org_slug}/commerce/promotions/{promotion_id}The promotion and what it has given, per currency
PATCH/api/v1/orgs/{org_slug}/commerce/promotions/{promotion_id}The fields the body names
DELETE/api/v1/orgs/{org_slug}/commerce/promotions/{promotion_id}A promotion no order used
POST/api/v1/orgs/{org_slug}/commerce/promotions/{promotion_id}/mintMint promotion code
GET/api/v1/orgs/{org_slug}/commerce/publishable-keysThe store's publishable keys, newest first, each with its token
POST/api/v1/orgs/{org_slug}/commerce/publishable-keysA key for a storefront
DELETE/api/v1/orgs/{org_slug}/commerce/publishable-keys/{key_id}Revoke key
GET/api/v1/orgs/{org_slug}/commerce/regionsThe store's regions by name, disabled ones included
POST/api/v1/orgs/{org_slug}/commerce/regionsA region
GET/api/v1/orgs/{org_slug}/commerce/regions/{region_id}Get region
PATCH/api/v1/orgs/{org_slug}/commerce/regions/{region_id}Fields change when named; countries replaces the region's
DELETE/api/v1/orgs/{org_slug}/commerce/regions/{region_id}The region, its countries and the prices set for it go
GET/api/v1/orgs/{org_slug}/commerce/sales-channelsThe store's sales channels, oldest first, disabled ones included
POST/api/v1/orgs/{org_slug}/commerce/sales-channelsCreate channel
PATCH/api/v1/orgs/{org_slug}/commerce/sales-channels/{channel_id}Update channel
DELETE/api/v1/orgs/{org_slug}/commerce/sales-channels/{channel_id}Delete channel
GET/api/v1/orgs/{org_slug}/commerce/sales-channels/{channel_id}/stock-locationsThe locations a channel sells from
PUT/api/v1/orgs/{org_slug}/commerce/sales-channels/{channel_id}/stock-locationsThe channel sells from exactly these locations; an empty list means every enabled location
GET/api/v1/orgs/{org_slug}/commerce/shipping-optionsThe store's shipping options, grouped by zone and in each zone by rank
POST/api/v1/orgs/{org_slug}/commerce/shipping-optionsAn option in a zone for a profile (the default one when profileid is left out)
GET/api/v1/orgs/{org_slug}/commerce/shipping-options/{option_id}Get option
PATCH/api/v1/orgs/{org_slug}/commerce/shipping-options/{option_id}Update option
DELETE/api/v1/orgs/{org_slug}/commerce/shipping-options/{option_id}Carts that chose it choose again; orders keep the name and amount they were charged
GET/api/v1/orgs/{org_slug}/commerce/shipping-profilesThe store's shipping profiles, the default first, then oldest first
POST/api/v1/orgs/{org_slug}/commerce/shipping-profilesCreate profile
PATCH/api/v1/orgs/{org_slug}/commerce/shipping-profiles/{profile_id}Update profile
DELETE/api/v1/orgs/{org_slug}/commerce/shipping-profiles/{profile_id}A profile no product and no option uses
GET/api/v1/orgs/{org_slug}/commerce/shipping-zonesThe store's shipping zones, oldest first, each with how many options it holds
POST/api/v1/orgs/{org_slug}/commerce/shipping-zonesCreate zone
GET/api/v1/orgs/{org_slug}/commerce/shipping-zones/{zone_id}Get zone
PATCH/api/v1/orgs/{org_slug}/commerce/shipping-zones/{zone_id}Update zone
DELETE/api/v1/orgs/{org_slug}/commerce/shipping-zones/{zone_id}The zone and its options
GET/api/v1/orgs/{org_slug}/commerce/stock-locationsList locations
POST/api/v1/orgs/{org_slug}/commerce/stock-locationsA warehouse, shop or 3PL site
GET/api/v1/orgs/{org_slug}/commerce/stock-locations/{location_id}Get location
PATCH/api/v1/orgs/{org_slug}/commerce/stock-locations/{location_id}Isdefault: true moves the default here
DELETE/api/v1/orgs/{org_slug}/commerce/stock-locations/{location_id}Delete location
GET/api/v1/orgs/{org_slug}/commerce/storeThe store, its sales channels, and where its merchant review stands
PATCH/api/v1/orgs/{org_slug}/commerce/storeChange the store