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.

Permissions by area
AreaScopesResources
Catalogread_products write_productsproducts, variants, images, categories, brands, collections, attributes, templates, modifiers, translations, bulk creation and import
Inventoryread_inventory write_inventorystock levels, locations
Pricesread_prices write_pricesprices, price lists
Customersread_customers write_customerscustomers, customer groups
Ordersread_orders write_ordersorders, status changes, payments, returns, refunds, carriers, payment methods
Integrationsread_integrations write_integrationsconnections, 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-001 and PLY-001 are the same. Variants are identified by SKU. Customers, orders, lines, returns, images, connections and external references are identified by their public id: a UUID you must store as a string. The API never exposes internal IDs.
  • Upsert. PUT /<resource>/<code>/ creates (201) or replaces (200). PATCH updates only the fields sent and returns 404 if it does not exist.
  • Declarative. The lists options, variants (with variants_mode: "exact"), addresses, attributes, modifiers and hours leave 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 the next URL as is until it is null. Records come out in creation order, so nothing is repeated or skipped while you read. There is no count or previous, and offset returns 400.
  • Incremental sync. ?updated_since=2026-10-14T00:00:00Z on lists; on orders also ?placed_since=.
  • Safe retries. POST /orders/, /pay/, /returns/ and /refunds/ accept the Idempotency-Key header (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 2xx to acknowledge. On 5xx, 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 other 4xx is considered failed without retrying. Each retry sends the same body, the same id and the same signature.
  • At least once: a delivery may arrive more than once. Deduplicate by id.
  • No guaranteed order: compare occurred_at; in inventory.changed ignore 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.refunded also carries refund (id, payment_id, amount, currency, status, reason).

Errors and limits

Response codes
CodeMeaning
200 201 204Successful. 201 creates; 204 has no body.
202Bulk job queued: check its id.
400Validation. Details per field, for example {"sku": ["<message>"]}. In bulk operations, {"errors": {"<index>": [...]}}.
401No key, or an invalid or deactivated key.
403The key lacks the permission: {"detail": "<message>"}.
404Does not exist in the key's store (also if it belongs to another store).
409The step does not apply to the current status (for example, shipping a cancelled order), or an Idempotency-Key was used with different data.
429Budget 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.

  1. Test the connection and permissions

    GET /. Confirms the store and that the key has the scopes you need.

  2. 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) or POST /products/import/ for first loads of hundreds of thousands (NDJSON, create only). If your ERP already has variants, use variants_mode: "exact". Check the job result row by row.

  3. Prices

    POST /prices/bulk/?partial=true in blocks of 1000, per price list. price: null deletes the line.

  4. Inventory

    POST /inventory/bulk/?partial=true with absolute on_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.

  5. Order pull

    Every few minutes: GET /orders/?updated_since=<last mark> (or ?placed_since=), following next. Store the mark of the last successful run, not your clock's time. With the order.created webhook you can find out instantly; the pull remains the safety net.

  6. Confirm that the ERP received it

    POST /external-references/ with object_type: "order" and the order number links the order to its ERP document; POST /orders/{id}/prepare/ sets the erp_accepted_at stamp. Neither releases nor deducts stock.

  7. Status push

    When the ERP advances: pay (charged), ship (with carrier_code and tracking_number), ready (ready for pickup), deliver, cancel, returns and refunds. They return the full order, or 409 if the step does not apply. Use Idempotency-Key on pay, returns and refunds.

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

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.