cashod
Développeurs · API v1

Connectez vos propres systèmes à Cashod.

L'API Cashod permet à vos systèmes d'envoyer des commandes COD dans Cashod, d'en récupérer le statut de livraison et de garder votre catalogue produits et votre stock synchronisés. C'est une API HTTP JSON — aucun SDK nécessaire.

URL de basehttps://api.cashod.ma/api/v1
Créer une commande
$ 
Réponse
Authentification

Une clé, une boutique, uniquement les scopes cochés.

Créez une clé dans Cashod sous Paramètres → Développeurs. Le secret n'est affiché qu'une fois, à la création, et ne peut plus être récupéré ensuite — en cas de perte, révoquez la clé et créez-en une autre.

Envoyez-la comme bearer token à chaque requête :

Authorization
Authorization: Bearer csk_live_…
Une clé appartient à une seule boutique.

Elle voit et modifie les données de cette boutique et rien d'autre, vous n'avez donc jamais à transmettre d'identifiant de boutique — la clé indique déjà de quelle boutique il s'agit. Un marchand qui gère plusieurs boutiques crée une clé par boutique.

Scopes

Chaque clé porte aussi des scopes, cochés à sa création. Un scope d'écriture accorde aussi la lecture correspondante : une clé qui crée des commandes peut donc les relire. Donnez une clé en lecture seule à un script de reporting et il ne pourra rien créer ni annuler, même en cas de fuite.

  • orders:readLire les commandes et leur statut de livraison
  • orders:writeCréer des commandes — accorde aussi orders:read
  • products:readLire le catalogue produits
  • products:writeCréer et modifier des produits — accorde aussi products:read
  • stock:writeDéfinir le niveau de stock des variantes
Démarrage rapide

Votre première commande en trois étapes.

  1. 01
    Créez une clé

    Dans Cashod, ouvrez Paramètres → Développeurs, cochez les scopes nécessaires et copiez le secret — il n'est affiché qu'une seule fois.

  2. 02
    Envoyez une commande

    Envoyez-la en POST sur /v1/orders avec votre propre external_id, pour qu'une requête relancée ne crée jamais un second colis.

  3. 03
    Suivez sa livraison

    Lisez status, confirmation_status et tracking_number depuis /v1/orders/{id} au fil de l'acheminement par le transporteur.

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 } ]
  }'
Endpoints

Une API compacte, maîtrisée en un après-midi.

Tout se trouve sous https://api.cashod.ma/api/v1.

POST/v1/orders

Créez une commande depuis votre checkout. Relance sans risque grâce à external_id.

Scopeorders:write
GET/v1/orders/{id}

Lisez une commande : statut d'expédition, statut de confirmation et numéro de suivi.

Scopeorders:read
GET/v1/orders

Listez les commandes, les plus récentes d'abord, filtrez sur created_after, paginez avec un curseur.

Scopeorders:read
POST/v1/products

Créez ou modifiez un produit par SKU, avec ses variantes et ses images.

Scopeproducts:write
PATCH/v1/products/{productId}/variants/{variantId}/stock

Définissez le stock d'une variante à un niveau absolu.

Scopestock:write
GET/v1/openapi.json

Le document OpenAPI lisible par machine à partir duquel la référence est générée.

Assistant IA

Construisez-la avec un assistant IA

Vous utilisez un assistant de code IA ? Copiez ce prompt au lieu de retranscrire la page à la main. Il ne contient jamais de clé : le code produit lit la vôtre depuis une variable d'environnement CASHOD_API_KEY.

Collez-le dans ChatGPT, Claude, Cursor ou Lovable. Il contient les règles de cette page et un lien vers le document OpenAPI, pour que l'assistant écrive l'intégration adaptée à votre stack.

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

Ce que la liste des champs ne dit pas.

Créer une commande

Identifiez chaque article par son SKU — SKU de variante ou SKU de produit, selon votre façon de vendre. Omettez unit_price et le prix du catalogue s'applique ; envoyez-le et il prime, ce qui est utile quand la landing page affichait une promotion.

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 } ]
  }'

Idempotence — envoyez external_id

Envoyez votre propre identifiant de commande dans external_id et l'appel peut être relancé sans risque. Si le même external_id arrive à nouveau, Cashod renvoie la commande déjà créée au lieu d'en créer une seconde :

  • 201 Created — une nouvelle commande a été créée.
  • 200 OK — cet external_id avait déjà une commande ; elle est renvoyée telle quelle.

