Aller au contenu
CashChap CashChap Docs

API v2 v2

L'API REST de CashChap : encaissements Mobile Money, suivi des transactions, retraits, terminaux de paiement et liens de paiement. Une seule adresse, une seule clé par appel, des réponses toujours de la même forme, des messages en français. La spécification OpenAPI décrit les mêmes routes pour vos outils.

https://api.cashchap.com/v2

Version de l'API

L'adresse ne porte que la version majeure (/v2). La version complète est renvoyée dans l'en-tête de réponse CashChap-Version, aujourd'hui 2.4.0 :

  • il est présent sur toutes les réponses /v2, succès comme erreurs (401, 404, 422, 429, 5xx…), y compris la réponse au préflight CORS ;
  • un script d'une autre origine peut le lire (response.headers.get("CashChap-Version")) : l'API envoie Access-Control-Expose-Headers: CashChap-Version, Retry-After, X-Request-Id, ce qui rend aussi lisibles Retry-After (après un 429) et X-Request-Id ;
  • une évolution compatible change la version mineure ou corrective (2.x.y) sans changer l'adresse ; une rupture changerait l'adresse (/v3).

Chaque version publiée est décrite dans le journal des changements.

Authentification

Chaque appel porte une seule clé, dans l'en-tête Authorization :

HTTP
Authorization: Bearer VOTRE_CLE
Content-Type: application/json

Le type de clé détermine qui appelle, ce qu'il peut faire et le canal des transactions créées. Le mode (Live ou Sandbox) est celui de la clé : il n'y a pas de paramètre de mode.

