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

Source: https://docs.coritan.com/organizations/storefront/commerce-api/

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](/organizations/storefront/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.

Every route is under `https://api.coritan.com/api/v1/orgs/{org_slug}/commerce/`. The API reference lists them under [Commerce](/api/reference/organizations/commerce/commerce/), with the product routes under [Catalog & Services](/api/reference/organizations/catalog-services/commerce/) and the customer routes under [Customers](/api/reference/organizations/customers/commerce/). 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](/organizations/commerce/)).

## Before you begin

- Ask [support](https://www.coritan.com/dashboard/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](#authenticate) explains both.
- Have your company's legal details and its owners' names ready for the [merchant profile](#submit-the-merchant-profile). The store takes real orders only after we approve it.

## Authenticate

Send one of these with every request:

- An organization API key in the `X-API-Key` header, for your own systems. Create it through the API with the scopes it needs, as [Create a key with scopes](/organizations/api-keys/#create-a-key-with-scopes) shows. A key made in the dashboard has no scopes, 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](/organizations/staff-console/sign-in/) session. Their role decides what the token can do.

```bash
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. No route asks for a step-up.

### Scopes and roles

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, and an area with `:*`, such as `commerce.orders:*`, grants every scope in that area.

| Scope | What it allows | Lowest role |
| --- | --- | --- |
| `commerce.store:read` | Reading the store, its publishable keys and its event log. | 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 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.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.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. | Billing |
| `commerce.disputes:write` | Changing a dispute's evidence, sending it and accepting a dispute. | Billing |

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](/organizations/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](/organizations/storefront/commerce-payouts/#ask-for-a-payout) also takes an owner's or admin's token, and no key can ask for one.

## Check the store's status

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

## Submit the merchant profile

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.

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

| 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 fee, the reserve and the payout delay Coritan set, and `review_notes` hold what the reviewer wrote.

## Set up the store

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.

### Change the store's settings

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

```bash
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`. |
| `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. |
| `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](/organizations/storefront/commerce-returns/#set-the-returns-window-and-the-reasons), 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](/organizations/storefront/commerce-disputes/#add-your-policies-to-the-evidence) quotes them. |
| `payout_schedule` | How often we create [payouts](/organizations/storefront/commerce-payouts/#set-the-payout-schedule): `daily`, `weekly` or `monthly`. Defaults to `weekly`. |

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

### Add regions

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.

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

### Add stock locations

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

```bash
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](#connect-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`.

### Set up shipping

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.

```bash
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

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

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

## Add products

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.

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

Images are links: `POST /commerce/products/{product_id}/images` takes a `url`, `alt` text, and optionally a `variant_id`, a `rank` and the image's `width` and `height`.

### Group products

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

### Set sale and customer prices

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.

### Import products from Shopify

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

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

## Set stock levels

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

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

```bash
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

### Create a discount code

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

### Issue a gift card

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

## Manage customers

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.

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.

## Handle orders

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.

### Find orders

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

| Filter | Takes |
| --- | --- |
| `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 |
| `limit`, `offset` | Up to 200 a page; 50 by default |

```bash
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"
```

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

### Change an order

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

### Release a risk hold

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:

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

### Refund or cancel an order

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

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

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](/organizations/storefront/commerce-returns/#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.

## Fulfil orders

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

### Ship an order yourself

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.

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

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.

### Cancel or add a fulfilment

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

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

### Connect a fulfilment provider

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.

```bash
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](/organizations/storefront/commerce-returns/#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

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](/organizations/storefront/commerce-returns/#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](/organizations/storefront/commerce-returns/#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`.

### Send events from the provider

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

## Receive commerce webhooks

The store's events reach your [organization webhooks](/organizations/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](/organizations/webhooks/#deliveries-and-retries) 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}
}
```

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.order.placed` | A shopper completes a cart. | `order_id`, `public_id`, `display_id`, `livemode`, `risk_hold` |
| `commerce.order.updated` | An order's email, address, note or risk hold changes. | The same, plus `changes` |
| `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.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. | `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](/organizations/storefront/commerce-disputes/) opens, or ends in your favour or theirs. | `order_id`, `dispute_id`, `amount`, `currency_code` |
| `commerce.payout.created`, `.paid`, `.failed`, `.canceled` | A [payout](/organizations/storefront/commerce-payouts/) 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](/organizations/storefront/commerce-returns/#return-webhooks) 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.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`).

## 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 `commission`, which is the fee percentage in your terms plus any fixed fee. A refund books the reverse and returns the commission on what it refunds. A dispute books `chargeback` rows, as [Answer disputes on store orders](/organizations/storefront/commerce-disputes/#what-a-dispute-does-to-your-balance) describes. Test orders book to a separate ledger that you read with `livemode=false`, and never count towards the balance.

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](/organizations/storefront/commerce-payouts/) describes.

