Developer documentation
RM REST API
Connect your ERP, your point of sale or your own system to an RM store: catalog, prices, stock, customers and orders, with webhooks so you find out instantly.
Introduction
The API is REST over HTTPS, with UTF-8 JSON. Each store has its own keys and a key only sees and modifies its own store's data. There are 111 operations grouped by area: catalog, inventory, prices, customers, orders and integrations.
API base URL ({base_url} in this documentation; includes /api/v1):
- Public server:
https://retail-s.online/api/v1 - Local development:
http://127.0.0.1:8000/api/v1
To check that everything works:
export RM_BASE_URL="https://retail-s.online/api/v1"
export RM_API_KEY="rk_..." # your key; never commit it to a repository
curl -s "$RM_BASE_URL/" -H "Authorization: Bearer $RM_API_KEY"
It responds with the store, the key name and its permissions:
{
"api_version": "1",
"tenant": {"name": "My store", "slug": "mine", "default_currency": "USD", "prices_include_tax": false},
"api_key": "ERP connector",
"scopes": ["read_products", "write_products", "read_inventory"]
}
Local environments with their own certificate (for example the test server on the local network) require the client to trust that CA: with curl, --cacert ca.crt. The public server uses a regular certificate.
Error message texts (for example the detail field) are currently returned in Spanish. Rely on the HTTP status code and on field names, not on the message text.
Authentication and scopes
Generate a key in the back office (Settings → API keys) and send it with every request:
Authorization: Bearer rk_1a2b3c4d_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- No key, or an invalid or deactivated key:
401. A back office session does not work for the API. - The key is shown only once, when it is created. Keep it in an environment variable or a secrets manager.
Each key has per-area permissions, chosen when it is created: no access, read, or read and write. A GET requires read_<area>; POST, PUT, PATCH and DELETE require write_<area>. Write includes read. Without the permission: 403.
| Area | Scopes | Resources |
|---|---|---|
| Catalog | read_products write_products | products, variants, images, categories, brands, collections, attributes, templates, modifiers, translations, bulk creation and import |
| Inventory | read_inventory write_inventory | stock levels, locations |
| Prices | read_prices write_prices | prices, price lists |
| Customers | read_customers write_customers | customers, customer groups |
| Orders | read_orders write_orders | orders, status changes, payments, returns, refunds, carriers, payment methods |
| Integrations | read_integrations write_integrations | connections, sync runs, external references |
Give each connector only the permissions it uses (for example, read_orders without write_orders if it only reads orders).
Conventions
- Identity. Categories, brands, collections, attributes, locations, price lists, groups and products are identified by code (as in BC), stored in uppercase:
ply-001andPLY-001are the same. Variants are identified by SKU. Customers, orders, lines, returns, images, connections and external references are identified by their publicid: a UUID you must store as a string. The API never exposes internal IDs. - Upsert.
PUT /<resource>/<code>/creates (201) or replaces (200).PATCHupdates only the fields sent and returns404if it does not exist. - Declarative. The lists
options,variants(withvariants_mode: "exact"),addresses,attributes,modifiersandhoursleave the record exactly as you send them. - Decimals as strings (
"199.0000") so no precision is lost. Dates in ISO 8601. - Cursor pagination.
?limit=from 1 to 500 (100 by default). The response is{"next": url, "results": [...]}; request thenextURL as is until it isnull. Records come out in creation order, so nothing is repeated or skipped while you read. There is nocountorprevious, andoffsetreturns400. - Incremental sync.
?updated_since=2026-10-14T00:00:00Zon lists; on orders also?placed_since=. - Safe retries.
POST /orders/,/pay/,/returns/and/refunds/accept theIdempotency-Keyheader (up to 100 characters). Same key and same data: the operation is not repeated. Same key with different data:409.
The business rules of each resource (exact variants, hours, inventory with an ERP, refunds…) are in the full reference.
Resources
Each operation shows its permission and a curl example (it uses the variables from the introduction's example). Open one to see it. The full parameters, bodies and responses are in the OpenAPI.
Connection
Test the key and see its permissions.
GET / Test connection
Returns the store, the key name and its permissions. Any valid key works, whatever its scopes.
curl -s -X GET "$RM_BASE_URL/" \
-H "Authorization: Bearer $RM_API_KEY"
Catalog
Products, variants, images, categories, brands, collections, attributes, templates, modifiers and translations. Permission `products`.
GET /categories/ List categories read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/categories/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /categories/ Create the category write_products
The body carries `code`. `409`/`400` if the code already exists. Permission: `write_products`.
curl -s -X POST "$RM_BASE_URL/categories/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "EXAMPLE",
"name": "T-shirts",
"parent": "CLOTHING",
"is_active": true
}'
GET /categories/{code}/ Get the category read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/categories/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
PUT /categories/{code}/ Create or replace the category (upsert) write_products
`201` if it creates it, `200` if it replaces it. The code in the path takes precedence over the one in the body. Permission: `write_products`.
curl -s -X PUT "$RM_BASE_URL/categories/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "T-shirts",
"parent": "CLOTHING",
"is_active": true
}'
PATCH /categories/{code}/ Update fields of the category write_products
Only the fields sent; `404` if it does not exist. Permission: `write_products`.
curl -s -X PATCH "$RM_BASE_URL/categories/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "T-shirts"
}'
DELETE /categories/{code}/ Delete the category write_products
`400` if other records depend on it. Permission: `write_products`.
curl -s -X DELETE "$RM_BASE_URL/categories/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /variant-templates/ List variant-templates read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/variant-templates/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /variant-templates/ Create the variant template write_products
The body carries `code`. `409`/`400` if the code already exists. Permission: `write_products`.
curl -s -X POST "$RM_BASE_URL/variant-templates/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "EXAMPLE",
"name": "Clothing",
"options": [
{
"name": "Size",
"values": [
"S",
"M",
"L"
]
}
]
}'
GET /variant-templates/{code}/ Get the variant template read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/variant-templates/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
PUT /variant-templates/{code}/ Create or replace the variant template (upsert) write_products
`201` if it creates it, `200` if it replaces it. The code in the path takes precedence over the one in the body. Permission: `write_products`.
curl -s -X PUT "$RM_BASE_URL/variant-templates/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Clothing",
"options": [
{
"name": "Size",
"values": [
"S",
"M",
"L"
]
}
]
}'
PATCH /variant-templates/{code}/ Update fields of the variant template write_products
Only the fields sent; `404` if it does not exist. Permission: `write_products`.
curl -s -X PATCH "$RM_BASE_URL/variant-templates/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Clothing"
}'
DELETE /variant-templates/{code}/ Delete the variant template write_products
`400` if other records depend on it. Permission: `write_products`.
curl -s -X DELETE "$RM_BASE_URL/variant-templates/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /brands/ List brands read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/brands/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /brands/ Create the brand write_products
The body carries `code`. `409`/`400` if the code already exists. Permission: `write_products`.
curl -s -X POST "$RM_BASE_URL/brands/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "EXAMPLE",
"name": "Example Brand",
"is_active": true
}'
GET /brands/{code}/ Get the brand read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/brands/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
PUT /brands/{code}/ Create or replace the brand (upsert) write_products
`201` if it creates it, `200` if it replaces it. The code in the path takes precedence over the one in the body. Permission: `write_products`.
curl -s -X PUT "$RM_BASE_URL/brands/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Brand",
"is_active": true
}'
PATCH /brands/{code}/ Update fields of the brand write_products
Only the fields sent; `404` if it does not exist. Permission: `write_products`.
curl -s -X PATCH "$RM_BASE_URL/brands/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Brand"
}'
DELETE /brands/{code}/ Delete the brand write_products
`400` if other records depend on it. Permission: `write_products`.
curl -s -X DELETE "$RM_BASE_URL/brands/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /collections/ List collections read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/collections/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /collections/ Create the collection write_products
The body carries `code`. `409`/`400` if the code already exists. Permission: `write_products`.
curl -s -X POST "$RM_BASE_URL/collections/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "EXAMPLE",
"name": "New arrivals",
"is_active": true,
"featured": false,
"products": [
"PLY-001"
]
}'
GET /collections/{code}/ Get the collection read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/collections/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
PUT /collections/{code}/ Create or replace the collection (upsert) write_products
`201` if it creates it, `200` if it replaces it. The code in the path takes precedence over the one in the body. Permission: `write_products`.
curl -s -X PUT "$RM_BASE_URL/collections/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "New arrivals",
"is_active": true,
"featured": false,
"products": [
"PLY-001"
]
}'
PATCH /collections/{code}/ Update fields of the collection write_products
Only the fields sent; `404` if it does not exist. Permission: `write_products`.
curl -s -X PATCH "$RM_BASE_URL/collections/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "New arrivals"
}'
DELETE /collections/{code}/ Delete the collection write_products
`400` if other records depend on it. Permission: `write_products`.
curl -s -X DELETE "$RM_BASE_URL/collections/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /modifier-groups/ List modifier-groups read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/modifier-groups/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /modifier-groups/ Create the modifier group write_products
The body carries `code`. `409`/`400` if the code already exists. Permission: `write_products`.
curl -s -X POST "$RM_BASE_URL/modifier-groups/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "EXAMPLE",
"name": "Extras",
"min_select": 0,
"max_select": 2,
"modifiers": [
{
"code": "CHEESE",
"name": "Cheese",
"price": "0.75"
},
{
"code": "BACON",
"name": "Bacon",
"price": "1.25"
}
]
}'
GET /modifier-groups/{code}/ Get the modifier group read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/modifier-groups/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
PUT /modifier-groups/{code}/ Create or replace the modifier group (upsert) write_products
`201` if it creates it, `200` if it replaces it. The code in the path takes precedence over the one in the body. Permission: `write_products`.
curl -s -X PUT "$RM_BASE_URL/modifier-groups/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Extras",
"min_select": 0,
"max_select": 2,
"modifiers": [
{
"code": "CHEESE",
"name": "Cheese",
"price": "0.75"
},
{
"code": "BACON",
"name": "Bacon",
"price": "1.25"
}
]
}'
PATCH /modifier-groups/{code}/ Update fields of the modifier group write_products
Only the fields sent; `404` if it does not exist. Permission: `write_products`.
curl -s -X PATCH "$RM_BASE_URL/modifier-groups/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Extras"
}'
DELETE /modifier-groups/{code}/ Delete the modifier group write_products
`400` if other records depend on it. Permission: `write_products`.
curl -s -X DELETE "$RM_BASE_URL/modifier-groups/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /attributes/ List attributes read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/attributes/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /attributes/ Create the attribute write_products
The body carries `code`. `409`/`400` if the code already exists. Permission: `write_products`.
curl -s -X POST "$RM_BASE_URL/attributes/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "EXAMPLE",
"name": "Material",
"type": "option",
"scope": "product",
"options": [
"Cotton",
"Polyester"
],
"is_filterable": true
}'
GET /attributes/{code}/ Get the attribute read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/attributes/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
PUT /attributes/{code}/ Create or replace the attribute (upsert) write_products
`201` if it creates it, `200` if it replaces it. The code in the path takes precedence over the one in the body. Permission: `write_products`.
curl -s -X PUT "$RM_BASE_URL/attributes/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Material",
"type": "option",
"scope": "product",
"options": [
"Cotton",
"Polyester"
],
"is_filterable": true
}'
PATCH /attributes/{code}/ Update fields of the attribute write_products
Only the fields sent; `404` if it does not exist. Permission: `write_products`.
curl -s -X PATCH "$RM_BASE_URL/attributes/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Material"
}'
DELETE /attributes/{code}/ Delete the attribute write_products
`400` if other records depend on it. Permission: `write_products`.
curl -s -X DELETE "$RM_BASE_URL/attributes/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /products/ List products read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/products/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /products/ Create product write_products
Permission: `write_products`.
curl -s -X POST "$RM_BASE_URL/products/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "PLY-001",
"name": "Basic T-shirt",
"status": "active",
"category": "TSHIRTS",
"short_description": "100 % cotton",
"base_unit": "PC",
"tax_exempt": false,
"track_inventory": true,
"options": [
{
"name": "Color",
"values": [
"Black",
"White"
]
},
{
"name": "Size",
"values": [
"S",
"M",
"L"
]
}
],
"variants": [
{
"code": "NEGRO-CH",
"sku": "PLY-001-N-CH",
"barcode": "7501234567890"
}
],
"attributes": {
"MATERIAL": "Cotton"
}
}'
GET /products/{code}/ Get product read_products
Includes variants with their options, stock by location and prices, images and attributes. Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/products/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
PUT /products/{code}/ Create or replace product (upsert) write_products
`201`/`200`. `options` and `attributes` are declarative; it is applied in full or not at all. See `variants_mode: exact` in `docs/api.en.md`. Permission: `write_products`.
curl -s -X PUT "$RM_BASE_URL/products/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Basic T-shirt",
"status": "active",
"category": "TSHIRTS",
"short_description": "100 % cotton",
"base_unit": "PC",
"tax_exempt": false,
"track_inventory": true,
"options": [
{
"name": "Color",
"values": [
"Black",
"White"
]
},
{
"name": "Size",
"values": [
"S",
"M",
"L"
]
}
],
"variants": [
{
"code": "NEGRO-CH",
"sku": "PLY-001-N-CH",
"barcode": "7501234567890"
}
],
"attributes": {
"MATERIAL": "Cotton"
}
}'
PATCH /products/{code}/ Update product fields write_products
Permission: `write_products`.
curl -s -X PATCH "$RM_BASE_URL/products/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "archived"
}'
DELETE /products/{code}/ Delete product write_products
Permission: `write_products`.
curl -s -X DELETE "$RM_BASE_URL/products/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /products/{code}/duplicate/ Duplicate product write_products
Permission: `write_products`.
curl -s -X POST "$RM_BASE_URL/products/{code}/duplicate/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "PLY-001-COPY",
"name": "Basic T-shirt (copy)",
"copy_images": true,
"copy_prices": true
}'
POST /products/{code}/images/ Add or replace image write_products
`multipart/form-data` with `image`, or JSON with `url` (https, not downloaded). A repeated `external_key` replaces the image (`200`). Permission: `write_products`.
curl -s -X POST "$RM_BASE_URL/products/{code}/images/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://cdn.example.com/tshirt.jpg",
"external_key": "LS-1",
"alt_text": "Black T-shirt, front view",
"variant_skus": [
"PLY-001-N-CH"
]
}'
DELETE /products/{code}/images/ Delete image by external key write_products
Permission: `write_products`.
curl -s -X DELETE "$RM_BASE_URL/products/{code}/images/" \
-H "Authorization: Bearer $RM_API_KEY"
DELETE /products/{code}/images/{image_id}/ Delete image by id write_products
Permission: `write_products`.
curl -s -X DELETE "$RM_BASE_URL/products/{code}/images/{image_id}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /variants/ List variants read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/variants/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /variants/{sku}/ Get variant read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/variants/{sku}/" \
-H "Authorization: Bearer $RM_API_KEY"
PATCH /variants/{sku}/ Update variant write_products
Only `sku`, `barcode`, `weight`, `is_active` and `attributes`; combinations come from the product options. Permission: `write_products`.
curl -s -X PATCH "$RM_BASE_URL/variants/{sku}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"barcode": "7501234567890",
"is_active": true
}'
DELETE /variants/{sku}/ Delete variant write_products
`409` if it has orders (deactivate it with `is_active: false`) or if it is the product's only variant. Permission: `write_products`.
curl -s -X DELETE "$RM_BASE_URL/variants/{sku}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /translations/ Get translations read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/translations/" \
-H "Authorization: Bearer $RM_API_KEY"
PUT /translations/ Save translations write_products
The language must be enabled as an extra language in the store. An empty value deletes the translation. Permission: `write_products`.
curl -s -X PUT "$RM_BASE_URL/translations/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"object_type": "product",
"object_key": "PLY-001",
"language": "en",
"fields": {
"name": "Basic T-shirt"
}
}'
Bulk catalog
Bulk creation and NDJSON import of products. Permission `products`.
POST /products/bulk/ Bulk create products (up to 500) write_products
Upsert by code, processed by the worker. One bad product does not fail the others. Permission: `write_products`.
curl -s -X POST "$RM_BASE_URL/products/bulk/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"code": "PLY-001",
"name": "Basic T-shirt"
},
{
"code": "PLY-002",
"name": "Printed T-shirt"
}
]
}'
GET /products/bulk/{id}/ Status and result of a bulk creation read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/products/bulk/{id}/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /products/import/ Import NDJSON catalog (create only) write_products
`multipart/form-data` with `file` (NDJSON, up to 256 MB). Meant for first loads of tens or hundreds of thousands of products. Permission: `write_products`.
curl -s -X POST "$RM_BASE_URL/products/import/" \
-H "Authorization: Bearer $RM_API_KEY" \
-F "file=@catalog.ndjson"
GET /products/import/{id}/ Status of an import read_products
Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/products/import/{id}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /products/import/{id}/rows/ Full result of an import (NDJSON) read_products
One row per line (`application/x-ndjson`), in order and unpaginated. Permission: `read_products`.
curl -s -X GET "$RM_BASE_URL/products/import/{id}/rows/" \
-H "Authorization: Bearer $RM_API_KEY"
Inventory
Stock levels and locations. Permission `inventory`.
GET /locations/ List locations read_inventory
Permission: `read_inventory`.
curl -s -X GET "$RM_BASE_URL/locations/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /locations/ Create the location write_inventory
The body carries `code`. `409`/`400` if the code already exists. Permission: `write_inventory`.
curl -s -X POST "$RM_BASE_URL/locations/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "EXAMPLE",
"name": "Distribution center",
"kind": "warehouse",
"allows_pickup": false,
"ships_orders": true,
"hours": [
{
"weekday": 0,
"opens": "09:00",
"closes": "18:00"
}
]
}'
GET /locations/{code}/ Get the location read_inventory
Permission: `read_inventory`.
curl -s -X GET "$RM_BASE_URL/locations/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
PUT /locations/{code}/ Create or replace the location (upsert) write_inventory
`201` if it creates it, `200` if it replaces it. The code in the path takes precedence over the one in the body. Permission: `write_inventory`.
curl -s -X PUT "$RM_BASE_URL/locations/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Distribution center",
"kind": "warehouse",
"allows_pickup": false,
"ships_orders": true,
"hours": [
{
"weekday": 0,
"opens": "09:00",
"closes": "18:00"
}
]
}'
PATCH /locations/{code}/ Update fields of the location write_inventory
Only the fields sent; `404` if it does not exist. Permission: `write_inventory`.
curl -s -X PATCH "$RM_BASE_URL/locations/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Distribution center"
}'
DELETE /locations/{code}/ Delete the location write_inventory
`400` if other records depend on it. Permission: `write_inventory`.
curl -s -X DELETE "$RM_BASE_URL/locations/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /inventory/ Get stock levels read_inventory
Permission: `read_inventory`.
curl -s -X GET "$RM_BASE_URL/inventory/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /inventory/bulk/ Set stock levels in bulk (absolute) write_inventory
`on_hand` is the ERP's absolute stock, not an adjustment. Up to 1000 rows. All or nothing, unless `partial=true`. Permission: `write_inventory`.
curl -s -X POST "$RM_BASE_URL/inventory/bulk/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"sku": "PLY-001-N-CH",
"location": "DC",
"on_hand": "12"
},
{
"sku": "PLY-001-N-CH",
"location": "TDA-CENTRO",
"on_hand": "3",
"reference": "ERP 2026-10-14"
}
]
}'
Prices
Prices and price lists. Permission `prices`.
GET /price-lists/ List price-lists read_prices
Permission: `read_prices`.
curl -s -X GET "$RM_BASE_URL/price-lists/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /price-lists/ Create the price list write_prices
The body carries `code`. `409`/`400` if the code already exists. Permission: `write_prices`.
curl -s -X POST "$RM_BASE_URL/price-lists/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "EXAMPLE",
"name": "Wholesale",
"currency": "USD",
"prices_include_tax": false,
"priority": 10,
"customer_groups": [
"WHOLESALE"
]
}'
GET /price-lists/{code}/ Get the price list read_prices
Permission: `read_prices`.
curl -s -X GET "$RM_BASE_URL/price-lists/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
PUT /price-lists/{code}/ Create or replace the price list (upsert) write_prices
`201` if it creates it, `200` if it replaces it. The code in the path takes precedence over the one in the body. Permission: `write_prices`.
curl -s -X PUT "$RM_BASE_URL/price-lists/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Wholesale",
"currency": "USD",
"prices_include_tax": false,
"priority": 10,
"customer_groups": [
"WHOLESALE"
]
}'
PATCH /price-lists/{code}/ Update fields of the price list write_prices
Only the fields sent; `404` if it does not exist. Permission: `write_prices`.
curl -s -X PATCH "$RM_BASE_URL/price-lists/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Wholesale"
}'
DELETE /price-lists/{code}/ Delete the price list write_prices
`400` if other records depend on it. Permission: `write_prices`.
curl -s -X DELETE "$RM_BASE_URL/price-lists/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /prices/ Get prices read_prices
Permission: `read_prices`.
curl -s -X GET "$RM_BASE_URL/prices/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /prices/bulk/ Set prices in bulk write_prices
Up to 1000 rows; `price: null` deletes the line. All or nothing, unless `partial=true`. Permission: `write_prices`.
curl -s -X POST "$RM_BASE_URL/prices/bulk/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"price_list": "GENERAL",
"sku": "PLY-001-N-CH",
"price": "249.00",
"compare_at_price": "299.00"
},
{
"price_list": "GENERAL",
"sku": "PLY-001-N-CH",
"price": "220.00",
"min_quantity": "6"
},
{
"price_list": "MAYOREO",
"sku": "PLY-001-N-M",
"price": null
}
]
}'
Customers
Customers and customer groups. Permission `customers`.
GET /customer-groups/ List customer-groups read_customers
Permission: `read_customers`.
curl -s -X GET "$RM_BASE_URL/customer-groups/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /customer-groups/ Create the customer group write_customers
The body carries `code`. `409`/`400` if the code already exists. Permission: `write_customers`.
curl -s -X POST "$RM_BASE_URL/customer-groups/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "EXAMPLE",
"name": "Wholesalers",
"description": "Buys by the case"
}'
GET /customer-groups/{code}/ Get the customer group read_customers
Permission: `read_customers`.
curl -s -X GET "$RM_BASE_URL/customer-groups/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
PUT /customer-groups/{code}/ Create or replace the customer group (upsert) write_customers
`201` if it creates it, `200` if it replaces it. The code in the path takes precedence over the one in the body. Permission: `write_customers`.
curl -s -X PUT "$RM_BASE_URL/customer-groups/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Wholesalers",
"description": "Buys by the case"
}'
PATCH /customer-groups/{code}/ Update fields of the customer group write_customers
Only the fields sent; `404` if it does not exist. Permission: `write_customers`.
curl -s -X PATCH "$RM_BASE_URL/customer-groups/{code}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Wholesalers"
}'
DELETE /customer-groups/{code}/ Delete the customer group write_customers
`400` if other records depend on it. Permission: `write_customers`.
curl -s -X DELETE "$RM_BASE_URL/customer-groups/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /customers/ List customers read_customers
Permission: `read_customers`.
curl -s -X GET "$RM_BASE_URL/customers/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /customers/ Create customer write_customers
Permission: `write_customers`.
curl -s -X POST "$RM_BASE_URL/customers/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "C-100",
"first_name": "Carlos",
"last_name": "Hernández",
"company": "Bajío Uniforms",
"email": "purchasing@example.com",
"phone": "477 555 0101",
"group": "WHOLESALE",
"accepts_marketing": false,
"addresses": [
{
"label": "Headquarters",
"line1": "Blvd. López Mateos 3401",
"city": "León",
"state": "Guanajuato",
"postal_code": "37160",
"country": "MX"
}
]
}'
GET /customers/{id}/ Get customer read_customers
Permission: `read_customers`.
curl -s -X GET "$RM_BASE_URL/customers/{id}/" \
-H "Authorization: Bearer $RM_API_KEY"
PUT /customers/{id}/ Replace customer write_customers
`addresses` is declarative: if you send it, it replaces all addresses. Permission: `write_customers`.
curl -s -X PUT "$RM_BASE_URL/customers/{id}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "C-100",
"first_name": "Carlos",
"last_name": "Hernández",
"company": "Bajío Uniforms",
"email": "purchasing@example.com",
"phone": "477 555 0101",
"group": "WHOLESALE",
"accepts_marketing": false,
"addresses": [
{
"label": "Headquarters",
"line1": "Blvd. López Mateos 3401",
"city": "León",
"state": "Guanajuato",
"postal_code": "37160",
"country": "MX"
}
]
}'
PATCH /customers/{id}/ Update customer fields write_customers
Permission: `write_customers`.
curl -s -X PATCH "$RM_BASE_URL/customers/{id}/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "477 555 0102"
}'
DELETE /customers/{id}/ Delete customer write_customers
Permission: `write_customers`.
curl -s -X DELETE "$RM_BASE_URL/customers/{id}/" \
-H "Authorization: Bearer $RM_API_KEY"
Orders
Orders, status changes, payments, returns, refunds, carriers and payment methods. Permission `orders`.
GET /orders/ List orders read_orders
Creation order (not by number). For the ERP pull use `updated_since` or `placed_since`. Permission: `read_orders`.
curl -s -X GET "$RM_BASE_URL/orders/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /orders/ Create order write_orders
`201` the first time; with a repeated `Idempotency-Key` and the same data, `200` with the same order. Rejects (400) without stock or without a price. Permission: `write_orders`.
curl -s -X POST "$RM_BASE_URL/orders/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"customer_email": "purchasing@example.com",
"lines": [
{
"sku": "PLY-001-N-CH",
"quantity": "6"
},
{
"sku": "COC-002",
"quantity": "1",
"unit_price": "14.50"
}
],
"shipping_address": {
"line1": "Blvd. Los Próceres 12",
"city": "Antiguo Cuscatlán",
"state": "La Libertad",
"country": "SV"
},
"location": "DC",
"discount": "0",
"shipping": "8.00",
"notes": ""
}'
GET /orders/{id}/ Get order read_orders
Permission: `read_orders`.
curl -s -X GET "$RM_BASE_URL/orders/{id}/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /orders/{id}/pay/ Record payment write_orders
Returns the full order, or `409` if the step does not apply. With `Idempotency-Key` it always returns `200` and does not record another payment. Permission: `write_orders`.
curl -s -X POST "$RM_BASE_URL/orders/{id}/pay/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"method": "transfer",
"reference": "T-123"
}'
POST /orders/{id}/prepare/ Mark as prepared / accepted by the ERP write_orders
Returns the full order, or `409` if the step does not apply. Sets the informational stamp `erp_accepted_at`; it neither releases nor deducts stock. Permission: `write_orders`.
curl -s -X POST "$RM_BASE_URL/orders/{id}/prepare/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /orders/{id}/ship/ Mark as shipped (deducts stock) write_orders
Returns the full order, or `409` if the step does not apply. With `carrier_code` it computes `tracking_url`; with `carrier` (text) it first looks among the store's carriers. Permission: `write_orders`.
curl -s -X POST "$RM_BASE_URL/orders/{id}/ship/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"carrier_code": "C807",
"tracking_number": "ABC123"
}'
POST /orders/{id}/ready/ Mark as ready for pickup (deducts stock) write_orders
Returns the full order, or `409` if the step does not apply. `pickup` orders. Permission: `write_orders`.
curl -s -X POST "$RM_BASE_URL/orders/{id}/ready/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /orders/{id}/deliver/ Mark as delivered write_orders
Returns the full order, or `409` if the step does not apply. From `shipped` or `ready_for_pickup`. Permission: `write_orders`.
curl -s -X POST "$RM_BASE_URL/orders/{id}/deliver/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /orders/{id}/cancel/ Cancel order write_orders
Returns the full order, or `409` if the step does not apply. Releases reserved stock; refunds the payment if it was already charged. Permission: `write_orders`.
curl -s -X POST "$RM_BASE_URL/orders/{id}/cancel/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Requested by the customer"
}'
POST /orders/{id}/archive/ Archive order write_orders
Returns the full order, or `409` if the step does not apply. Only cancelled, delivered or refunded orders. Permission: `write_orders`.
curl -s -X POST "$RM_BASE_URL/orders/{id}/archive/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /orders/{id}/unarchive/ Unarchive order write_orders
Returns the full order, or `409` if the step does not apply. Restores it to how it was. Permission: `write_orders`.
curl -s -X POST "$RM_BASE_URL/orders/{id}/unarchive/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /orders/{id}/returns/ Record return write_orders
`201`, then `200` when repeated with the same `Idempotency-Key`. Permission: `write_orders`.
curl -s -X POST "$RM_BASE_URL/orders/{id}/returns/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"lines": [
{
"line_id": "0192f3a4-6b1c-7d2e-9f00-1a2b3c4d5e6f",
"quantity": "1"
}
],
"reason": "Size",
"restock": true
}'
POST /orders/{id}/refunds/ Refund a payment write_orders
Synchronous: with a payment gateway it is requested in the same call and, if it fails, `409` with no trace. `200` when repeated with the same key. Permission: `write_orders`.
curl -s -X POST "$RM_BASE_URL/orders/{id}/refunds/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": "25.00",
"reason": "Damaged product"
}'
GET /orders/{id}/payments/ List order payments read_orders
Read only; each payment includes its `refunds`. Permission: `read_orders`.
curl -s -X GET "$RM_BASE_URL/orders/{id}/payments/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /carriers/ List carriers read_orders
Read only. Includes deactivated ones. Permission: `read_orders`.
curl -s -X GET "$RM_BASE_URL/carriers/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /carriers/{code}/ Get carrier read_orders
Permission: `read_orders`.
curl -s -X GET "$RM_BASE_URL/carriers/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /payment-methods/ List payment methods read_orders
Read only. Includes deactivated ones. Permission: `read_orders`.
curl -s -X GET "$RM_BASE_URL/payment-methods/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /payment-methods/{code}/ Get payment method read_orders
Permission: `read_orders`.
curl -s -X GET "$RM_BASE_URL/payment-methods/{code}/" \
-H "Authorization: Bearer $RM_API_KEY"
Integrations
Connections, sync runs and external references. Permission `integrations`.
GET /connections/ List connections read_integrations
Read only; connections are created in the back office. Permission: `read_integrations`.
curl -s -X GET "$RM_BASE_URL/connections/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /connections/{id}/ Get connection read_integrations
Permission: `read_integrations`.
curl -s -X GET "$RM_BASE_URL/connections/{id}/" \
-H "Authorization: Bearer $RM_API_KEY"
GET /connections/{id}/sync-runs/ List sync runs read_integrations
Permission: `read_integrations`.
curl -s -X GET "$RM_BASE_URL/connections/{id}/sync-runs/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /connections/{id}/sync-runs/ Report a sync run write_integrations
Sets the connection's `last_sync_at` to `finished_at` (never moves it back; a `failed` run does not move it). Permission: `write_integrations`.
curl -s -X POST "$RM_BASE_URL/connections/{id}/sync-runs/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "inventory",
"started_at": "2026-10-17T10:00:00Z",
"finished_at": "2026-10-17T10:05:00Z",
"records": 1200,
"errors": 3,
"message": "3 SKUs without a variant"
}'
GET /external-references/ Search external references read_integrations
Permission: `read_integrations`.
curl -s -X GET "$RM_BASE_URL/external-references/" \
-H "Authorization: Bearer $RM_API_KEY"
POST /external-references/ Save external reference (upsert) write_integrations
Upsert by (connection, local record): `201` if it creates, `200` if it updates. An `external_id` cannot be linked to two records. Permission: `write_integrations`.
curl -s -X POST "$RM_BASE_URL/external-references/" \
-H "Authorization: Bearer $RM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"connection": "0192f3a4-6b1c-7d2e-9f00-1a2b3c4d5e6f",
"object_type": "product",
"object_key": "PLY-001",
"external_id": "5c2e0000-0000-0000-0000-000000000000",
"external_code": "1000"
}'
DELETE /external-references/{id}/ Delete external reference write_integrations
Permission: `write_integrations`.
curl -s -X DELETE "$RM_BASE_URL/external-references/{id}/" \
-H "Authorization: Bearer $RM_API_KEY"
Outgoing webhooks
The store's Webhooks app sends a POST with JSON to the address you configure, for each chosen event. Only public https addresses are accepted; redirects are not followed and the maximum wait is 5 s.
Events: order.created, order.updated, order.paid, order.payment_failed, order.shipped, order.ready_for_pickup, order.delivered, order.cancelled, order.returned, order.refunded, inventory.changed.
{"id": "6f1c0000-0000-0000-0000-000000000000", "event": "order.created", "store": "mine",
"occurred_at": "2026-10-15T20:31:02+00:00", "data": {"number": 1001, "payment_status": "pending", "...": "..."}}
Headers: X-RM-Event, X-RM-Event-Id (equal to id), X-RM-Signature and User-Agent: RM-Webhooks/1.
Verifying the signature
X-RM-Signature is the hex HMAC-SHA256 of the exact body (the bytes received, without parsing and re-serializing), with the app secret as the key. Compare in constant time.
import hashlib, hmac
def signature_is_valid(body: bytes, header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
import { createHmac, timingSafeEqual } from "node:crypto";
export function signatureIsValid(body, header, secret) {
const expected = createHmac("sha256", secret).update(body).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(header ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}
Delivery and retries
- Reply
2xxto acknowledge. On5xx,429,408, a timeout or a network error it is retried with growing backoff (30 s · 2n, maximum 1 h): up to 8 attempts in about 2 hours. Any other4xxis considered failed without retrying. Each retry sends the same body, the sameidand the same signature. - At least once: a delivery may arrive more than once. Deduplicate by
id. - No guaranteed order: compare
occurred_at; ininventory.changedignore the event older than the last one applied for that SKU. - After 5 consecutive failed deliveries the app is paused and the store sees a notice.
order.refundedalso carriesrefund(id,payment_id,amount,currency,status,reason).
Errors and limits
| Code | Meaning |
|---|---|
200 201 204 | Successful. 201 creates; 204 has no body. |
202 | Bulk job queued: check its id. |
400 | Validation. Details per field, for example {"sku": ["<message>"]}. In bulk operations, {"errors": {"<index>": [...]}}. |
401 | No key, or an invalid or deactivated key. |
403 | The key lacks the permission: {"detail": "<message>"}. |
404 | Does not exist in the key's store (also if it belongs to another store). |
409 | The step does not apply to the current status (for example, shipping a cancelled order), or an Idempotency-Key was used with different data. |
429 | Budget used up. Wait Retry-After seconds. |
Rate limit
Each key has its own budget of 600 requests per minute (sliding window). A key never spends another key's budget. Each authenticated response carries:
X-RateLimit-Limit: requests allowed per minute.X-RateLimit-Remaining: the ones left in the window.X-RateLimit-Reset: second (Unix epoch) at which the oldest slot is freed.
When it runs out, 429 with Retry-After. Rejected requests do not count: waiting that long is enough. Retry with growing backoff only 429, 5xx and network errors; never a 4xx. For large loads use the bulk endpoints (up to 1000 rows per call for prices and inventory, 500 products per call for bulk creation).
Connector guide
The typical flow of an ERP against RM, in the order in which it is best built. Every step is idempotent: you can repeat it without duplicating.
Test the connection and permissions
GET /. Confirms the store and that the key has the scopes you need.Catalog
First create what products reference: categories (
PUT /categories/{code}/), brands, attributes and locations. Then the products:PUT /products/{code}/one by one,POST /products/bulk/(up to 500, upsert) orPOST /products/import/for first loads of hundreds of thousands (NDJSON, create only). If your ERP already has variants, usevariants_mode: "exact". Check the job result row by row.Prices
POST /prices/bulk/?partial=truein blocks of 1000, per price list.price: nulldeletes the line.Inventory
POST /inventory/bulk/?partial=truewith absoluteon_hand(what your ERP says). Each RM order reserves its quantity when created; on shipping it is deducted. Your number rules: while an order is still open in both systems, RM shows less than the ERP, never more.Order pull
Every few minutes:
GET /orders/?updated_since=<last mark>(or?placed_since=), followingnext. Store the mark of the last successful run, not your clock's time. With theorder.createdwebhook you can find out instantly; the pull remains the safety net.Confirm that the ERP received it
POST /external-references/withobject_type: "order"and the order number links the order to its ERP document;POST /orders/{id}/prepare/sets theerp_accepted_atstamp. Neither releases nor deducts stock.Status push
When the ERP advances:
pay(charged),ship(withcarrier_codeandtracking_number),ready(ready for pickup),deliver,cancel,returnsandrefunds. They return the full order, or409if the step does not apply. UseIdempotency-Keyonpay,returnsandrefunds.Report the run
POST /connections/{id}/sync-runs/with the type, the times, the records and the errors: the store sees in RM when it last synced and how it went.
Downloads
- openapi.en.json OpenAPI 3.1 in English: open it in Postman, Insomnia, Swagger Editor or paste it into an AI assistant.
- rm-api.postman_collection.en.json Postman v2.1 collection by area, with example bodies and “Test connection”.
- rm-api.postman_environment.json Environment with
base_urlandapi_key(empty: put your own). - api.en.md Full reference with all the business rules.
- api-ia.en.md and llms.txt Context and prompts for AI.
In Postman: import the collection and the environment, choose the rm-api environment, paste your key into api_key and run “Test connection”.
Implement with AI
Paste the context and the prompt you need into your coding assistant, and attach openapi.en.json. Never paste a real key: use an environment variable. All of this is also in api-ia.en.md and, in agent format, in /llms.txt?lang=en.
Compact API context
Always paste it before the prompt.
RM REST API v1 (multi-tenant ecommerce). Base: {base_url} (e.g. https://retail-s.online/api/v1). JSON, UTF-8.
TENANT AND KEYS
- Each API key belongs to ONE store: everything it reads or writes belongs to that store. There is no way to pick another one.
- Authentication: header "Authorization: Bearer rk_<id>_<secret>". A back office session does not work.
- GET {base_url}/ returns the store, the key name and its scopes (connection test).
SCOPES (per area; GET requires read_<area>, POST/PUT/PATCH/DELETE require write_<area>; write includes read)
- products: products, variants, images, categories, brands, collections, attributes, templates, modifiers, translations, bulk creation, import.
- inventory: stock levels and locations. prices: prices and price lists. customers: customers and groups.
- orders: orders, statuses, payments, returns, refunds, carriers and payment methods.
- integrations: connections, sync runs and external references.
- Without the permission: 403 {"detail": "<message>"}.
- Error message texts are currently returned in Spanish: rely on the status code and on the field names, never on the text.
RECORD IDENTITY
- With a code (products, categories, brands, collections, attributes, locations, price lists, groups): the code, in uppercase (ply-001 = PLY-001).
- Variants: by SKU. Customers, orders, lines, returns, images, connections and external references: public UUID "id" (store it as a string).
- PUT /<resource>/<code>/ is an upsert (201 creates, 200 replaces). PATCH updates only what is sent (404 if it does not exist). The arrays "options", "variants" (with variants_mode "exact"), "addresses" and "attributes" are DECLARATIVE: what you send is what remains.
- Amounts and quantities: decimal strings ("199.0000"). Dates: ISO 8601 with a time zone.
PAGINATION AND SYNC
- Cursor lists: ?limit= (1 to 500, default 100). Response {"next": url|null, "results": [...]}. Request the "next" URL AS IS until it is null. There is no count, previous or offset (offset gives 400).
- Incremental: ?updated_since=<ISO 8601> on the code-based lists and on customers and orders; orders also ?placed_since=.
- Results of bulk jobs (bulk/import) are paginated with ?offset= and ?limit= on the job, not by cursor.
IDEMPOTENCY
- Optional header "Idempotency-Key" (up to 100 characters) on POST /orders/, POST /orders/{id}/pay/, /returns/ and /refunds/.
- Same key and same data: the operation is not repeated (201 the first time and 200 when repeated; pay is always 200). Same key with different data: 409.
- Without the header, a retry may create a duplicate (a repeated pay gives 409 "already paid").
RESPONSE CODES
- 200/201/204 success; 202 bulk job queued.
- 400 validation (details per field; in batches, {"errors": {"<index>": [...]}}). 401 missing, invalid or deactivated key. 403 missing scope. 404 does not exist in your store. 409 the step does not apply to the status, or Idempotency-Key reused with different data.
- 429: 600 requests per minute per key. Wait "Retry-After" seconds. Authenticated responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (epoch), except on 401 and 403.
- Retry with growing backoff only on 429, 5xx and network errors; never on a 4xx.
BULK OPERATIONS
- POST /prices/bulk/ and POST /inventory/bulk/: up to 1000 rows. All or nothing by default; ?partial=true applies the valid ones and returns 200 with "rows" per row. inventory on_hand is ABSOLUTE (what the ERP says), not an adjustment.
- POST /products/bulk/ (up to 500 products, upsert, 202 + job id; poll GET /products/bulk/{id}/).
- POST /products/import/ (multipart, NDJSON, create only, 202; result at GET /products/import/{id}/rows/).
ORDERS
- Separate statuses: status (open|cancelled|archived), payment_status, fulfillment_status; delivery_type (shipping|pickup|takeout|dine_in|delivery_app).
- POST /orders/ rejects (400) without stock or without a price. Changes: POST /orders/{id}/{pay,prepare,ship,ready,deliver,cancel,archive,unarchive,returns,refunds}/ (409 if the step does not apply).
- Inventory: each order reserves stock on creation; ship/ready deducts; cancel releases. /prepare/ only sets the informational stamp erp_accepted_at.
OUTGOING WEBHOOKS (RM -> your system; the store's "Webhooks" app)
- JSON POST to a public https URL: {"id", "event", "store", "occurred_at", "data"}.
- Events: order.created, order.updated, order.paid, order.payment_failed, order.shipped, order.ready_for_pickup, order.delivered, order.cancelled, order.returned, order.refunded, inventory.changed.
- Headers: X-RM-Event, X-RM-Event-Id (= id), X-RM-Signature = hex HMAC-SHA256 of the EXACT BODY (bytes, not re-serialized) with the app secret; User-Agent RM-Webhooks/1.
- Reply 2xx quickly (maximum wait 5 s). Retries on 5xx, 429, 408 or a network error: up to 8 in ~2 h, same body, same id, same signature. Any other 4xx is not retried.
- At least once and with no guaranteed order: deduplicate by id and compare occurred_at (inventory.changed: ignore anything older than the last one applied per SKU).
- After 5 consecutive failures the app is paused.
1. Catalog and inventory connector from an ERP
Paste together with: the compact context, `openapi.en.json`, and a sample of 3 to 5 items from your ERP (JSON or CSV, with no real customer data).
You are a senior integration engineer. Using the context and the OpenAPI of the RM API that I am attaching, write a connector
that syncs the catalog and inventory from my ERP to RM. Language: <Python 3.12 | Node 20>. Source ERP: <name>.
Requirements:
1. Read the key from the RM_API_KEY environment variable and the base URL from RM_BASE_URL. No secrets in the code.
2. First load: products through POST /products/bulk/ (batches of 500) or, if there are more than 20,000, POST /products/import/ with NDJSON. Poll
the job until it finishes and save the rows with status "error" to an errors file.
3. Incremental load: use the ERP's updated_since; send PUT /products/{code}/ only for what changed. If the ERP already has variants, use variants_mode "exact".
4. Prices: POST /prices/bulk/?partial=true in batches of 1000, one per price list. Stock: POST /inventory/bulk/?partial=true with
ABSOLUTE on_hand. Report each failed row without stopping the batch.
5. Keep the ERP <-> RM map with POST /external-references/ (object_type product or variant) and report each run with POST /connections/{id}/sync-runs/.
6. Retries: growing backoff on 429 (respect Retry-After), 5xx and network errors; never retry a 4xx. Respect the limit of 600 requests per minute.
7. Clear code, with logging that has no personal data, one test for each function that transforms data, and a README explaining how to run it.
Deliver: file structure, the complete code and the commands to test it against a test store. If the contract does not cover something, say so instead of inventing an endpoint.
2. Order webhook consumer with signature verification
Paste together with: the compact context and, if you want real examples, a test event body.
Using the context of the RM API that I am attaching, write an HTTP receiver for order webhooks. Framework: <FastAPI | Express | Django>.
Requirements:
1. Verify X-RM-Signature: hex HMAC-SHA256 over the EXACT body received (raw bytes, not parsed or re-serialized)
with the secret from RM_WEBHOOK_SECRET. Compare with a constant-time function. Invalid signature: 401 without processing.
2. Reply 2xx in under 5 s: store the event in a queue or table and process it separately.
3. Deduplicate by "id" (X-RM-Event-Id): the same event may arrive several times. Make the processing idempotent.
4. Do not assume order: compare occurred_at and discard an event older than the last one applied (in inventory.changed, per sku).
5. Handle order.created, order.paid, order.shipped, order.cancelled and order.refunded; ignore events you do not know, replying 2xx.
6. After a 2xx RM does not retry; on 5xx, 429 or a timeout it does. Tell temporary errors (5xx) apart from invalid data (4xx).
7. Tests: valid signature, invalid signature, repeated event and out-of-order event. Include a curl command that signs a sample body with openssl.
Deliver the complete code and the tests. Do not log the secret or the full order body.
3. Bulk import script
Paste together with: the compact context, `openapi.en.json` and a sample of 5 rows from your file (CSV or Excel) with the real columns.
Using the context and the OpenAPI of the RM API that I am attaching, write a command-line script that imports my products file <CSV | XLSX> into RM.
Language: <Python 3.12 | Node 20>. I am giving you a sample of 5 rows.
Requirements:
1. Map the columns to the structure of PUT /products/{code}/ (code, name, status, category, options, variants, attributes...). Ask for confirmation of the mapping before sending.
2. --dry-run mode: validate and show how many products would be created and which rows are invalid, without calling the API.
3. Sending: POST /products/import/ (NDJSON) for large loads of new products; POST /products/bulk/ in batches of 500 if existing ones must be updated.
4. Poll the job until it finishes (with growing backoff) and write a report with the "error" rows and their message. Non-zero exit code if there were errors.
5. Key in RM_API_KEY and base URL in RM_BASE_URL; never in the code or in the report file.
6. It is resumable: it can run again without duplicating (use upsert by code or skip the ones already created).
7. Afterwards, optionally, images by URL (POST /products/{code}/images/ with an https "url") and stock with /inventory/bulk/.
Deliver the complete script, an example input file and the command to run it.
4. API client in Python or Node
Paste together with: the compact context and `openapi.en.json`.
Using the OpenAPI and the context of the RM API that I am attaching, generate a small typed client for <Python 3.12 (httpx) | Node 20 (fetch) with TypeScript>.
Requirements:
1. A class RMClient(base_url, api_key) with the key in the Authorization: Bearer header.
2. One method per operation of the resources: products, variants, inventory, prices, customers and orders (including the actions pay, ship, cancel, returns and refunds).
3. Lists are iterators/generators that follow "next" as is until it is null; they accept limit and updated_since.
4. Retries with growing backoff on 429 (respect Retry-After), 5xx and network errors; no retries on 4xx. Typed errors: ValidationError (400, with details per field),
AuthError (401/403), NotFound (404) and ConflictError (409).
5. Optional Idempotency-Key in create_order, pay_order, create_return and refund_order; generate a UUID if the caller does not provide one and reuse THAT SAME UUID on every retry of the same operation (a new one per operation, never a new one per retry).
6. Decimals as strings (not float); ISO 8601 dates.
7. Tests with a mock server for: pagination, 429 with Retry-After and 409 for a reused Idempotency-Key.
Deliver the client, the tests and a 20-line usage example. Do not include any real key.
5. Connector security review
Paste together with: the compact context and the full connector code (without the `.env` file).
Act as a security reviewer. I am giving you the context of the RM API and the code of my connector. Review it and give me a prioritized list (critical, high, medium, low)
with file and line, the risk in one sentence and the concrete fix.
Look in particular for:
1. Secrets: API key (rk_...), webhook secret or ERP credentials in the code, in logs, in error messages, in git history or in example files.
2. Webhook receiver: HMAC-SHA256 signature over the raw body, constant-time comparison, rejection when unsigned, deduplication by id, body size limit and fast response.
3. Least privilege: the key must have only the scopes the connector uses (read_orders without write if it only reads orders, etc.).
4. Retries: none on 4xx, growing backoff on 429/5xx, respect for Retry-After, Idempotency-Key on operations that create or charge.
5. Validation of ERP data before sending it; error handling that neither loses data nor duplicates it; TLS verification on (never verify=False).
6. Customers' personal data in logs, temporary files or reports.
7. Outdated dependencies or dependencies without a pinned version.
Do not fix anything without showing me the finding first. If something in the RM contract is not clear to you, say so instead of assuming.