CléOù la trouverUsageCanal
Clé secrète SKLIVE_… / SKSANDBOX_…Portail, Développeurs › Clés API (après votre mot de passe)Votre serveur uniquement : toutes les routes, données complètes des clients, retraits, terminaux.api
Clé publique PKLIVE_… / PKSANDBOX_…Portail, Développeurs › Clés APIPages web et applications : moyens de paiement, devis, encaissement, lecture d'une transaction (données du client masquées).widget
Clé de terminal tk_live_… / tk_test_…Remise une seule fois à l'activation d'un TPEUn terminal de paiement : encaisser, lire ses propres transactions.pos
Jeton de session ts_…Créé par votre serveur (sessions de caisse)Une caisse tenue par un collaborateur, 8 heures au plus.shop ou pos
  • Clé absente : 401 missing_api_key (avec l'en-tête WWW-Authenticate) ; clé inconnue ou désactivée : 401 invalid_api_key.
  • Route réservée à la clé secrète : 403 secret_key_required.
  • La clé secrète ne doit jamais figurer dans une page web, une application mobile ou un dépôt de code : si elle a pu être exposée, régénérez les clés depuis le portail.

Format des réponses

Toutes les réponses sont du JSON, avec un vrai code HTTP et une seule enveloppe. Il n'y a jamais de champ success : un code 2xx signifie que la demande est acceptée.

Succès : un objet
HTTP/1.1 200 OK
CashChap-Version: 2.4.0
X-Request-Id: req_01JA2Q8X3T9Z0F6V4M2K7C1B5N

{
  "data": { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "object": "transaction", "status": "succeeded", "...": "..." },
  "meta": { "request_id": "req_01JA2Q8X3T9Z0F6V4M2K7C1B5N" }
}
Succès : une liste
{
  "data": [ { "id": "…", "object": "transaction" }, { "id": "…", "object": "transaction" } ],
  "meta": { "request_id": "req_…", "has_more": true, "next_cursor": "eyJpZCI6…", "limit": 20 }
}
Erreur
HTTP/1.1 422 Unprocessable Entity
CashChap-Version: 2.4.0
X-Request-Id: req_01JA2QB4N8F3K2M7X9T5V1C6ZD

{
  "error": {
    "code": "validation_failed",
    "type": "validation_error",
    "message": "La requête contient 2 champs invalides.",
    "details": {
      "fields": [
        { "field": "amount", "code": "invalid_amount", "message": "Le montant doit être un nombre JSON (sans guillemets) supérieur à 0." },
        { "field": "customer.email", "code": "invalid_email", "message": "L'adresse e-mail est invalide." }
      ]
    },
    "request_id": "req_01JA2QB4N8F3K2M7X9T5V1C6ZD",
    "doc_url": "https://cashchap.com/docs/v2/erreurs#validation_failed"
  }
}
error.code
Code stable, en anglais (snake_case) : c'est lui que votre programme teste. Liste complète : Erreurs de l'API v2.
error.type
Famille de l'erreur (validation_error, payment_failed, rate_limited…), liée au code HTTP.
error.message
Phrase en français, affichable telle quelle à votre client ou à votre équipe.
error.details
Précisions : field, fields (erreurs de saisie, une par champ), transaction_id (référence déjà utilisée), limits… Objet vide sinon.
error.request_id
Identifiant de l'appel, aussi dans l'en-tête X-Request-Id et dans vos journaux d'API du portail : donnez-le au support.
error.doc_url
Lien vers l'explication du code : https://cashchap.com/docs/v2/erreurs#<code>.
HTTPSignification
200Lecture ou action réussie.
201Ressource créée (terminal, session de caisse).
202Encaissement ou retrait accepté : transaction créée, résultat à suivre.
400 / 413Requête mal formée (JSON invalide, en-tête manquant, corps de plus de 32 Ko).
401 / 403Clé absente ou invalide / opération non permise à cette clé ou à ce compte.
404 / 405Ressource ou route inconnue / méthode non acceptée.
409Conflit : référence déjà utilisée, clé d'idempotence réutilisée, annulation impossible.
422Champs invalides ou demande refusée par une règle (opérateur, devise, plafonds…).
429Trop de requêtes : respectez l'en-tête Retry-After.
5xxProblème chez CashChap ou chez l'opérateur : réessayez avec la même clé d'idempotence.

Montants, dates et champs

  • Montants : nombres JSON dans l'unité de la devise (1000 = 1 000 XOF, 12.5 = 12,50 EUR), jamais des chaînes ni des centimes. Le nombre de décimales suit la devise (aucune en XOF). Chaque montant est accompagné de sa devise.
  • Dates : ISO 8601 en UTC, avec Z (2026-10-11T09:14:21Z). Le Bénin est à UTC+1.
  • Clés : en anglais, snake_case. Un champ absent de la requête vaut sa valeur par défaut ; un champ inconnu est refusé (unknown_field), pour détecter tout de suite une faute de frappe.
  • Valeurs absentes : les champs sont toujours présents dans les réponses ; une valeur inconnue vaut null.
  • Numéros : au format international obligatoire, sans « + » : indicatif du pays puis numéro national, par exemple 2290197000000 pour le Bénin. Un numéro national seul (0197000000) est refusé avec phone_international_required. Le pays est déduit de l'indicatif, et les réponses rendent toujours le numéro sous cette forme. Détails : Numéros de téléphone.

Numéros de téléphone

Tout champ phone de l'API v2 (encaissement, paiement d'un lien, retrait, vérification d'un numéro) s'écrit au format international, sans « + » : l'indicatif du pays, puis le numéro national, en chiffres. Au Bénin : 229, puis 01 et 8 chiffres, soit 2290197000000.

  • Tolérances en entrée : un + ou un 00 en tête (+2290197000000, 002290197000000) et les séparateurs (espaces, points, tirets, parenthèses : 229 01 97 00 00 00, (+229) 01-97-00-00-00) sont acceptés. Les réponses ne les reprennent jamais.
  • Pays déduit de l'indicatif : la partie nationale est contrôlée selon ce pays. L'ancien format béninois à 8 chiffres avec indicatif (22997000000) est accepté et complété en 2290197000000.
  • Numéro national refusé : un numéro qui commence par 0 (même précédé de + ou 00) est refusé avec phone_international_required. Sans + ni 00, un numéro n'est lu comme international que s'il commence par un indicatif connu et compte au moins 10 chiffres ; sinon, même code. Avec + ou 00, les premiers chiffres sont lus comme l'indicatif : un indicatif inconnu ou une partie nationale de mauvaise longueur donnent invalid_phone_number (+66000000 est lu avec l'indicatif 66).
  • Opérateur du même pays : le numéro doit être un numéro du pays de l'opérateur choisi (un numéro 226… n'est pas accepté par mtn-benin).
  • country facultatif : code ISO 3166-1 du pays du numéro (BJ, ou bj : la casse est ignorée). Il ne complète jamais un numéro national : il sert seulement au contrôle, et à départager un indicatif commun à plusieurs pays. S'il contredit l'indicatif : phone_country_mismatch.
  • Réponses : customer.phone vaut 2290197000000 avec la clé secrète ; masqué (22901••••0000 : indicatif et 2 chiffres au début, 4 chiffres à la fin) avec les autres clés. Les webhooks utilisent le même format.
Code (422)QuandExemple de message
phone_international_requiredNuméro national seul (0197000000, 97000000).« Le numéro de téléphone doit être au format international, indicatif du pays compris (exemple pour le Bénin : 2290166000000). »
invalid_phone_numberIndicatif inconnu, longueur ou caractères invalides, numéro d'un autre pays que l'opérateur.« Numéro de téléphone invalide pour Bénin (indicatif 229) : 10 chiffres attendus après l'indicatif, par exemple 2290197000000. »
phone_country_mismatchcountry contredit l'indicatif ou n'est pas un code ISO connu.« Le numéro commence par l'indicatif 229 (Bénin) alors que country vaut TG (Togo, indicatif 228). Le pays est déduit de l'indicatif : corrigez le numéro ou retirez country. »

Quand le numéro est le seul refus de la requête, la réponse porte ce code et details.field = phone (country pour phone_country_mismatch). Avec d'autres erreurs de saisie, la réponse est validation_failed et le code figure dans details.fields[].code.

