cashod
Разработчикам · API v1

Подключите свои системы к Cashod.

API Cashod позволяет вашим системам отправлять COD-заказы в Cashod, получать обратно статус доставки и синхронизировать каталог товаров и остатки. Это JSON HTTP API — SDK не нужен.

Базовый URLhttps://api.cashod.ma/api/v1
Создать заказ
$ 
Ответ
Аутентификация

Один ключ, один магазин, только отмеченные права.

Создайте ключ в Cashod в разделе Настройки → Разработчикам. Секрет показывается один раз, при создании, и потом его нельзя восстановить — если он потерян, отзовите ключ и создайте новый.

Передавайте его как bearer-токен в каждом запросе:

Authorization
Authorization: Bearer csk_live_…
Ключ принадлежит одному магазину.

Он видит и меняет данные только этого магазина, поэтому id магазина передавать не нужно — ключ уже указывает, о каком магазине речь. Продавец с несколькими магазинами создаёт по ключу на каждый.

Права (scopes)

У каждого ключа есть права (scopes), которые отмечаются при создании. Право на запись включает соответствующее чтение, поэтому ключ, создающий заказы, может их и читать. Дайте скрипту отчётов ключ только на чтение — и он ничего не создаст и не отменит, даже если утечёт.

  • orders:readЧтение заказов и их статуса доставки
  • orders:writeСоздание заказов — включает orders:read
  • products:readЧтение каталога товаров
  • products:writeСоздание и обновление товаров — включает products:read
  • stock:writeУстановка остатков вариантов
Быстрый старт

Первый заказ за три шага.

  1. 01
    Создайте ключ

    В Cashod откройте Настройки → Разработчикам, отметьте нужные права и скопируйте секрет — он показывается только один раз.

  2. 02
    Отправьте заказ

    Отправьте POST на /v1/orders со своим external_id, чтобы повторный запрос никогда не превратился во вторую посылку.

  3. 03
    Следите за доставкой

    Читайте status, confirmation_status и tracking_number из /v1/orders/{id} по мере того, как служба доставки везёт заказ.

POST /v1/orders
curl -X POST https://api.cashod.ma/api/v1/orders \
  -H "Authorization: Bearer csk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "order-77",
    "customer": { "name": "Amine Berrada", "phone": "+212600000000" },
    "shipping": { "address": "12 Rue Tarik Ibn Ziad", "city": "Casablanca" },
    "items": [ { "sku": "MON-001-M-NOIR", "quantity": 2 } ]
  }'
Эндпоинты

Компактный API, который можно освоить за вечер.

Всё находится по адресу https://api.cashod.ma/api/v1.

POST/v1/orders

Создать заказ из вашего чекаута. Можно безопасно повторять с external_id.

Правоorders:write
GET/v1/orders/{id}

Получить один заказ: статус доставки, статус подтверждения и трек-номер.

Правоorders:read
GET/v1/orders

Список заказов, сначала новые, фильтр по created_after, пагинация курсором.

Правоorders:read
POST/v1/products

Создать или обновить товар по SKU, с вариантами и изображениями.

Правоproducts:write
PATCH/v1/products/{productId}/variants/{variantId}/stock

Установить абсолютный остаток варианта.

Правоstock:write
GET/v1/openapi.json

Машиночитаемый документ OpenAPI, на основе которого построен справочник.

ИИ-ассистент

Соберите интеграцию с ИИ-ассистентом

Пользуетесь ИИ-ассистентом для кода? Скопируйте этот промпт вместо того, чтобы пересказывать страницу вручную. В нём нет ключа: сгенерированный код читает ваш ключ из переменной окружения CASHOD_API_KEY.

Вставьте его в ChatGPT, Claude, Cursor или Lovable. В нём правила этой страницы и ссылка на документ OpenAPI, так что ассистент напишет интеграцию под ваш стек.

cashod-api-prompt.md
You are helping me integrate my system (online store, landing page or ERP) with the Cashod API, a cash-on-delivery order platform. Write the integration code following the specification below exactly. Ask me which language/framework I use if you cannot tell from context, and which of the tasks below I need.

The full machine-readable specification is at https://api.cashod.ma/api/v1/openapi.json. Fetch it if you can, and trust it over this summary for field details.

## Rules
- Call the API from my SERVER only, never from browser JavaScript: the key must not reach visitors.
- Read the key from an environment variable named CASHOD_API_KEY (it looks like csk_live_…). Never hard-code it. I create it in Cashod under Settings → Developers; one key belongs to one store.
- Every request: header "Authorization: Bearer <CASHOD_API_KEY>", JSON bodies with "Content-Type: application/json".
- Send only the fields listed here or in the OpenAPI document: unknown fields are rejected with 400 invalid_request.
- Rate limit: 120 requests per minute per key.
- Never let a Cashod failure break my customer's checkout: log it and retry in the background.

