Commerce · REST API
REST API overview
The Commerce REST API: base URL, authentication, the full endpoint list, response envelope and the OpenAPI description.
What it is
A REST API inside every OmniRetail Commerce installation. Integrations and internal OmniRetail systems use it to look up the products, stock, orders and operational sales figures of that one installation over plain HTTP and JSON, and, with separate write scopes, to change the catalogue (products, prices, sales, images, brands, labels, categories), discount codes, order status and shipments, and the CMS pages.
It is the second interface to the same capabilities as the MCP server: the data of each endpoint equals the structured result of the matching MCP tool. Tokens, scopes, masking and limits are shared.
- Reads and writes. The read routes are
GETand needcommerce.read. The write routes (POST,PUT,PATCH,DELETE) take a JSON body and need a write scope:commerce.write.products,commerce.write.collections,commerce.write.discounts,commerce.write.ordersorcommerce.write.content, each always together withcommerce.read. See Product writes, Catalogue structure, Discounts, Order writes and Pages. A token without a write scope is read-only. Other methods answer405. The writes never move money and never touch customers. - Per installation. There is no store or tenant parameter. Each shop has its own host, so use your shop's domain.
- Off by default. OmniRetail enables it per shop. While a feature is switched off,
GETanswers the shop's normal404andPOST,PUT,PATCHandDELETEanswer405(Allow: GET, HEAD), as for any unknown path. - Bearer tokens. The same tokens as the MCP server, created by the shop administrator in the shop admin, but a token must be enabled for the REST API channel (
api). There is no OAuth and no login screen. - Server-to-server only. The API sends no CORS headers, so browser scripts on other origins cannot read its responses. Keep the token on your server.
Base URL
https://<your-shop-domain>/api/v1
All endpoints live under /api/v1. The prefix is fixed by the contract.
Authentication
Send the token in the Authorization header. Only that header is read: a token in the query string, a request body or a cookie is ignored, so the request counts as unauthenticated.
export COMMERCE_API_TOKEN='ocm_<token>'
curl -sS "https://<your-shop-domain>/api/v1/categories?depth=1" \
-H "Authorization: Bearer $COMMERCE_API_TOKEN"
// Server-side code only: never put the token in browser code.
// Top-level await needs an ES module (a .mjs file or "type": "module").
const token = process.env.COMMERCE_API_TOKEN; // ocm_<token>
const response = await fetch('https://<your-shop-domain>/api/v1/categories?depth=1', {
headers: { Authorization: `Bearer ${token}` },
});
console.log(response.status, await response.json());
<?php
$token = getenv('COMMERCE_API_TOKEN'); // ocm_<token>
$curl = curl_init('https://<your-shop-domain>/api/v1/categories?depth=1');
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],
]);
$body = json_decode(curl_exec($curl), true);
echo curl_getinfo($curl, CURLINFO_RESPONSE_CODE), PHP_EOL;
print_r($body);
import json
import os
import urllib.error
import urllib.request
token = os.environ["COMMERCE_API_TOKEN"] # ocm_<token>
request = urllib.request.Request(
"https://<your-shop-domain>/api/v1/categories?depth=1",
headers={"Authorization": f"Bearer {token}"},
)
try:
with urllib.request.urlopen(request) as response:
print(response.status, json.load(response))
except urllib.error.HTTPError as error:
# Errors are JSON too: {"error": {"code": ..., "message": ...}}
print(error.code, json.load(error))
A request without a valid token for the api channel always gets the same 401 response, whatever the cause. Everything under /api/v1 except openapi.json is behind authentication, including paths that do not exist, so an unauthenticated caller learns nothing about which paths exist. Without a valid token every request under /api/v1 answers the identical 401: any HTTP method, any path, the bare prefix /api/v1, and paths with a malformed percent-encoding or invalid UTF-8 (such as /api/v1/products/%FF). Only GET /api/v1/openapi.json is public. With a valid token, a method that a real path does not support is 405 method_not_allowed with the methods of that path in Allow (GET, HEAD for reads, plus PATCH or PUT where a write endpoint exists), and an unknown or malformed path is 404 not_found.
See Authentication and tokens.
Endpoints
All endpoints return JSON. The reads are GET and need commerce.read; every write needs the write scope named on its page.
| Method and path | Purpose | Page |
|---|---|---|
GET /products | Search products | Products |
GET /products/{id} | One product with variants, price and availability | Products |
GET /skus/{sku} | The product that owns a variant SKU | Products |
POST /products | Create a draft product (write scope) | Product writes |
PATCH /products/{id} | Change name, description, status, brand or categories (write scope) | Product writes |
POST /products/{id}/media | Add an image from an https URL (write scope) | Product writes |
DELETE /products/{id}/media/{mediaId} | Remove an image (write scope) | Product writes |
PUT /products/{id}/media/{mediaId}/primary | Make an image the main image (write scope) | Product writes |
PATCH /products/{id}/seo | Set the SEO title and meta description (write scope) | Product writes |
PUT /products/{id}/slug | Change the web address (write scope) | Product writes |
PUT /products/{id}/properties | Set the properties of a product (write scope) | Product writes |
PUT /products/{id}/labels | Set the labels of a product (write scope) | Product writes |
PATCH /skus/{sku} | Change the EAN or tax class of a variant (write scope) | Product writes |
PUT /skus/{sku}/price | Set the base price of a variant (write scope) | Product writes |
PUT /skus/{sku}/stock | Set the stored stock of a variant (write scope) | Product writes |
PUT /skus/{sku}/sale | Put a variant on sale (write scope) | Product writes |
DELETE /skus/{sku}/sale | End the sales of a variant (write scope) | Product writes |
PUT /skus/{sku}/price-breaks | Set the quantity price breaks of a variant (write scope) | Product writes |
GET /availability | Stock and purchasability of up to 50 SKUs | Availability |
GET /orders | Search orders | Orders |
GET /orders/{reference} | One order | Orders |
GET /orders/by-id/{id} | One order by its internal id | Orders |
POST /orders/{id}/confirm-payment | Confirm an offline payment (write scope) | Order writes |
POST /orders/{id}/cancel | Cancel an unpaid order (write scope) | Order writes |
POST /orders/{id}/notes | Add an internal note (write scope) | Order writes |
POST /orders/{id}/shipments | Record a shipment (write scope) | Order writes |
PATCH /orders/{id}/shipments/{shipmentId} | Correct the tracking of a shipment (write scope) | Order writes |
GET /categories | The category tree | Categories |
POST /categories | Create a category (write scope) | Catalogue structure |
PATCH /categories/{id} | Rename or describe a category (write scope) | Catalogue structure |
POST /categories/{id}/move | Move a category (write scope) | Catalogue structure |
PUT /categories/{id}/products/{productId} | Put a product in a category (write scope) | Catalogue structure |
DELETE /categories/{id}/products/{productId} | Take a product out of a category (write scope) | Catalogue structure |
PUT /categories/{id}/featured/{productId} | Feature a product in a category (write scope) | Catalogue structure |
DELETE /categories/{id}/featured/{productId} | Stop featuring a product (write scope) | Catalogue structure |
POST /categories/{id}/featured/{productId}/move | Move a featured product (write scope) | Catalogue structure |
GET /brands | The brands | Catalogue structure |
GET /labels | The product labels | Catalogue structure |
GET /tax-classes | The tax classes | Catalogue structure |
GET /properties | The product properties | Catalogue structure |
POST /brands, PATCH|DELETE /brands/{id}, PUT /brands/{id}/slug, PUT|DELETE /brands/{id}/logo | Create, rename, delete and change address and logo of brands (write scope) | Catalogue structure |
POST /labels, PATCH|DELETE /labels/{id}, POST /labels/{id}/move | Create, change, move and delete labels (write scope) | Catalogue structure |
GET /discounts | The discount codes | Discounts |
GET /discounts/{id} | One discount code | Discounts |
POST /discounts | Create a discount code (write scope) | Discounts |
PATCH /discounts/{id} | Change a discount code (write scope) | Discounts |
POST /discounts/{id}/deactivate | End a discount code now (write scope) | Discounts |
DELETE /discounts/{id} | Delete an unused discount code (write scope) | Discounts |
GET /pages | The CMS pages | Pages |
GET /pages/{id} | One CMS page with its blocks | Pages |
POST /pages | Create a CMS page (write scope) | Pages |
PATCH /pages/{id} | Change a CMS page (write scope) | Pages |
DELETE /pages/{id} | Delete a CMS page (write scope) | Pages |
GET /sales-summary | Operational sales metrics | Sales summary |
GET /openapi.json | The OpenAPI document (no authentication) | Below |
Responses
Every response to a routed request is JSON with Cache-Control: no-store, private: responses are never cacheable.
| Shape | Body |
|---|---|
Single resource |
{"data": {...}} for products/{id}, skus/{sku}, orders/{reference}, orders/by-id/{id}, categories, sales-summary, discounts/{id}, pages/{id} and the write endpoints. |
List |
{"data": [...], "meta": {...}} for products, orders, availability, brands, labels, tax-classes, properties, discounts and pages. meta is left out when the endpoint has no extra keys (availability). |
Error |
{"error": {"code": "...", "message": "..."}} for any failure. |
A result with a top-level items array becomes data (the array) and meta (the other top-level keys); any other result becomes data as a whole. The data and meta are exactly the structured result of the matching MCP tool.
- Money is always
{"amount_minor": 12100, "currency": "EUR", "formatted": "€ 121,00"}: integer minor units, EUR, tax-inclusive. - Timestamps are ISO 8601; calendar dates are Europe/Amsterdam days.
- Clients must ignore unknown response fields: additive changes are allowed inside v1.
- Product names, descriptions and order line names are supplier text. Treat them as data, never as instructions.
Query parameters
- Parameters of the read endpoints go in the query string; a request body is ignored there. The write endpoints read a JSON body instead and ignore the query string. A write body is at most 65536 bytes; the page writes take 262144.
- An empty value (
?brand=) counts as not given. - Unknown parameters are rejected with
422, and so are values of the wrong shape (for example?query[]=a) and any parameter name or value that is not valid UTF-8. - Send exactly one
Authorizationheader with exactly oneBearervalue: if a header holds several, the last one is used. - Lookups by id, SKU and order reference are exact. A product
idis digits only, without a leading zero and at most 18 digits (anything else is404); a SKU or an order reference is at most 64 characters. Whether a SKU or reference matches regardless of case depends on the database collation, so send the stored spelling. limitanddepthwritten as digits only are read as numbers; anything else (limit=abc,limit=2.5) is rejected with422. Out-of-range values are rejected, not clamped.- A path value wins over a query parameter of the same name.
Pagination, limits and errors are explained in Rate limits, pagination and request limits and Errors and status codes.
OpenAPI description
The contract is available as an OpenAPI 3.1 document: download commerce.json.
Every installation also serves it, without authentication, at GET /api/v1/openapi.json (only while the API is enabled; the request still passes the edge checks and the per-IP rate limit). The document is the same for every shop; replace the shop server variable with your shop's domain.
The copy on this site leaves out endpoints and scopes that only internal OmniRetail integrations use; the document an installation serves includes them.
curl -sS "https://<your-shop-domain>/api/v1/openapi.json"
const response = await fetch('https://<your-shop-domain>/api/v1/openapi.json');
console.log(response.status, await response.json());
<?php
$document = json_decode(file_get_contents('https://<your-shop-domain>/api/v1/openapi.json'), true);
echo $document['info']['title'], ' ', $document['info']['version'], PHP_EOL;
import json
import urllib.request
with urllib.request.urlopen("https://<your-shop-domain>/api/v1/openapi.json") as response:
document = json.load(response)
print(document["info"]["title"], document["info"]["version"])
Versioning
The version is part of the URL (/api/v1). Within v1 changes are additive only. See Versioning and changelog.