Numéro national refusé
HTTP/1.1 422 Unprocessable Entity
CashChap-Version: 2.4.0

{
  "error": {
    "code": "phone_international_required",
    "type": "validation_error",
    "message": "Le numéro de téléphone doit être au format international, indicatif du pays compris (exemple pour le Bénin : 2290166000000).",
    "details": { "field": "phone" },
    "request_id": "req_01JA9ZK3Q7T2M4D8N6B1X5C0VE",
    "doc_url": "https://cashchap.com/docs/v2/erreurs#phone_international_required"
  }
}

Idempotence

Les créations d'argent (POST /v2/collections, POST /v2/payouts, POST /v2/payment-links/{id}/collections) exigent un en-tête Idempotency-Key : une valeur unique par opération, de 1 à 128 caractères (lettres, chiffres, - et _), par exemple un UUID ou votre numéro de commande suivi d'un numéro d'essai.

  • Rejouer la même requête avec la même clé (coupure réseau, délai dépassé, erreur 5xx) rend la même réponse, avec l'en-tête Idempotent-Replayed: true, sans créer une seconde transaction.
  • La même clé avec un corps différent : 409 idempotency_conflict. Une requête encore en cours avec cette clé : 409 request_in_progress et Retry-After: 1.
  • Une erreur de saisie (4xx) libère la clé : corrigez la requête et renvoyez-la avec la même clé.
  • Les réponses sont gardées 72 heures. L'identifiant de la transaction est toujours choisi par CashChap.
  • Combinez-la avec une reference unique par commande : la clé protège un appel rejoué, la référence protège la commande contre un second paiement (duplicate_reference).

Limites

  • Débit : 120 requêtes par minute et par clé (comptées par clé et par adresse IP pour les clés publique et secrète), et 600 par minute et par adresse IP. Au-delà : 429 rate_limited avec Retry-After.
  • Corps : 32 Ko au plus (request_too_large).
  • Attente longue : ?wait= sur la lecture d'une transaction attend au plus 5 secondes ; au-delà de 2 attentes simultanées par clé, la réponse est immédiate (en-tête CashChap-Wait: skipped), sans erreur.
  • Mode test : au plus 60 créations par minute et par clé, et un quota quotidien par service (sandbox_rate_limited).
  • Contrôles antifraude : trop de demandes vers un même numéro ou pour un même marchand en peu de temps sont refusées (429). Voir limites de fréquence.
  • Délais : prévoyez 30 secondes de délai d'attente pour vos appels.

L'objet transaction

Encaissements, retraits Mobile Money et virements bancaires ont la même forme, dans toutes les réponses :

