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.
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 envoieAccess-Control-Expose-Headers: CashChap-Version, Retry-After, X-Request-Id, ce qui rend aussi lisiblesRetry-After(après un 429) etX-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 :
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 trouver | Usage | Canal |
|---|---|---|---|
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 API | Pages 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 TPE | Un 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êteWWW-Authenticate) ; clé inconnue ou désactivée : 401invalid_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.
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" }
}
{
"data": [ { "id": "…", "object": "transaction" }, { "id": "…", "object": "transaction" } ],
"meta": { "request_id": "req_…", "has_more": true, "next_cursor": "eyJpZCI6…", "limit": 20 }
}
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-Idet 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>.
| HTTP | Signification |
|---|---|
200 | Lecture ou action réussie. |
201 | Ressource créée (terminal, session de caisse). |
202 | Encaissement ou retrait accepté : transaction créée, résultat à suivre. |
400 / 413 | Requête mal formée (JSON invalide, en-tête manquant, corps de plus de 32 Ko). |
401 / 403 | Clé absente ou invalide / opération non permise à cette clé ou à ce compte. |
404 / 405 | Ressource ou route inconnue / méthode non acceptée. |
409 | Conflit : référence déjà utilisée, clé d'idempotence réutilisée, annulation impossible. |
422 | Champs invalides ou demande refusée par une règle (opérateur, devise, plafonds…). |
429 | Trop de requêtes : respectez l'en-tête Retry-After. |
5xx | Problè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
2290197000000pour le Bénin. Un numéro national seul (0197000000) est refusé avecphone_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 un00en 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é en2290197000000. - Numéro national refusé : un numéro qui commence par
0(même précédé de+ou00) est refusé avecphone_international_required. Sans+ni00, 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+ou00, les premiers chiffres sont lus comme l'indicatif : un indicatif inconnu ou une partie nationale de mauvaise longueur donnentinvalid_phone_number(+66000000est 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é parmtn-benin). countryfacultatif : code ISO 3166-1 du pays du numéro (BJ, oubj: 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.phonevaut2290197000000avec 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) | Quand | Exemple de message |
|---|---|---|
phone_international_required | Numé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_number | Indicatif 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_mismatch | country 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.
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é : 409request_in_progressetRetry-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
referenceunique 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_limitedavecRetry-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êteCashChap-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 :
{
"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) oupayout(retrait ou virement).- status
pending,succeeded,failed,expiredoucancelled(cycle de vie).- pending_reason
- En attente seulement :
awaiting_provider(validation du client ou réponse de l'opérateur) ouawaiting_bank_transfer(virement à traiter). - mode
liveousandbox.- 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:merchantoucustomer) 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), ounull. Depuis 2.3.0, quand le paiement porte des frais de change :fee(montant, devise pivot),fee_currencyetfee_payer(customer: compris dansamount_charged;merchant: retenus surnet_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é) etmargin_payer(customeroumerchant) ; 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, ounull. - failure
- Échec ou expiration :
code(par exempleinsufficient_funds) etmessageen français.nullsinon. - metadata description
- Vos données, renvoyées telles quelles.
- available
- Encaissement : fonds disponibles sur votre solde (
true) ou encore en cours (mise à disposition).nullpour 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) ounull(paiement non réussi) ;simulated:trueen 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 :
"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_chargedau 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
feesqui 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 ;0sans 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 dansfees, jamais dansexchange_fee. - fees_under_minimum
truequand 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),historyouapprox(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.
{
"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…) ouautopour le choix automatique d'après le numéro. - payment_method
mobile_money(par défaut),cardoudirect_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 : refusphone_country_mismatchs'il contredit l'indicatif. - customer obligatoire en clé secrète, publique ou lien
name(255 caractères au plus) etemaildu 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éfixecashchap_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 -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 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 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.
curl "https://api.cashchap.com/v2/transactions/2f1c3b6e-9a0d-4c1e-8f5a-2b7d9e4c6a10?wait=5" \
-H "Authorization: Bearer $CASHCHAP_CLE_SECRETE"
{
"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(fulloumasked),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 toujoursphone_masked(« 22901••••0000 ») ; channel:code(payment_link,shop_qr,checkout_qr,shop,pos,widget,api),labeletdisplayen 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_localetcompleted_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 :
| Appelant | Reç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 faux | Masqué : 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
availabledès cette confirmation. Nombre :pending_confirmation_count; texte à montrer :pending_confirmation_message. - simulated
trueen Sandbox : rien n'est retenu,pending_confirmationmontre ce qui le serait en Live et ce montant est déjà compté dansavailable.- 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).countryfacultatif, comme pour un encaissement. - reference description
- Votre référence et un libellé.
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 ;
XOFavant). 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_indansdetails.fields. - description
- Libellé facultatif, 255 caractères au plus.
urlest 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 estopen, 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
| Route | Clé | Usage |
|---|---|---|
GET /v2/session | toutes | Qui appelle : type de clé, mode, canal, service, terminal, collaborateur. |
POST /v2/quotes | toutes (retrait : secrète) | Devis : total, frais, crédit, change. |
GET /v2/currencies | toutes | Devises actives. |
GET /v2/exchange-rates?from=&to= | toutes | Taux de change. |
GET /v2/payment-links/{id}POST /v2/payment-links/{id}/collections | aucune | Liens de paiement (CashChap Link) : détail public et paiement d'un lien. |
/v2/checkouts… | secrète, terminal | QR codes de caisse : création, état, annulation. |
/v2/terminals… | secrète, terminal | Terminaux de paiement et sessions de caisse. |
POST /v2/refunds, GET /v2/refunds/{id} | secrète | Pas encore disponible : 501 not_implemented. Les remboursements se demandent au portail. |
/v2/webhook_endpoints…, /v2/webhook_event_types | secrète | Webhooks du service dans le mode de la clé (ajout, liste, suppression ; types d'événements) : Webhooks. |
GET /v2/events, /v2/events/{id} | secrète | Pas 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.