Подключите свои системы к Cashod.
API Cashod позволяет вашим системам отправлять COD-заказы в Cashod, получать обратно статус доставки и синхронизировать каталог товаров и остатки. Это JSON HTTP API — SDK не нужен.
https://api.cashod.ma/api/v1$ Один ключ, один магазин, только отмеченные права.
Создайте ключ в Cashod в разделе Настройки → Разработчикам. Секрет показывается один раз, при создании, и потом его нельзя восстановить — если он потерян, отзовите ключ и создайте новый.
Передавайте его как bearer-токен в каждом запросе:
Authorization: Bearer csk_live_…Он видит и меняет данные только этого магазина, поэтому id магазина передавать не нужно — ключ уже указывает, о каком магазине речь. Продавец с несколькими магазинами создаёт по ключу на каждый.
У каждого ключа есть права (scopes), которые отмечаются при создании. Право на запись включает соответствующее чтение, поэтому ключ, создающий заказы, может их и читать. Дайте скрипту отчётов ключ только на чтение — и он ничего не создаст и не отменит, даже если утечёт.
orders:readЧтение заказов и их статуса доставкиorders:writeСоздание заказов — включает orders:readproducts:readЧтение каталога товаровproducts:writeСоздание и обновление товаров — включает products:readstock:writeУстановка остатков вариантов
Первый заказ за три шага.
- 01Создайте ключ
В Cashod откройте Настройки → Разработчикам, отметьте нужные права и скопируйте секрет — он показывается только один раз.
- 02Отправьте заказ
Отправьте POST на
/v1/ordersсо своимexternal_id, чтобы повторный запрос никогда не превратился во вторую посылку. - 03Следите за доставкой
Читайте
status,confirmation_statusиtracking_numberиз/v1/orders/{id}по мере того, как служба доставки везёт заказ.
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.
/v1/ordersСоздать заказ из вашего чекаута. Можно безопасно повторять с external_id.
Правоorders:write/v1/orders/{id}Получить один заказ: статус доставки, статус подтверждения и трек-номер.
Правоorders:read/v1/ordersСписок заказов, сначала новые, фильтр по created_after, пагинация курсором.
Правоorders:read/v1/productsСоздать или обновить товар по SKU, с вариантами и изображениями.
Правоproducts:write/v1/products/{productId}/variants/{variantId}/stockУстановить абсолютный остаток варианта.
Правоstock:write/v1/openapi.jsonМашиночитаемый документ OpenAPI, на основе которого построен справочник.
Соберите интеграцию с ИИ-ассистентом
Пользуетесь ИИ-ассистентом для кода? Скопируйте этот промпт вместо того, чтобы пересказывать страницу вручную. В нём нет ключа: сгенерированный код читает ваш ключ из переменной окружения CASHOD_API_KEY.
Вставьте его в ChatGPT, Claude, Cursor или Lovable. В нём правила этой страницы и ссылка на документ OpenAPI, так что ассистент напишет интеграцию под ваш стек.
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 — будет использована цена из каталога; передайте — и приоритет у неё, что нужно, когда на лендинге шла акция.
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 после отправки.
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 существовал и был обновлён.
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.
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.
Курсоры, а не номера страниц, потому что заказы приходят постоянно: при смещениях новый заказ сдвигает все последующие страницы на одну позицию, и клиент, листающий историю, молча пропускает одни строки и читает другие повторно.
Ошибки
Любая ошибка имеет одинаковую структуру:
{
"error": {
"type": "invalid_request",
"message": "No product or variant with SKU \"GHOST-1\" in this store."
}
}Ориентируйтесь на type, а не на текст message — формулировка может улучшаться, тип не изменится.
invalid_requestПроблема в вашем запросе. Сообщение укажет, где именно.authentication_errorКлюч отсутствует, неизвестен или отозван.permission_errorУ ключа нет нужного права, или подписка аккаунта неактивна.not_foundНет такого заказа или товара в магазине этого ключа.rate_limit_exceededПодождите и повторите.server_errorПроблема на нашей стороне. Повторять безопасно — и дважды тоже, если вы передали external_id.Лимиты запросов
120 запросов в минуту на ключ. Лимит считается по ключу, а не по IP-адресу, поэтому другой клиент на том же хостинге не израсходует ваш лимит.
Версионирование
Версия указана в пути: /v1. В рамках версии в ответы могут добавляться новые необязательные поля, поэтому разбирайте ответы гибко и игнорируйте незнакомое. Всё, что может сломать существующую интеграцию, выходит в новой версии.
Каждый эндпоинт, каждое поле.
Генерируется из документа OpenAPI самого бэкенда, поэтому обновляется вместе с кодом при каждом деплое и не устаревает.