## Task 1: send an order
POST https://api.cashod.ma/api/v1/orders  (scope orders:write)
{
  "external_id": "order-77",
  "customer": { "name": "Amine Berrada", "phone": "+212600000000" },
  "shipping": { "address": "12 Rue Tarik Ibn Ziad", "city": "Casablanca" },
  "items": [ { "sku": "MON-001-M-NOIR", "quantity": 2, "unit_price": 149 } ]
}
- Always send external_id, set to my own order id. It makes the call idempotent: a retry returns the existing order (200) instead of creating a second one (201). Without it a retried timeout becomes a second real parcel.
- customer.name, customer.phone, shipping.address, shipping.city are required. shipping.phone is optional (defaults to the customer's).
- items: at least one. Each needs quantity (integer ≥ 1) and one of: sku (variant or product SKU, the usual way), product_id (Cashod product id), or name (free text for an item not in the catalogue; unit_price is then required).
- unit_price: what the customer actually paid per unit. Omit it to use the catalogue price.
- Optional: total (final agreed total; the difference is recorded as a discount), discount, notes, scheduled_for (ISO 8601), source_name (e.g. "Landing page Ramadan"), confirmed (true ONLY if I already confirmed the order with the customer; it skips the confirmation call).

## Task 2: read delivery status
GET https://api.cashod.ma/api/v1/orders/{id}  or  GET https://api.cashod.ma/api/v1/orders?created_after=<ISO 8601>  (scope orders:read)
- An order has status (shipping, e.g. NOT_SHIPPED, SHIPPED, DELIVERED, RETURNED), confirmation_status (e.g. PENDING, CONFIRMED, CANCELLED) and tracking_number once shipped.
- Lists are cursor-paginated, newest first: read data, has_more and next_cursor, and pass ?cursor=<next_cursor> for the next page. limit defaults to 50, max 100.
- Poll at most once a minute.

## Task 3: sync products
POST https://api.cashod.ma/api/v1/products  (scope products:write)
- Keyed on sku: an unknown sku creates the product (201), a known one updates it (200). Safe to send the whole catalogue on a schedule.
- Required: sku, name, category, price, cost_price. Optional: description, sale_price, is_active, track_stock, min_stock, images (URLs), variants.
- variants: each needs sku; optional size, color, price, sale_price, cost_price, stock. Variants are matched by sku; one I do not list is left untouched, never deleted.
- images: omit to keep the existing ones, send a list to replace them, send [] to clear them.
- Setting stock here also needs the stock:write scope.
- 409 conflict: another product already uses this name, or a variant sku belongs to another product. Report it to me; do not retry.

## Task 4: keep stock in sync
PATCH https://api.cashod.ma/api/v1/products/{productId}/variants/{variantId}/stock  (scope stock:write)
{ "stock": 12 }
- Stock is set to an absolute level, never a delta, so a retried call cannot double-apply.

## Errors
Every failure looks like {"error": {"type": "...", "message": "..."}}. Branch on error.type, never on the message text.
- invalid_request (400): fix the payload, do not retry.
- authentication_error (401): missing, unknown or revoked key. Do not retry.
- permission_error (403): the key lacks the scope, or the subscription is not active. Do not retry.
- not_found (404): no such order or product in this key's store.
- rate_limit_exceeded (429) and server_error (500), plus timeouts and network errors: retry with backoff (for example 1 min, 5 min, 30 min). Orders are safe to retry because of external_id.

New optional fields may appear in responses: ignore what you do not recognise.

## When you are done
Show me where the calls happen in my code, the environment variable to set, and a curl command to send one test order.
Гайды

Чего не расскажет список полей.

Создание заказа

Указывайте каждую позицию по SKU — SKU варианта или товара, смотря по чему вы продаёте. Не передавайте unit_price — будет использована цена из каталога; передайте — и приоритет у неё, что нужно, когда на лендинге шла акция.

POST /v1/orders
curl -X POST https://api.cashod.ma/api/v1/orders \
  -H "Authorization: Bearer csk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "order-77",
    "customer": { "name": "Amine Berrada", "phone": "+212600000000" },
    "shipping": { "address": "12 Rue Tarik Ibn Ziad", "city": "Casablanca" },
    "items": [ { "sku": "MON-001-M-NOIR", "quantity": 2 } ]
  }'

Идемпотентность — передавайте external_id

Передайте свой id заказа как external_id — и запрос можно безопасно повторять. Если тот же external_id придёт снова, Cashod вернёт уже созданный заказ, а не создаст второй:

  • 201 Created — создан новый заказ.
  • 200 OK — по этому external_id заказ уже был; он возвращается без изменений.

Это важнее, чем кажется. Запрос, завершившийся по таймауту, обычно всё равно обработан; без external_id ваш повтор станет вторым реальным заказом, который уйдёт реальному курьеру, и клиент получит две посылки.