Encaissement réussi (vue avec la clé secrète)
{
  "id": "2f1c3b6e-9a0d-4c1e-8f5a-2b7d9e4c6a10",
  "object": "transaction",
  "type": "collection",
  "status": "succeeded",
  "pending_reason": null,
  "mode": "live",
  "channel": "api",
  "reference": "CMD-2026-00417",
  "description": "Commande A-2026-00417",
  "amount": 2500,
  "currency": "XOF",
  "fees": {
    "amount": 43,
    "currency": "XOF",
    "payer": "merchant",
    "rule": { "kind": "percentage", "value": 1.7, "level": "platform", "channel": null, "terminal": null }
  },
  "amount_charged": 2500,
  "charged_currency": "XOF",
  "net_amount": 2457,
  "net_currency": "XOF",
  "exchange": null,
  "display": {
    "currency": "XOF", "amount": 2500, "fees": 0, "amount_charged": 2500, "net_amount": 2457, "fees_under_minimum": false,
    "rate": { "value": 1, "per_unit": 1, "at": null, "source": "origin" }
  },
  "customer": { "name": "Koffi Mensah", "email": "koffi.mensah@example.com", "phone": "2290197000000" },
  "payment_method": { "type": "mobile_money", "network": "mtn", "card_last4": null },
  "provider": "mtn-benin",
  "terminal": null,
  "operator": null,
  "payment_link": null,
  "failure": null,
  "metadata": { "commande": "A-2026-00417" },
  "available": true,
  "refunded_amount": 0,
  "created_at": "2026-10-11T09:14:02Z",
  "completed_at": "2026-10-11T09:14:21Z"
}
id
Identifiant CashChap de la transaction (UUID).
type
collection (encaissement) ou payout (retrait ou virement).
status
pending, succeeded, failed, expired ou cancelled (cycle de vie).
pending_reason
En attente seulement : awaiting_provider (validation du client ou réponse de l'opérateur) ou awaiting_bank_transfer (virement à traiter).
mode
live ou sandbox.
channel
Canal déduit de la clé : api, widget, payment_link, shop, pos, dashboard.
reference
Votre référence, unique par environnement.
amount currency
Prix demandé, dans la devise demandée.
amount_charged charged_currency
Total débité au client (encaissement) ou débité de votre solde (retrait, frais compris), dans la devise de l'opérateur. En XOF, multiple de 5 (arrondi).
fees
Commission de CashChap (amount, currency), qui la paie (payer : merchant ou customer) et la règle appliquée (rule : kind, value, level, channel, terminal).
net_amount net_currency
Ce que reçoit votre solde, en XOF.
exchange
Conversion appliquée (from, to, rate, amount), ou null. Depuis 2.3.0, quand le paiement porte des frais de change : fee (montant, devise pivot), fee_currency et fee_payer (customer : compris dans amount_charged ; merchant : retenus sur net_amount). Depuis 2.4.0, quand le prix est dans une devise à taux variable prélevée dans une autre devise, la marge de change : margin (montant, devise pivot), margin_currency, margin_percent (pourcentage appliqué) et margin_payer (customer ou merchant) ; ces quatre champs ne sont rendus qu'avec une clé secrète, de terminal ou de session.
display
Depuis 2.2.0 : montants tels que montrés au payeur, dans la devise du service (bloc display). Ajout seulement : les champs ci-dessus ne changent pas.
customer
name, email, phone. Masqués (« Koffi M. », « k•••@example.com », « 22901••••0000 ») avec une clé autre que la clé secrète.
payment_method
type (mobile_money, card, direct_debit, bank_transfer), network (mtn, moov…).
provider
Opérateur utilisé (mtn-benin, moov-benin…), y compris quand il a été choisi automatiquement : dès la réponse 202, il indique l'opérateur retenu.
terminal operator, payment_link
Terminal (id, label, point_of_sale), collaborateur qui a encaissé (id, name) et lien de paiement (id, title) à l'origine de la transaction, ou null.
failure
Échec ou expiration : code (par exemple insufficient_funds) et message en français. null sinon.
metadata description
Vos données, renvoyées telles quelles.
available
Encaissement : fonds disponibles sur votre solde (true) ou encore en cours (mise à disposition). null pour un retrait.
funds
Ajout compatible (2.5.0), encaissement seulement, jamais dans les réponses destinées au payeur : status = available, pending (mise à disposition en cours), pending_confirmation (encaissement à confirmer : fonds versés sur le compte bancaire de CashChap, en attente de la confirmation de leur réception), held (vérification CashChap), withheld (retenu après vérification) ou null (paiement non réussi) ; simulated : true en Sandbox.
refunded_amount
Montant remboursé au payeur (0 sinon).
created_at completed_at
Création et fin (statut final), en UTC.
receipt_token
Réponse de création d'un encaissement seulement (POST /v2/collections, POST /v2/payment-links/{id}/collections) : jeton du payeur, 22 caractères. Présenté avec ?receipt_token=, il ouvre le reçu complet sans clé secrète. Absent des lectures, des listes et des webhooks : gardez-le dans la page qui a lancé le paiement.
payout
Virement bancaire seulement : state (to_process, in_progress, transferred, rejected, cancelled), bank, account_masked, bank_reference, transfer_on.

Bloc display : la devise du service (2.2.0)

Quand le service est réglé sur une devise (par exemple l'euro), votre client voit toute la chaîne de paiement dans cette devise ; l'opérateur prélève toujours dans la sienne (XOF au Bénin) et votre solde reste en XOF. Le bloc display donne ces montants, sur transaction (lectures et réponse 202), quote, receipt et payment_link :

Paiement d'un lien à 25 EUR : 16 845 XOF prélevés dont 446 XOF de frais (164 XOF de frais de change)
"display": {
  "currency": "EUR",
  "amount": 25,
  "fees": 0.68,
  "amount_charged": 25.68,
  "net_amount": 25,
  "fees_under_minimum": false,
  "exchange_fee": 0.25,
  "rate": { "value": 0.001524490172374, "per_unit": 655.957, "at": "2026-10-09T22:12:28Z", "source": "fixed" }
}
currency
Devise du service (sinon du compte) au moment du paiement : celle que voit le payeur.
amount fees, amount_charged
Prix, frais et total payé, dans cette devise. Les frais sont la différence total − prix : amount + fees = amount_charged au centime près. Une seule conversion, depuis la valeur XOF enregistrée (ou le prix d'origine s'il est déjà dans cette devise), arrondie au plus proche.
net_amount
Net crédité, converti au même taux. Absent des réponses sans clé (lien de paiement public, reçu).
exchange_fee
Depuis 2.3.0 : part de fees qui correspond aux frais de change payés par le client (prix fixé dans une autre devise que celle de l'opérateur), dans la même devise ; 0 sans frais de change ou quand ils sont retenus sur votre net. La marge de change (2.4.0) payée par le client est comprise dans fees, jamais dans exchange_fee.
fees_under_minimum
true quand le client paie des frais réels inférieurs à la plus petite unité (« moins de 0,01 EUR »).
rate
Taux utilisé : value (1 XOF = value × devise), per_unit (1 devise en XOF), at, source : origin (aucune conversion), fixed (parité fixe : XOF, XAF, EUR), transaction (taux du moment du paiement, devise à taux variable), history ou approx (paiement antérieur, taux de sa date).

Sur payment_link, le bloc ne donne que le prix à montrer : currency, amount (null pour un prix libre), converted (devise du lien différente de celle du service) et rate ; voir API publique des liens. Sur receipt et sur le devis public d'un lien, net_amount est absent. Un serveur qui ne renvoie pas encore le bloc (avant 2.2.0) se lit avec les anciens champs : votre page doit alors montrer amount_charged et charged_currency.

Le bloc est figé à la création du paiement : le reçu et la facture du client ne changent jamais, même si la devise du service change ensuite. Le texte summary du devis est rédigé dans cette devise (« Vous paierez 25,68 EUR, dont 0,68 EUR de frais (frais de change de 0,25 EUR compris). »). Les moyens de paiement donnent display_currency, display_min_amount (arrondi au supérieur) et display_max_amount (arrondi à l'inférieur) ; les canaux api, shop et pos acceptent aussi la devise du service dans currency, convertie par CashChap.

Moyens de paiement du service

GET /v2/payment-methods — toutes les clés. Liste à plat des moyens ouverts pour le service et le canal de la clé. Filtres facultatifs : amount, currency, country.

JSON
{
  "data": [
    {
      "provider": "mtn-benin",
      "name": "MTN Mobile Money",
      "type": "mobile_money",
      "network": "mtn",
      "country": "BJ",
      "currency": "XOF",
      "decimals": 0,
      "currencies": ["XOF"],
      "min_amount": 100,
      "max_amount": 5000000,
      "display_currency": "XOF",
      "display_min_amount": 100,
      "display_max_amount": 5000000,
      "fees_preview": { "kind": "percentage", "value": 1.7, "payer": "customer" },
      "phone": { "length": 10, "prefix_length": 4, "prefixes": ["0142", "0146", "…"], "country_code": "229" },
      "logo_url": "https://…"
    }
  ],
  "meta": { "request_id": "req_…", "has_more": false, "next_cursor": null, "limit": 1 }
}

phone décrit le numéro national attendu (longueur, préfixes) et phone.country_code l'indicatif du pays de l'opérateur (« 229 ») : placez-le devant le numéro national pour obtenir le format international sans « + » (« 2290166000000 »).

min_amount et max_amount sont dans la devise de l'opérateur (currency). Depuis 2.2.0, display_currency est la devise du service, celle que voit le client, et display_min_amount (arrondi au supérieur) et display_max_amount (arrondi à l'inférieur) les mêmes plafonds dans cette devise : sur un service en euros, "currencies": ["XOF", "EUR"], "display_currency": "EUR", 0.16 et 7622.45. Un montant saisi égal à un plafond affiché est accepté.

fees_preview donne la commission générale de l'opérateur ; le devis donne le total exact pour votre canal. GET /v2/session indique qui appelle : type de clé, mode, canal, service (name, public_name à afficher au client), terminal et collaborateur.

Demander un paiement (encaissement)

POST /v2/collections — clé secrète, publique, de terminal ou de session. En-tête Idempotency-Key obligatoire. Réponse 202 : la transaction est créée en pending et lisible tout de suite ; l'en-tête Location donne son adresse.

Corps de la requête

amount obligatoire
Prix, nombre JSON supérieur à 0, décimales selon la devise.
currency obligatoire
Code ISO 4217 d'une devise active (XOF…). Avec la clé secrète, une clé de terminal ou une session de caisse, la devise de l'opérateur ou celle du service (depuis 2.2.0) ; CashChap convertit vers la devise de l'opérateur.
provider obligatoire
Opérateur (mtn-benin, moov-benin…) ou auto pour le choix automatique d'après le numéro.
payment_method
mobile_money (par défaut), card ou direct_debit.
phone obligatoire hors carte
Numéro Mobile Money du payeur, au format international, sans « + » (2290197000000), du pays de l'opérateur choisi (Numéros de téléphone).
country
Facultatif : code ISO du pays du numéro (BJ…). Il sert seulement au contrôle : refus phone_country_mismatch s'il contredit l'indicatif.
customer obligatoire en clé secrète, publique ou lien
name (255 caractères au plus) et email du payeur. Facultatif depuis un terminal ou une caisse.
reference
Votre référence, 1 à 64 caractères (lettres, chiffres, _ . : / -), unique par environnement.
description
Texte libre de 255 caractères au plus.
metadata
Objet de 20 clés au plus (a-z, 0-9, _, 40 caractères ; préfixe cashchap_ réservé), valeurs texte de 500 caractères au plus.
card
Carte seulement, et seulement si elle est ouverte : number, exp_month, exp_year, cvc. À n'utiliser que si votre serveur respecte les exigences de sécurité des données de carte ; sinon passez par le widget.

Les champs channel, initBy, externalId ou customer.fullname de la v1 sont refusés (unknown_field).

Exemples

curl
curl -X POST https://api.cashchap.com/v2/collections \
  -H "Authorization: Bearer $CASHCHAP_CLE_SECRETE" \
  -H "Idempotency-Key: CMD-2026-00417-1" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 2500,
    "currency": "XOF",
    "provider": "auto",
    "phone": "2290197000000",
    "customer": { "name": "Koffi Mensah", "email": "koffi.mensah@example.com" },
    "reference": "CMD-2026-00417",
    "metadata": { "commande": "A-2026-00417" }
  }'
PHP
<?php
// PHP 8 avec cURL. La clé secrète vient de la configuration du serveur, jamais du code source.
$corps = [
    'amount' => 2500,
    'currency' => 'XOF',
    'provider' => 'auto',
    'phone' => '2290197000000',
    'customer' => ['name' => 'Koffi Mensah', 'email' => 'koffi.mensah@example.com'],
    'reference' => 'CMD-2026-00417',
];
$ch = curl_init('https://api.cashchap.com/v2/collections');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CASHCHAP_CLE_SECRETE'),
        'Idempotency-Key: CMD-2026-00417-1',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($corps),
]);
$reponse = json_decode((string) curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($http === 202) {
    $transactionId = $reponse['data']['id']; // à enregistrer avec la commande
} else {
    // $reponse['error']['code'] pour votre programme, $reponse['error']['message'] pour l'affichage.
}
Node.js
// Node.js 18+ (fetch intégré).
const reponse = await fetch("https://api.cashchap.com/v2/collections", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CASHCHAP_CLE_SECRETE}`,
    "Idempotency-Key": "CMD-2026-00417-1",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    amount: 2500,
    currency: "XOF",
    provider: "auto",
    phone: "2290197000000",
    customer: { name: "Koffi Mensah", email: "koffi.mensah@example.com" },
    reference: "CMD-2026-00417",
  }),
});
const corps = await reponse.json();
if (reponse.status === 202) {
  console.log("Transaction créée", corps.data.id, corps.data.status); // pending
} else {
  console.error(corps.error.code, corps.error.message);
}

Lire une transaction

GET /v2/transactions/{id} — toutes les clés (une clé de terminal ne lit que ses transactions). Paramètre facultatif wait (0 à 20, plafonné à 5 secondes) : si la transaction est pending, la réponse attend son statut final pendant ce délai.

Terminal
curl "https://api.cashchap.com/v2/transactions/2f1c3b6e-9a0d-4c1e-8f5a-2b7d9e4c6a10?wait=5" \
  -H "Authorization: Bearer $CASHCHAP_CLE_SECRETE"
Encaissement échoué (extrait)
{
  "data": {
    "id": "4e1d2c3b-5a69-4b8c-9d0e-1f2a3b4c5d6e",
    "object": "transaction",
    "status": "failed",
    "failure": { "code": "insufficient_funds", "message": "Le solde Mobile Money du client est insuffisant." },
    "...": "..."
  },
  "meta": { "request_id": "req_…" }
}

Pour suivre un encaissement : lisez-le au retour du client, puis toutes les 5 à 10 secondes avec wait=5 tant qu'il est pending (deux minutes), puis toutes les minutes pendant quinze minutes. Combinez avec les webhooks.

Lister les transactions

GET /v2/transactions — clé secrète (une clé de terminal ne voit que ses transactions des dernières 24 heures). Paramètres : limit (1 à 100, 20 par défaut), cursor (reprendre meta.next_cursor), status (une ou plusieurs valeurs séparées par des virgules), type, channel, terminal_id, reference, created_after, created_before (ISO 8601). Sans created_after, la liste couvre les 90 derniers jours. GET /v2/transactions?reference=CMD-2026-00417 retrouve une transaction par votre référence.

Reçu

GET /v2/transactions/{id}/receipt — format=json (par défaut) ou format=text avec width=32 ou 48 colonnes, pour une imprimante de caisse. Avec la clé publique, seulement pour une transaction réussie. La page d'un lien de paiement lit le même reçu sans clé : GET /v2/payment-links/{id}/transactions/{transaction}/receipt (paiement réussi de ce lien seulement).

Le reçu (objet receipt, API 2.1.0 ; bloc display depuis 2.2.0) contient tout ce qu'un reçu CashChap affiche :

  • merchant : public_name, service_name, logo_url, address, phone (international sans « + »), email ;
  • product (produit ou motif), amount, fees, amount_charged (total payé), payment_method.label (« MTN Mobile Money ») ; display : prix, frais et total dans la devise du service, tels que le payeur les a vus (écran final, image, reçu imprimé, facture et page de vérification)
  • payeur, selon l'appelant (voir ci-dessous) : customer_details (full ou masked), customer_phone (numéro complet au format international sans « + », 2290166000000), phone_display (lu par groupes : « 229 01 66 00 00 00 », masqué « 229 01 •• •• 00 00 »), customer_name (« Koffi Mensah », masqué « Koffi M. »), customer_email, et toujours phone_masked (« 22901••••0000 ») ;
  • channel : code (payment_link, shop_qr, checkout_qr, shop, pos, widget, api), label et display en français (« Caisse (QR code) · Caisse principale »), terminal_label, operator_name (caissier) ;
  • receipt_number, transaction_id (référence CashChap), provider_reference (référence opérateur), reference (votre référence de commande) ;
  • completed_at (UTC), completed_at_local et completed_at_text (« 12 octobre 2026 à 02 h 03 », heure de Porto-Novo), mode ;
  • verify_url : adresse de la page publique de vérification (https://cashchap.com/verifier/<code>), à imprimer en QR code ; invoice_url : facture PDF du paiement (https://cashchap.com/facture/<code>). Ces deux codes sont signés par CashChap : ils ne se devinent pas et un code modifié est refusé.

Reçu complet ou masqué. Le reçu remis au payeur et la facture portent ses coordonnées complètes : numéro, nom et prénom, e-mail s'il a été donné (champ absent, null, sinon). Ils ne sont donnés qu'au payeur et au marchand :

AppelantReçu rendu
Clé secrète ; terminal ou caisse (ses propres ventes)Complet, avec invoice_url.
Clé publique ou route d'un lien, avec ?receipt_token= (jeton rendu à la création du paiement)Complet, avec invoice_url : c'est ainsi que la page qui vient de payer affiche le reçu du payeur.
Clé publique ou route d'un lien, sans jeton ou avec un jeton fauxMasqué : customer_phone, customer_email et invoice_url valent null, nom « Koffi M. ».

La page de vérification affiche Paiement authentique (Live), Paiement de test (Sandbox) ou Reçu introuvable, avec le détail du paiement (numéro et nom du payeur masqués). Le reçu texte se termine par l'adresse de vérification. Détail de la page et de la facture dans Reçus et vérification.

Vérifier un numéro de téléphone

POST /v2/phone-numbers/validate avec {"phone": "2290197000000", "provider": "mtn-benin"} (country facultatif) : rend le numéro au format international sans « + » ("phone": "2290197000000") et le pays ("country": "BJ"), ou 422 avec une précision sur le format attendu : phone_international_required pour un numéro national seul, invalid_phone_number ou phone_country_mismatch. Utile pour valider un formulaire avant l'encaissement.

Lire vos soldes

GET /v2/balance — clé secrète. Ajout compatible (2.5.0). Soldes du service de la clé, dans la devise des montants nets (currency, comme net_currency), recalculés en continu :

available
Retirable maintenant.
pending
Encaissé, en cours de mise à disposition (hors encaissements à confirmer).
pending_confirmation
Paiements réussis dont les fonds, versés par l'opérateur sur le compte bancaire de CashChap, attendent la confirmation de leur réception ; ils entreront dans available dès cette confirmation. Nombre : pending_confirmation_count ; texte à montrer : pending_confirmation_message.
simulated
true en Sandbox : rien n'est retenu, pending_confirmation montre ce qui le serait en Live et ce montant est déjà compté dans available.
updated_at
Dernier recalcul des soldes.

Retrait Mobile Money

POST /v2/payouts — clé secrète, Idempotency-Key obligatoire. Le numéro doit être un compte autorisé actif du service. Réponse 202 avec la transaction (type = payout) en pending.

amount obligatoire
Montant à verser, en XOF.
currency obligatoire
XOF (seule devise de retrait).
provider obligatoire
Opérateur (mtn-benin, moov-benin), auto, ou une banque pour un virement.
phone
Numéro bénéficiaire (Mobile Money), au format international, sans « + » (2290197000000). country facultatif, comme pour un encaissement.
reference description
Votre référence et un libellé.
Terminal
curl -X POST https://api.cashchap.com/v2/payouts \
  -H "Authorization: Bearer $CASHCHAP_CLE_SECRETE" \
  -H "Idempotency-Key: versement-2026-10-11-1" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 50000, "currency": "XOF", "provider": "mtn-benin", "phone": "2290197000000", "reference": "VERS-2026-10-11" }'

Refus fréquents : recipient_not_authorized, insufficient_balance, payout_amount_too_small, payouts_temporarily_unavailable. Le résultat se lit comme un encaissement (GET /v2/transactions/{id}) et arrive par les webhooks withdrawal.*.

Virement bancaire

Même route, avec provider = uba-benin, ecobank-benin ou boa-benin et, si le service a plusieurs comptes de cette banque, bank_account_id (identifiant du compte autorisé ; sinon bank_account_ambiguous). Le virement est traité à la main par CashChap (pending_reason = awaiting_bank_transfer, suivi dans payout.state). Réservé au titulaire et aux administrateurs, en Live.

POST /v2/transactions/{id}/cancel — clé secrète : annule un virement encore to_process (sinon 409 cannot_cancel). Le montant et le frais réservés reviennent sur votre solde.

QR codes de caisse

Un QR de caisse (objet checkout) est un QR code à usage unique et à durée limitée, pour un montant fixé par la caisse : le client le scanne, paie ce montant sur la page de paiement CashChap, et la caisse suit le paiement. C'est ce qu'affiche Ma boutique avec « Afficher un QR ». Routes réservées à la clé secrète, à une clé de terminal ou à un jeton de session de caisse (Terminaux de paiement) ; la clé publique est refusée. Le terminal et le caissier sont ceux de l'authentification, jamais ceux du corps de la requête.

POST /v2/checkouts — Idempotency-Key facultative (la même clé rend le même QR, la même clé avec un autre corps répond 409 idempotency_conflict). Réponse 201 avec l'objet checkout et l'en-tête Location.

amount obligatoire
Montant demandé au client, en unités de la devise (15000 = 15 000 XOF). Le client paie ce montant, plus les frais à sa charge et l'arrondi, au tarif de la caisse.
currency
Devise du montant ; par défaut la devise du service (depuis 2.2.0 ; XOF avant). Précisez-la toujours : sur un service en EUR, {"amount": 5000} vaut 5 000 EUR.
expires_in
Durée de validité en secondes, entier de 60 à 900 (300 par défaut, soit 5 minutes) ; sinon invalid_expires_in dans details.fields.
description
Libellé facultatif, 255 caractères au plus.
  • url est le contenu du QR code à afficher : la page de paiement CashChap du QR, dans le mode de la clé (/live/… ou /sandbox/…). Dessinez-la avec n'importe quelle bibliothèque de QR code.
  • status : open (payable), processing (un client est en train de payer), succeeded (payé : le QR ne sert plus), expired (délai passé sans paiement), cancelled (annulé par la caisse).
  • expires_in : secondes restantes tant que le QR est open, sinon 0. transaction : la dernière demande de paiement lancée par ce QR (objet transaction, données du client masquées hors clé secrète).

GET /v2/checkouts/{id} — état du QR. Paramètre facultatif wait (0 à 20, plafonné à 5 secondes) : tant que le QR est open ou processing, la réponse attend un changement d'état pendant ce délai. Une caisse affiche « Paiement reçu » quand status vaut succeeded, puis peut imprimer le reçu de transaction.id (/v2/transactions/{id}/receipt).

POST /v2/checkouts/{id}/cancel — annule un QR open (réponse : l'objet, status = cancelled). Refus 409 checkout_not_cancellable si un paiement est en cours ou réussi.

Une clé de terminal ne voit que les QR de ce terminal, une clé secrète ceux de son environnement ; un autre QR répond 404 checkout_not_found. Au plus 30 QR créés par minute et par terminal (rate_limited).

Contrôles côté serveur. L'usage unique et l'expiration sont vérifiés par CashChap à chaque demande, pas par l'écran : sur les routes publiques du lien (aperçu, moyens de paiement, devis, paiement), un QR expiré répond 410 payment_link_expired, annulé 410 payment_link_cancelled, déjà payé 409 payment_link_already_paid, en cours de paiement par un autre téléphone 409 payment_link_in_progress. Un seul paiement est traité à la fois ; une demande rejouée avec la même Idempotency-Key rend la même transaction. Une demande acceptée avant l'expiration va à son terme ; un paiement échoué rend le QR de nouveau payable jusqu'à son expiration.

La vente est attribuée à la caisse qui a créé le QR : canal shop (ou pos pour un terminal de ce type), terminal et caissier de la caisse, tarif de ce canal ; dans le portail, elle figure dans Paiements marchand.

Autres routes

RouteCléUsage
GET /v2/sessiontoutesQui appelle : type de clé, mode, canal, service, terminal, collaborateur.
POST /v2/quotestoutes (retrait : secrète)Devis : total, frais, crédit, change.
GET /v2/currenciestoutesDevises actives.
GET /v2/exchange-rates?from=&to=toutesTaux de change.
GET /v2/payment-links/{id}
POST /v2/payment-links/{id}/collections
aucuneLiens de paiement (CashChap Link) : détail public et paiement d'un lien.
/v2/checkouts…secrète, terminalQR codes de caisse : création, état, annulation.
/v2/terminals…secrète, terminalTerminaux de paiement et sessions de caisse.
POST /v2/refunds, GET /v2/refunds/{id}secrètePas encore disponible : 501 not_implemented. Les remboursements se demandent au portail.
/v2/webhook_endpoints…, /v2/webhook_event_typessecrèteWebhooks du service dans le mode de la clé (ajout, liste, suppression ; types d'événements) : Webhooks.
GET /v2/events, /v2/events/{id}secrètePas encore disponible : 501.

Toute autre adresse sous /v2 répond 404 route_not_found.

Journal des appels

Chaque appel apparaît dans Développeurs › Journaux API du portail, pour l'environnement affiché : méthode, chemin, code HTTP, durée, adresse IP, request_id, requête et réponse (secrets et numéros masqués), puis le résultat final de la transaction. Consultez-le avant de contacter le support et donnez le request_id.