## Retry requests safely

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](#refund-or-cancel-an-order) and [Handle returns, exchanges and claims](/organizations/storefront/commerce-returns/#limits-and-retries) explain.

Sending a dispute's evidence, accepting a dispute and asking for a payout take an `Idempotency-Key`, as [Answer disputes on store orders](/organizations/storefront/commerce-disputes/#limits-and-retries) and [Ask for a payout](/organizations/storefront/commerce-payouts/#ask-for-a-payout) explain.

In the Store API, completing a cart takes an `Idempotency-Key`, as [Complete the order](/organizations/storefront/store-api/#complete-the-order) explains.

## Errors

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, dispute answers or payout requests. `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. |

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](/organizations/api-keys/#troubleshooting).

## Result

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.

## Troubleshooting

`404` with `commerce_not_enabled`
: Commerce is not turned on for the organization, or Coritan disabled the store. Ask [support](https://www.coritan.com/dashboard/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](#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](#release-a-risk-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 with its address. Ask the provider to change the address.

`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](/organizations/storefront/commerce-disputes/#limits-and-retries) or [payout requests](/organizations/storefront/commerce-payouts/#ask-for-a-payout). 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.

## Related

- [Build a checkout with the Store API](/organizations/storefront/store-api/)
- [Answer disputes on store orders](/organizations/storefront/commerce-disputes/)
- [Get store payouts and tax reports](/organizations/storefront/commerce-payouts/)
- [Handle returns, exchanges and claims](/organizations/storefront/commerce-returns/)
- [Create organization API keys](/organizations/api-keys/)
- [Receive organization webhooks](/organizations/webhooks/)
- [Organization roles and permissions](/organizations/roles-and-permissions/)

## API

- `GET /api/v1/orgs/{org_slug}/commerce/balance`: Get balance (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-balance)
- `GET /api/v1/orgs/{org_slug}/commerce/categories`: List categories (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-categories)
- `POST /api/v1/orgs/{org_slug}/commerce/categories`: A category; isinternal keeps it out of the Store API (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-categories)
- `GET /api/v1/orgs/{org_slug}/commerce/categories/{category_id}`: Get category (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-categories-category-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/categories/{category_id}`: Moving a category under itself or one of its children is a 409 (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-categories-category-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/categories/{category_id}`: A category with children answers 409; move or delete them first (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-categories-category-id)
- `GET /api/v1/orgs/{org_slug}/commerce/collections`: List collections (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-collections)
- `POST /api/v1/orgs/{org_slug}/commerce/collections`: Create collection (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-collections)
- `GET /api/v1/orgs/{org_slug}/commerce/collections/{collection_id}`: The collection and its product ids, in the collection's order (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-collections-collection-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/collections/{collection_id}`: Turning a manual collection smart drops the products added by hand (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-collections-collection-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/collections/{collection_id}`: The collection goes; its products stay (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-collections-collection-id)
- `PUT /api/v1/orgs/{org_slug}/commerce/collections/{collection_id}/products`: A manual collection's products become exactly these, in this order (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-put-api-v1-orgs-org-slug-commerce-collections-collection-id-products)
- `GET /api/v1/orgs/{org_slug}/commerce/customer-groups`: Every group, by name, with its customer count (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-customer-groups)
- `POST /api/v1/orgs/{org_slug}/commerce/customer-groups`: Create group (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-customer-groups)
- `GET /api/v1/orgs/{org_slug}/commerce/customer-groups/{group_id}`: The group; its customers are GET /commerce/customers?groupid= (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-customer-groups-group-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/customer-groups/{group_id}`: Update group (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-customer-groups-group-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/customer-groups/{group_id}`: Delete group (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-customer-groups-group-id)
- `PUT /api/v1/orgs/{org_slug}/commerce/customer-groups/{group_id}/customers`: Set group customers (https://docs.coritan.com/api/reference/organizations/customers/commerce/#op-put-api-v1-orgs-org-slug-commerce-customer-groups-group-id-customers)
- `GET /api/v1/orgs/{org_slug}/commerce/customers`: Newest first, with each customer's order count, spend and groups (https://docs.coritan.com/api/reference/organizations/customers/commerce/#op-get-api-v1-orgs-org-slug-commerce-customers)
- `GET /api/v1/orgs/{org_slug}/commerce/customers/{customer_id}`: The customer, their saved addresses, note, tags, groups and spend (https://docs.coritan.com/api/reference/organizations/customers/commerce/#op-get-api-v1-orgs-org-slug-commerce-customers-customer-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/customers/{customer_id}`: Note, tags, tax exemption, VAT id and its check; acceptsmarketing takes only false (https://docs.coritan.com/api/reference/organizations/customers/commerce/#op-patch-api-v1-orgs-org-slug-commerce-customers-customer-id)
- `GET /api/v1/orgs/{org_slug}/commerce/events`: What the store announced, newest first: the log behind its webhooks (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-events)
- `GET /api/v1/orgs/{org_slug}/commerce/fulfillment-providers`: List providers (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-fulfillment-providers)
- `POST /api/v1/orgs/{org_slug}/commerce/fulfillment-providers`: A provider (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-fulfillment-providers)
- `GET /api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}`: Get provider (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-fulfillment-providers-provider-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}`: Rename, enable or disable, move the endpoint, change settings (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-fulfillment-providers-provider-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}`: Refused while a fulfillment that has not shipped uses it; disable it instead (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-fulfillment-providers-provider-id)
- `GET /api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}/deliveries`: Requests sent (or waiting to be sent) to the provider, newest first (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-fulfillment-providers-provider-id-deliveries)
- `GET /api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}/events`: Events the provider sent, newest first, with what became of each (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-fulfillment-providers-provider-id-events)
- `POST /api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}/events`: One event from a fulfillment provider (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-fulfillment-providers-provider-id-events)
- `POST /api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}/rotate-secrets`: New outbound and inbound secrets, returned this once (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-fulfillment-providers-provider-id-rotate-secr)
- `POST /api/v1/orgs/{org_slug}/commerce/fulfillment-providers/{provider_id}/test`: Send a signed ping and report what the endpoint answered (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-fulfillment-providers-provider-id-test)
- `GET /api/v1/orgs/{org_slug}/commerce/gift-cards`: Newest first (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-gift-cards)
- `POST /api/v1/orgs/{org_slug}/commerce/gift-cards`: Issue a card (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-gift-cards)
- `GET /api/v1/orgs/{org_slug}/commerce/gift-cards/{gift_card_id}`: The card and its latest transactions, newest first (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-gift-cards-gift-card-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/gift-cards/{gift_card_id}`: Disable or re-enable a card, move its expiry, correct its recipient (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-gift-cards-gift-card-id)
- `POST /api/v1/orgs/{org_slug}/commerce/gift-cards/{gift_card_id}/adjust`: Move the balance by a signed amount; 409 giftcardbalancenegative below zero (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-gift-cards-gift-card-id-adjust)
- `GET /api/v1/orgs/{org_slug}/commerce/imports`: The store's imports, newest first, without their error lists; count counts every match (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-imports)
- `POST /api/v1/orgs/{org_slug}/commerce/imports`: Queue a Shopify product export for import and answer 202 with it (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-imports)
- `POST /api/v1/orgs/{org_slug}/commerce/imports/shopify-products`: Queue a Shopify product export for import and answer 202 with it (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-imports-shopify-products)
- `GET /api/v1/orgs/{org_slug}/commerce/imports/{import_id}`: One import: its status, progress, counts and every row it could not use (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-imports-import-id)
- `POST /api/v1/orgs/{org_slug}/commerce/inventory/levels/batch`: Set levels by sku (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-inventory-levels-batch)
- `GET /api/v1/orgs/{org_slug}/commerce/inventory-items`: Inventory items by SKU, with their levels (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-inventory-items)
- `GET /api/v1/orgs/{org_slug}/commerce/inventory-items/{item_id}`: Get item (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-inventory-items-item-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/inventory-items/{item_id}`: What customs and shipping need to know about the item (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-inventory-items-item-id)
- `PUT /api/v1/orgs/{org_slug}/commerce/inventory-items/{item_id}/levels/{location_id}`: Set level (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-put-api-v1-orgs-org-slug-commerce-inventory-items-item-id-levels-location-id)
- `GET /api/v1/orgs/{org_slug}/commerce/ledger`: Every movement of the store's money, newest first (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-ledger)
- `GET /api/v1/orgs/{org_slug}/commerce/merchant-profile`: Get profile (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-merchant-profile)
- `PUT /api/v1/orgs/{org_slug}/commerce/merchant-profile`: Update profile (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-put-api-v1-orgs-org-slug-commerce-merchant-profile)
- `POST /api/v1/orgs/{org_slug}/commerce/merchant-profile/submit`: Send the profile to platform review (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-merchant-profile-submit)
- `GET /api/v1/orgs/{org_slug}/commerce/orders`: Newest first; count is every order the filters match (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-orders)
- `GET /api/v1/orgs/{org_slug}/commerce/orders/{order_id}`: The order with its items, payments, refunds, fulfillments, documents and timeline (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-orders-order-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/orders/{order_id}`: Update order (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-orders-order-id)
- `POST /api/v1/orgs/{org_slug}/commerce/orders/{order_id}/cancel`: Refund what is left, give the stock back and end the order (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-orders-order-id-cancel)
- `GET /api/v1/orgs/{org_slug}/commerce/orders/{order_id}/documents`: The invoice and credit notes, with the issuing entity's details as issued (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-orders-order-id-documents)
- `GET /api/v1/orgs/{org_slug}/commerce/orders/{order_id}/fulfillments`: The order's fulfillments with their 3PL requests, and each item's progress (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-orders-order-id-fulfillments)
- `POST /api/v1/orgs/{org_slug}/commerce/orders/{order_id}/fulfillments`: Fulfill chosen units (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-orders-order-id-fulfillments)
- `POST /api/v1/orgs/{org_slug}/commerce/orders/{order_id}/fulfillments/{fulfillment_id}/cancel`: Cancel before it ships (409 alreadyshipped after) (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-orders-order-id-fulfillments-fulfillment-id-c)
- `POST /api/v1/orgs/{org_slug}/commerce/orders/{order_id}/fulfillments/{fulfillment_id}/deliver`: Delivered (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-orders-order-id-fulfillments-fulfillment-id-d)
- `POST /api/v1/orgs/{org_slug}/commerce/orders/{order_id}/fulfillments/{fulfillment_id}/dispatch`: Dispatch fulfillment (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-orders-order-id-fulfillments-fulfillment-id-d)
- `POST /api/v1/orgs/{org_slug}/commerce/orders/{order_id}/fulfillments/{fulfillment_id}/ship`: Shipped, with tracking (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-orders-order-id-fulfillments-fulfillment-id-s)
- `POST /api/v1/orgs/{org_slug}/commerce/orders/{order_id}/notes`: A note on the timeline; customervisible shows it on the shopper's order page (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-orders-order-id-notes)
- `POST /api/v1/orgs/{org_slug}/commerce/orders/{order_id}/refunds`: Refund an amount, or items and shipping, never more than is left (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-orders-order-id-refunds)
- `GET /api/v1/orgs/{org_slug}/commerce/price-lists`: List price lists (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-price-lists)
- `POST /api/v1/orgs/{org_slug}/commerce/price-lists`: Create price list (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-price-lists)
- `GET /api/v1/orgs/{org_slug}/commerce/price-lists/{price_list_id}`: Get price list (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-price-lists-price-list-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/price-lists/{price_list_id}`: Fields change when named; prices replaces the list's prices (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-price-lists-price-list-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/price-lists/{price_list_id}`: The list and its prices go; base prices are untouched (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-price-lists-price-list-id)
- `PUT /api/v1/orgs/{org_slug}/commerce/price-lists/{price_list_id}/prices`: Replace price list prices (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-put-api-v1-orgs-org-slug-commerce-price-lists-price-list-id-prices)
- `GET /api/v1/orgs/{org_slug}/commerce/product-tags`: The tags on live products, most used first: what a tag picker suggests (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-product-tags)
- `GET /api/v1/orgs/{org_slug}/commerce/products`: Products, most recently changed first (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-get-api-v1-orgs-org-slug-commerce-products)
- `POST /api/v1/orgs/{org_slug}/commerce/products`: Create product (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-post-api-v1-orgs-org-slug-commerce-products)
- `GET /api/v1/orgs/{org_slug}/commerce/products/{product_id}`: Get product (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-get-api-v1-orgs-org-slug-commerce-products-product-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/products/{product_id}`: Fields change when named (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-patch-api-v1-orgs-org-slug-commerce-products-product-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/products/{product_id}`: Archived and gone from every list and the Store API (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-delete-api-v1-orgs-org-slug-commerce-products-product-id)
- `POST /api/v1/orgs/{org_slug}/commerce/products/{product_id}/images`: An https image (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-post-api-v1-orgs-org-slug-commerce-products-product-id-images)
- `PATCH /api/v1/orgs/{org_slug}/commerce/products/{product_id}/images/{image_id}`: Update image (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-patch-api-v1-orgs-org-slug-commerce-products-product-id-images-image-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/products/{product_id}/images/{image_id}`: If it was the thumbnail, the next image takes its place (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-delete-api-v1-orgs-org-slug-commerce-products-product-id-images-image-id)
- `POST /api/v1/orgs/{org_slug}/commerce/products/{product_id}/options`: An option such as Size (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-post-api-v1-orgs-org-slug-commerce-products-product-id-options)
- `PATCH /api/v1/orgs/{org_slug}/commerce/products/{product_id}/options/{option_id}`: Update option (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-patch-api-v1-orgs-org-slug-commerce-products-product-id-options-option-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/products/{product_id}/options/{option_id}`: Delete option (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-delete-api-v1-orgs-org-slug-commerce-products-product-id-options-option-id)
- `POST /api/v1/orgs/{org_slug}/commerce/products/{product_id}/variants`: Create variant (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-post-api-v1-orgs-org-slug-commerce-products-product-id-variants)
- `PATCH /api/v1/orgs/{org_slug}/commerce/products/{product_id}/variants/{variant_id}`: Fields change when named; prices replaces the variant's base prices (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-patch-api-v1-orgs-org-slug-commerce-products-product-id-variants-variant-id)
- `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 (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-delete-api-v1-orgs-org-slug-commerce-products-product-id-variants-variant-id)
- `PUT /api/v1/orgs/{org_slug}/commerce/products/{product_id}/variants/{variant_id}/prices`: Replace variant prices (https://docs.coritan.com/api/reference/organizations/catalog-services/commerce/#op-put-api-v1-orgs-org-slug-commerce-products-product-id-variants-variant-id-prices)
- `GET /api/v1/orgs/{org_slug}/commerce/promotions`: Newest first (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-promotions)
- `POST /api/v1/orgs/{org_slug}/commerce/promotions`: A code (or an automatic promotion) (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-promotions)
- `GET /api/v1/orgs/{org_slug}/commerce/promotions/{promotion_id}`: The promotion and what it has given, per currency (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-promotions-promotion-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/promotions/{promotion_id}`: Update promotion (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-promotions-promotion-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/promotions/{promotion_id}`: A promotion no order used (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-promotions-promotion-id)
- `GET /api/v1/orgs/{org_slug}/commerce/publishable-keys`: List keys (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-publishable-keys)
- `POST /api/v1/orgs/{org_slug}/commerce/publishable-keys`: A key for a storefront (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-publishable-keys)
- `DELETE /api/v1/orgs/{org_slug}/commerce/publishable-keys/{key_id}`: Revoke key (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-publishable-keys-key-id)
- `GET /api/v1/orgs/{org_slug}/commerce/regions`: Every region of the store, disabled ones included (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-regions)
- `POST /api/v1/orgs/{org_slug}/commerce/regions`: A region (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-regions)
- `GET /api/v1/orgs/{org_slug}/commerce/regions/{region_id}`: Get region (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-regions-region-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/regions/{region_id}`: Fields change when named; countries replaces the region's (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-regions-region-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/regions/{region_id}`: The region, its countries and the prices set for it go (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-regions-region-id)
- `GET /api/v1/orgs/{org_slug}/commerce/sales-channels`: List channels (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-sales-channels)
- `POST /api/v1/orgs/{org_slug}/commerce/sales-channels`: Create channel (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-sales-channels)
- `PATCH /api/v1/orgs/{org_slug}/commerce/sales-channels/{channel_id}`: Update channel (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-sales-channels-channel-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/sales-channels/{channel_id}`: Delete channel (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-sales-channels-channel-id)
- `GET /api/v1/orgs/{org_slug}/commerce/sales-channels/{channel_id}/stock-locations`: The locations a channel sells from (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-sales-channels-channel-id-stock-locations)
- `PUT /api/v1/orgs/{org_slug}/commerce/sales-channels/{channel_id}/stock-locations`: The channel sells from exactly these locations; an empty list means every enabled location (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-put-api-v1-orgs-org-slug-commerce-sales-channels-channel-id-stock-locations)
- `GET /api/v1/orgs/{org_slug}/commerce/shipping-options`: List options (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-shipping-options)
- `POST /api/v1/orgs/{org_slug}/commerce/shipping-options`: An option in a zone for a profile (the default one when profileid is left out) (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-shipping-options)
- `GET /api/v1/orgs/{org_slug}/commerce/shipping-options/{option_id}`: Get option (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-shipping-options-option-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/shipping-options/{option_id}`: Update option (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-shipping-options-option-id)
- `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 (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-shipping-options-option-id)
- `GET /api/v1/orgs/{org_slug}/commerce/shipping-profiles`: Every profile, the default first (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-shipping-profiles)
- `POST /api/v1/orgs/{org_slug}/commerce/shipping-profiles`: Create profile (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-shipping-profiles)
- `PATCH /api/v1/orgs/{org_slug}/commerce/shipping-profiles/{profile_id}`: Update profile (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-shipping-profiles-profile-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/shipping-profiles/{profile_id}`: A profile no product and no option uses (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-shipping-profiles-profile-id)
- `GET /api/v1/orgs/{org_slug}/commerce/shipping-zones`: Every zone, with how many options each holds (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-shipping-zones)
- `POST /api/v1/orgs/{org_slug}/commerce/shipping-zones`: Create zone (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-shipping-zones)
- `GET /api/v1/orgs/{org_slug}/commerce/shipping-zones/{zone_id}`: Get zone (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-shipping-zones-zone-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/shipping-zones/{zone_id}`: Update zone (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-shipping-zones-zone-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/shipping-zones/{zone_id}`: The zone and its options (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-shipping-zones-zone-id)
- `GET /api/v1/orgs/{org_slug}/commerce/stock-locations`: Every location, the default first, with the channels that sell from it (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-stock-locations)
- `POST /api/v1/orgs/{org_slug}/commerce/stock-locations`: A warehouse, shop or 3PL site (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-stock-locations)
- `GET /api/v1/orgs/{org_slug}/commerce/stock-locations/{location_id}`: Get location (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-stock-locations-location-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/stock-locations/{location_id}`: Isdefault: true moves the default here (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-stock-locations-location-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/stock-locations/{location_id}`: Delete location (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-stock-locations-location-id)
- `GET /api/v1/orgs/{org_slug}/commerce/store`: The store, its sales channels, and where its merchant review stands (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-store)
- `PATCH /api/v1/orgs/{org_slug}/commerce/store`: Update store (https://docs.coritan.com/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-store)