Статус доставки

У заказа есть status (доставка), confirmation_status (звонок-подтверждение) и tracking_number после отправки.

GET /v1/orders/{id}
curl https://api.cashod.ma/api/v1/orders/ORDER_ID \
  -H "Authorization: Bearer csk_live_…"

Чтобы следить за многими заказами, получайте список через GET /v1/orders с фильтром created_after. Опрашивайте не чаще раза в минуту: частые запросы ничего не дадут — статусы меняются в темпе службы доставки.

Создание и обновление товаров

Один эндпоинт по вашему sku: неизвестный SKU создаёт товар, известный — обновляет. Поэтому можно отправлять весь каталог по расписанию, не проверяя, что уже есть.

  • 201 Created — этот SKU был новым.
  • 200 OK — товар с этим SKU существовал и был обновлён.
POST /v1/products
curl -X POST https://api.cashod.ma/api/v1/products \
  -H "Authorization: Bearer csk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "sku": "MON-001",
    "name": "Montre Or",
    "category": "Accessoires",
    "price": 199,
    "cost_price": 60,
    "images": [ "https://cdn.example/montre.jpg" ],
    "variants": [
      { "sku": "MON-001-M-NOIR", "size": "M", "color": "Noir", "stock": 7 }
    ]
  }'

Варианты сопоставляются по SKU, а неуказанные остаются нетронутыми — они никогда не удаляются. За вариантом обычно стоит история заказов, поэтому синхронизация, в которой он случайно пропущен, не должна его уничтожать. Удаляйте варианты в панели Cashod.

Изображения работают наоборот: не передадите поле images — текущие останутся; передадите список — он станет новым набором; передайте [], чтобы очистить.

cost_price обязателен. Cashod считает по нему COGS и прибыль, а товар без него показывает маржу 100% в каждом заказе. При обновлении он игнорируется, если по товару уже есть закупленные остатки — иначе изменилась бы себестоимость уже имеющегося товара.

Чтобы задать stock варианта через этот эндпоинт, нужно ещё право stock:write. Без него запрос отклоняется, а не сохраняет молча всё, кроме остатков, — так ключ для каталога остаётся ключом для каталога.

Стоит обработать два конфликта: другой товар в магазине уже использует это название (названия уникальны в пределах магазина, отдельно от SKU), или один из SKU ваших вариантов принадлежит другому товару. Оба возвращаются как 409 conflict с указанием коллизии.

Синхронизация остатков

Остаток задаётся абсолютным значением, а не изменением на дельту. Передайте нужный уровень для варианта — тогда повторный запрос не применится дважды, как это молча случилось бы с -1.

PATCH /v1/products/{productId}/variants/{variantId}/stock
curl -X PATCH https://api.cashod.ma/api/v1/products/PRODUCT_ID/variants/VARIANT_ID/stock \
  -H "Authorization: Bearer csk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "stock": 12 }'

Пагинация

Списки пагинируются курсором, сначала новые. В ответе есть data, has_more и next_cursor. Передайте курсор обратно как ?cursor=, чтобы получить следующую страницу. Размер страницы по умолчанию 50, максимум 100.

Курсоры, а не номера страниц, потому что заказы приходят постоянно: при смещениях новый заказ сдвигает все последующие страницы на одну позицию, и клиент, листающий историю, молча пропускает одни строки и читает другие повторно.

Ошибки

Любая ошибка имеет одинаковую структуру:

application/json
{
  "error": {
    "type": "invalid_request",
    "message": "No product or variant with SKU \"GHOST-1\" in this store."
  }
}

Ориентируйтесь на type, а не на текст message — формулировка может улучшаться, тип не изменится.

400invalid_requestПроблема в вашем запросе. Сообщение укажет, где именно.
401authentication_errorКлюч отсутствует, неизвестен или отозван.
403permission_errorУ ключа нет нужного права, или подписка аккаунта неактивна.
404not_foundНет такого заказа или товара в магазине этого ключа.
429rate_limit_exceededПодождите и повторите.
500server_errorПроблема на нашей стороне. Повторять безопасно — и дважды тоже, если вы передали external_id.

Лимиты запросов

0запросов в минуту на ключ

120 запросов в минуту на ключ. Лимит считается по ключу, а не по IP-адресу, поэтому другой клиент на том же хостинге не израсходует ваш лимит.

Версионирование

Версия указана в пути: /v1. В рамках версии в ответы могут добавляться новые необязательные поля, поэтому разбирайте ответы гибко и игнорируйте незнакомое. Всё, что может сломать существующую интеграцию, выходит в новой версии.

Справочник API

Каждый эндпоинт, каждое поле.

Генерируется из документа OpenAPI самого бэкенда, поэтому обновляется вместе с кодом при каждом деплое и не устаревает.

openapi.json
API Cashod для разработчиков: заказы, остатки, вебхуки