C'est plus important qu'il n'y paraît. Une requête qui expire a généralement bien été traitée ; sans external_id, votre relance devient une seconde vraie commande, confiée à un vrai transporteur, et le client reçoit deux colis.

Lire le statut de livraison

Une commande porte status (expédition), confirmation_status (l'appel de confirmation) et tracking_number une fois expédiée.

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

Pour suivre de nombreuses commandes, listez-les avec GET /v1/orders et filtrez sur created_after. N'interrogez pas plus d'une fois par minute ; des boucles plus serrées n'apportent rien, les statuts évoluant au rythme du transporteur.

Créer et modifier des produits

Un seul endpoint, indexé sur votre sku : un SKU inconnu crée le produit, un SKU connu le met à jour. Vous pouvez ainsi envoyer tout votre catalogue à intervalles réguliers sans vérifier ce qui existe déjà.

  • 201 Created — ce SKU était nouveau.
  • 200 OK — un produit avec ce SKU existait et a été mis à jour.
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 }
    ]
  }'

Les variantes sont associées par SKU, et celles que vous ne listez pas restent intactes — jamais supprimées. Une variante a généralement un historique de commandes : une synchronisation qui en omet une ne doit pas la détruire. Supprimez une variante depuis le tableau de bord Cashod.

Les images fonctionnent à l'inverse : omettez le champ images et les images existantes restent en place ; envoyez une liste et elle les remplace ; envoyez [] pour les supprimer.

cost_price est obligatoire. Cashod en déduit le COGS et le bénéfice, et un produit créé sans lui affiche une marge de 100 % sur chaque commande où il figure. Lors d'une mise à jour, il est ignoré dès qu'un stock a été acheté pour ce produit — le modifier réécrirait le coût du stock que vous détenez déjà.

Définir stock sur une variante via cet endpoint nécessite aussi le scope stock:write. Sans lui, l'appel est refusé plutôt que d'enregistrer discrètement tout sauf le stock — une clé catalogue reste ainsi une clé catalogue.

Deux conflits méritent d'être gérés : un autre produit de la boutique utilise déjà ce nom (les noms sont uniques par boutique, indépendamment des SKU), ou l'un de vos SKU de variante appartient à un autre produit. Les deux renvoient 409 conflict en précisant la collision.

Garder le stock synchronisé

Le stock est défini à un niveau absolu, pas ajusté par un écart. Envoyez le niveau que la variante doit avoir — ainsi, un appel relancé ne peut pas s'appliquer deux fois, ce qu'un -1 ferait sans prévenir.

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 }'

Pagination

Les endpoints de liste sont paginés par curseur, les plus récents d'abord. Une réponse contient data, has_more et next_cursor. Renvoyez ce curseur en ?cursor= pour la page suivante. La taille de page est de 50 par défaut et plafonnée à 100.

Des curseurs plutôt que des numéros de page, car les commandes arrivent en continu : avec des offsets, chaque nouvelle commande décale toutes les pages suivantes d'un cran, et un client qui parcourt son historique manque des lignes et en relit d'autres sans s'en rendre compte.

Erreurs

Chaque échec a la même forme :

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

Basez votre logique sur type, jamais sur le libellé de message — le libellé peut être amélioré, le type ne changera pas.

400invalid_requestUn problème dans votre payload. Le message l'indique.
401authentication_errorClé absente, inconnue ou révoquée.
403permission_errorLa clé n'a pas le scope, ou l'abonnement du compte n'est pas actif.
404not_foundAucune commande ou produit de ce type dans la boutique de cette clé.
429rate_limit_exceededPatientez puis réessayez.
500server_errorProblème de notre côté. Relance sans risque, même deux fois si vous avez envoyé un external_id.

Limites de débit

0requêtes par minute, par clé

120 requêtes par minute et par clé. La limite s'applique par clé et non par adresse IP, pour qu'un autre client sur le même hébergement ne puisse pas consommer votre quota.

Versions

La version figure dans le chemin : /v1. De nouveaux champs optionnels peuvent être ajoutés aux réponses au sein d'une version : parsez de façon défensive et ignorez ce que vous ne reconnaissez pas. Tout ce qui casserait une intégration existante passe dans une nouvelle version.

Référence API

Chaque endpoint, chaque champ.

Générée à partir du document OpenAPI du backend lui-même : elle évolue avec le code à chaque déploiement et ne peut pas devenir obsolète.

openapi.json
API Cashod pour développeurs : commandes, stock, webhooks