API documentation
Connect your own store, landing page or ERP to Cashod.
Getting started
The Cashod API lets your own systems push COD orders into Cashod, read their delivery status back, and keep your product catalogue and stock in sync. It is a JSON HTTP API — no SDK required.
Everything lives under https://api.cashod.ma/api/v1.
Authentication
Create a key in Cashod under Settings → Developers. The secret is shown once, at creation, and is not recoverable afterwards — if it is lost, revoke the key and create another.
Send it as a bearer token on every request:
Authorization: Bearer csk_live_…A key belongs to one store. It sees and changes that store's data and nothing else, so you never pass a store id — the key already says which store you mean. A merchant running several stores creates a key per store.
Each key also carries scopes, ticked when you create it: orders:read, orders:write, products:read, products:write, stock:write. A write scope also grants the matching read, so a key that creates orders can read them back. Give a reporting script a read-only key and it cannot create or cancel anything, even if it leaks.
Create an order
Identify each item by its SKU — a variant SKU or a product SKU, whichever you sell by. Leave unit_price out and the catalogue price is used; send it and it wins, which is what you want when the landing page ran a promotion.
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 } ]
}'Idempotency — send external_id
Send your own id for the order as external_id and the call becomes safe to retry. If the same external_id arrives again, Cashod returns the order it already created rather than creating a second one:
- 201 Created — a new order now exists.
- 200 OK — this
external_idalready had an order; it is returned unchanged.
This matters more than it looks. A request that times out has usually still been processed; without an external_id, your retry becomes a second real order, dispatched to a real courier, and the customer receives two parcels.
Read delivery status
An order carries status (shipping), confirmation_status (the confirmation call) and tracking_number once it ships.
curl https://api.cashod.ma/api/v1/orders/ORDER_ID \
-H "Authorization: Bearer csk_live_…"To follow many orders, list them with GET /v1/orders and filter on created_after. Poll no more than once a minute; there is nothing to gain from tighter loops, since statuses change at courier pace.
Create and update products
One endpoint keyed on your sku: an unknown SKU creates the product, a known one updates it. So you can send your whole catalogue on a schedule without checking what already exists.
- 201 Created — this SKU was new.
- 200 OK — a product with this SKU existed and was updated.
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 }
]
}'Variants are matched by SKU, and one you do not list is left untouched — never deleted. A variant usually has order history behind it, so a sync that happens to omit one must not destroy it. Delete a variant in the Cashod dashboard.
Images work the other way: omit the images field and the existing ones are left alone; send a list and it becomes the set; send [] to clear them.
cost_price is required. Cashod computes COGS and profit from it, and a product created without one reports a 100% margin on every order it appears in. On an update it is ignored once there is stock bought against the product — changing it then would rewrite the cost of stock you already hold.
Setting stock on a variant through this endpoint also needs the stock:write scope. Without it the call is refused rather than quietly saving everything except the stock — so a catalogue key stays a catalogue key.
Two conflicts are worth handling: another product in the store already uses that name (names are unique per store, separately from SKUs), or one of your variant SKUs belongs to a different product. Both come back as 409 conflict naming the collision.
Keep stock in sync
Stock is set to an absolute level, not adjusted by a delta. Send the level you want the variant to have — that way a retried call cannot double-apply, which a -1 silently would.
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 }'Pagination
List endpoints are cursor-paginated, newest first. A response carries data, has_more and next_cursor. Pass that cursor back as ?cursor= for the next page. Page size defaults to 50 and caps at 100.
Cursors rather than page numbers because orders arrive constantly: with offsets, a new order shifts every later page down by one, so a client walking their history silently misses rows and re-reads others.
Errors
Every failure has the same shape:
{
"error": {
"type": "invalid_request",
"message": "No product or variant with SKU \"GHOST-1\" in this store."
}
}Branch on type, never on the wording of message — the wording may be improved, the type will not change.
invalid_request(400) — something in your payload. The message names it.authentication_error(401) — missing, unknown or revoked key.permission_error(403) — the key lacks the scope, or the account's subscription is not active.not_found(404) — no such order or product in this key's store.rate_limit_exceeded(429) — back off and retry.server_error(500) — our side. Safe to retry, and safe twice if you sent anexternal_id.
Rate limits
120 requests per minute per key. The limit is per key rather than per IP address, so another client on the same hosting cannot spend your allowance.
Versioning
The version is in the path: /v1. New optional fields may be added to responses within a version, so parse defensively and ignore what you do not recognise. Anything that would break an existing integration goes into a new version instead.