# Cartflox : documentation complete / full documentation # Authentification Source: https://cartflox.com/docs/authentification/ > Clé publique, clé secrète, en-têtes acceptés et rotation des clés API Cartflox. Chaque appel serveur est authentifié par votre clé secrète. ## Types de clés | Clé | Rôle | |---|---| | `af_live_pub_...` | Clé publique. Identifie votre application, peut apparaître côté client (widget, SoftPay). Elle ne permet que de créer des sessions et d'en lire les moyens de paiement et le statut. | | `af_live_sec_...` | Clé secrète. Signe vos appels API et vos webhooks. Serveur uniquement. | | `af_test_pub_...` / `af_test_sec_...` | Les mêmes, en mode test : aucun agrégateur appelé, paiements simulés, données isolées. Voir [Mode test](https://cartflox.com/docs/demarrage/mode-test/). | ## Utilisation Passez la clé secrète dans l'en-tête `Authorization: Bearer `. L'en-tête `x-api-key: ` est accepté en alternative. Toute réponse d'objet (session, transfert, livraison de webhook) et tout webhook portent `livemode` : `true` avec une clé de production, `false` avec une clé de test. ```bash title="curl" curl https://cartflox.com/api/v1/config/webhook \ -H "Authorization: Bearer af_live_sec_VOTRE_CLE" ``` ## Rotation Depuis [API et Logs](https://cartflox.com/developer), vous pouvez régénérer vos clés. L'ancienne clé secrète cesse immédiatement de fonctionner : mettez à jour vos serveurs avant de confirmer. > **Clé exposée ?** Régénérez-la sans attendre. Une clé secrète divulguée permet de créer des sessions de paiement en votre nom et de contrefaire des webhooks. --- # Démarrage rapide Source: https://cartflox.com/docs/demarrage/demarrage-rapide/ > Cinq minutes pour encaisser un premier paiement avec Cartflox. Cinq minutes suffisent pour encaisser un premier paiement. 1. **Créez votre compte et branchez votre agrégateur.** [Créez un compte](https://cartflox.com/auth/register), puis dans [Passerelles](https://cartflox.com/gateways), ajoutez les clés API de votre agrégateur. Cartflox vérifie les clés auprès du fournisseur avant de les enregistrer. Dans [Moyens de paiement](https://cartflox.com/methods), activez les opérateurs que vos clients pourront utiliser. 2. **Récupérez vos clés API.** Dans [API et Logs](https://cartflox.com/developer), vous trouvez votre clé publique `af_live_pub_...` et votre clé secrète `af_live_sec_...`. La clé secrète authentifie vos appels serveur : ne la mettez jamais dans une page web ou une application mobile. 3. **Créez une session de paiement.** ```bash title="curl" curl -X POST https://cartflox.com/api/v1/checkout/sessions \ -H "Authorization: Bearer af_live_sec_VOTRE_CLE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: commande-1042" \ -d '{ "amount": 5000, "currency": "XOF", "customer_name": "Awa Koné", "customer_email": "awa@example.com", "customer_phone": "+2250700000000", "description": "Commande #1042", "success_url": "https://maboutique.com/merci", "cancel_url": "https://maboutique.com/panier", "metadata": { "order_id": "1042" } }' ``` La réponse contient l'adresse de la page de paiement : redirigez-y votre client. ```json title="Réponse 201" { "id": "cmf3k2p1x0001abcd9e8f7g6h", "object": "checkout.session", "url": "https://checkout.cartflox.com/cmf3k2p1x0001abcd9e8f7g6h", "order_id": "CS-MF3K2A-9X1QZ", "amount": 5000, "currency": "XOF", "status": "pending", "created": "2026-09-02T10:15:00.000Z" } ``` 4. **Recevez la confirmation.** Renseignez l'URL de votre serveur dans [API et Logs](https://cartflox.com/developer), ou par l'API (voir [Webhooks](https://cartflox.com/docs/webhooks/configuration/)). Dès que le paiement aboutit, Cartflox envoie l'événement `payment.completed`, signé. Vous pouvez aussi interroger [le statut de la session](https://cartflox.com/docs/paiements/verifier-le-statut/) à tout moment. 5. **Passez en production.** Remplacez les clés sandbox de votre passerelle par les clés de production. Complétez votre [vérification d'identité](https://cartflox.com/settings?tab=kyc) : elle est obligatoire pour une plateforme de paiement. > Pas de serveur ? Le [widget](https://cartflox.com/docs/integrations/widget/), [SoftPay](https://cartflox.com/docs/integrations/softpay/) et les [liens de paiement](https://cartflox.com/docs/integrations/sans-code/) fonctionnent avec votre seule clé publique. --- # Introduction Source: https://cartflox.com/docs/demarrage/introduction/ > Ce que fait l'API Cartflox, la base URL, le format des réponses et les environnements. Cartflox est un orchestrateur de paiement panafricain : une seule intégration pour encaisser par Mobile Money (Orange Money, Wave, MTN MoMo, Moov...) et carte bancaire dans plus de 15 pays, en passant par vos propres comptes agrégateurs : 22 passerelles prises en charge (PayDunya, PawaPay, Hub2, Wave Business, Djamo, CinetPay, Stripe, Paystack, Flutterwave, FedaPay, Magma OnePay, PayTech, iPay Money...), la liste complète est sur la page [Passerelles prises en charge](https://cartflox.com/docs/paiements/passerelles/). > **Votre argent ne transite jamais par Cartflox** Vous branchez vos propres clés d'agrégateurs dans le tableau de bord. Chaque paiement est encaissé directement sur vos comptes. Cartflox se charge du routage, de la page de paiement, du suivi et des webhooks. > **Cartflox est open source** Le moteur est publié sous licence AGPL-3.0 : [github.com/uvan00/cartflox](https://github.com/uvan00/cartflox). Vous pouvez lire le code, vérifier ce qui se passe avec vos clés, et l'héberger vous-même sur votre propre serveur. L'espace géré, où Cartflox encaisse pour votre compte, ne fait pas partie de la version ouverte. ## Ce que fait l'API Créez une session en un appel, redirigez votre client vers la page de paiement hébergée, récupérez le résultat par webhook ou en interrogeant le statut. Générez un lien à partager par WhatsApp, e-mail ou réseaux sociaux, sans écrire une ligne de code côté client. Chaque opérateur et chaque pays est dirigé vers la passerelle de votre choix, avec bascule automatique si elle échoue. Chaque changement de statut est poussé vers votre serveur avec une signature HMAC-SHA256 que vous vérifiez. ## Base URL Toutes les requêtes se font en HTTPS vers l'adresse ci-dessous. Les endpoints versionnés sont préfixés par `/v1`. ```text title="Base URL" https://cartflox.com/api ``` ## Format des réponses Les réponses sont en JSON. Une erreur renvoie un code HTTP 4xx ou 5xx et un objet `{ "error": "..." }` (ou `{ "success": false, "message": "..." }` sur les endpoints de la page de paiement). Les montants sont des entiers dans la plus petite unité courante de la devise : `5000` vaut 5 000 XOF. ## Environnements Il n'y a pas d'hôte « sandbox » séparé : ce sont vos clés qui font le monde. Chaque espace a deux jeux de clés, `af_test_...` pour construire et vérifier votre intégration sans appeler d'agrégateur (l'issue du paiement se choisit sur la page), `af_live_...` pour encaisser. Les données de test restent à l'écart de la production. Tout est décrit dans [Mode test](https://cartflox.com/docs/demarrage/mode-test/). Une passerelle peut aussi, de son côté, être branchée avec les clés sandbox de votre agrégateur : le tableau de bord le signale (bandeau orange). > **Numéros de test** En mode test Cartflox, n'importe quel numéro valide convient : l'issue se choisit sur la page. Avec les clés sandbox d'un agrégateur, utilisez ses numéros et cartes de test (PayDunya, CinetPay, Stripe...). --- # Mode test Source: https://cartflox.com/docs/demarrage/mode-test/ > Clés de test af_test_..., paiements simulés depuis la page de paiement, webhooks avec livemode false, données isolées et effacées après 90 jours. Chaque espace possède deux jeux de clés : un de production et un de test. Même API, mêmes routes, même page de paiement. Avec une clé de test, aucun agrégateur n'est appelé et aucun argent ne bouge : vous construisez et vérifiez votre intégration avant même que votre identité soit vérifiée. ## Les deux jeux de clés | Production | Test | Rôle | |---|---|---| | `af_live_pub_...` | `af_test_pub_...` | Clé publique (widget, SoftPay) : créer une session et lire son statut | | `af_live_sec_...` | `af_test_sec_...` | Clé secrète (serveur) : tout le reste, et la signature des webhooks | Les clés de test se trouvent dans [API et Logs](https://cartflox.com/developer), sous les clés de production, et se régénèrent séparément. Tant que votre identité n'est pas vérifiée, votre espace reste en sandbox et seules les clés de test existent : une clé de production répondrait `403` avec le code `identity_unverified`. Après la vérification, les clés de production apparaissent et l'interrupteur Sandbox se déverrouille. ## Ce qui se passe avec une clé de test 1. Vous créez une session comme d'habitude. La réponse porte `"livemode": false`. 2. La page de paiement affiche un bandeau « Mode test » et, après le choix du moyen et du numéro, propose deux boutons : **simuler un paiement réussi** ou **simuler un échec**. Si l'espace n'a encore aucune passerelle, un moyen fictif « Mobile Money (test) » est proposé pour dérouler le parcours. 3. La transaction passe en `SUCCESS` ou `FAILED` avec `provider: "sandbox"`, et votre webhook reçoit `payment.completed` ou `payment.failed` exactement comme en production, avec `"livemode": false`. ```bash title="Créer une session de test" curl -X POST https://cartflox.com/api/v1/checkout/sessions \ -H "Authorization: Bearer af_test_sec_VOTRE_CLE" \ -H "Content-Type: application/json" \ -d '{ "amount": 5000, "currency": "XOF", "customer_email": "test@example.com" }' ``` ```json title="201 Created" { "id": "cmu8a1b2c0001xyz", "object": "checkout.session", "url": "https://checkout.cartflox.com/cmu8a1b2c0001xyz", "order_id": "CS-MU8A1B-2C0X1", "amount": 5000, "currency": "XOF", "status": "pending", "livemode": false, "created": "2026-09-19T08:00:00.000Z" } ``` ### Simuler l'issue sans ouvrir la page Pour vos tests automatisés, l'issue se déclenche aussi par l'API, sans clé : l'identifiant de session, impossible à deviner, suffit. **POST /api/checkout/sandbox** : Dénouer un paiement de test ```bash title="curl" curl -X POST https://cartflox.com/api/checkout/sandbox \ -H "Content-Type: application/json" \ -d '{ "transactionId": "cmu8a1b2c0001xyz", "issue": "succes" }' ``` `issue` vaut `succes` ou `echec`. La route refuse une transaction de production (HTTP 403) et une transaction déjà dénouée (HTTP 409). ## Webhooks de test Les événements de test vont à votre adresse de production, avec `livemode: false`, sauf si vous enregistrez une adresse de test distincte : ```bash title="Adresse de test" curl -X PATCH https://cartflox.com/api/v1/config/webhook \ -H "Authorization: Bearer af_test_sec_VOTRE_CLE" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://staging.maboutique.com/webhooks/cartflox", "mode": "test" }' ``` La signature d'un événement de test est calculée avec votre **clé secrète de test**. Vérifiez donc la signature avec la clé du même monde que l'événement (`livemode`). ## Des données isolées - Les paiements de test n'apparaissent jamais dans vos listes, statistiques, exports ni notifications de production. Une clé de test ne lit que des sessions de test, et inversement. - Pour les consulter dans le tableau de bord : interrupteur « Sandbox » en haut de page. Il ne bloque jamais vos paiements réels : ce sont les clés qui font le monde d'un paiement. Un espace dont l'identité n'est pas encore vérifiée reste en sandbox, interrupteur verrouillé. - Les transactions de test sont effacées après 90 jours. ## Limites du mode test | Ce qui n'est pas disponible | Pourquoi | |---|---| | Transferts d'argent (`POST /v1/transfers`) | Ils font sortir de l'argent : HTTP 403, code `test_mode`. | | Liens de paiement (`POST /v1/payment-links`) | Un lien vit en production. Créez une session de test à la place. | | Carte bancaire réelle | Le formulaire de carte n'est pas monté en mode test : simulez l'issue. | > **Et le mode test d'une passerelle ?** Il existe toujours. Une passerelle branchée avec les clés sandbox de votre agrégateur reste en test de son côté, indépendamment de vos clés Cartflox. Le mode test Cartflox est plus simple : rien à configurer chez l'agrégateur, l'issue se choisit sur la page. --- # Documentation Cartflox Source: https://cartflox.com/docs/ > Encaissez par Mobile Money et carte bancaire partout en Afrique, avec vos propres passerelles. Une API, un widget, SoftPay, WooCommerce. ## Choisissez votre façon d'encaisser Un lien de paiement à partager par WhatsApp, e-mail ou sur votre site. Aucune ligne de code. [Créer un lien](https://cartflox.com/docs/integrations/sans-code/) Deux lignes sur votre page : un bouton ouvre la page de paiement Cartflox par-dessus votre site. [Installer le widget](https://cartflox.com/docs/integrations/widget/) Le paiement dans votre page, à vos couleurs ou entièrement dessiné par vous. Opérateur, numéro, OTP, suivi : tout reste chez vous. [Découvrir SoftPay](https://cartflox.com/docs/integrations/softpay/) Le plugin officiel : installez, collez votre clé, vos clients paient en Mobile Money. [Installer le plugin](https://cartflox.com/docs/integrations/woocommerce/) Créez des sessions depuis votre serveur, recevez les confirmations par webhook signé. [Voir la référence](https://cartflox.com/docs/paiements/creer-une-session/) Chaque opérateur et chaque pays vers la passerelle de votre choix, avec bascule automatique en cas d'échec. [Configurer le routage](https://cartflox.com/docs/routage/) ## L'essentiel en trois points - [Votre argent ne transite jamais par Cartflox](https://cartflox.com/docs/demarrage/introduction/): Chaque paiement est encaissé directement sur vos comptes agrégateurs. Cartflox ne prend aucune commission. - [Un webhook signé pour chaque paiement](https://cartflox.com/docs/webhooks/signature/): HMAC-SHA256 avec votre clé secrète, dix tentatives sur 72 heures, horodatage anti-rejeu. La seule preuve fiable d'un paiement. - [Des clés de test dès le premier jour](https://cartflox.com/docs/demarrage/mode-test/): af_test_... : même API, paiements simulés depuis la page, webhooks avec livemode false, données isolées. --- # Sans code : lien et bouton de paiement Source: https://cartflox.com/docs/integrations/sans-code/ > Encaisser sans développeur avec un lien de paiement Cartflox, sur Wix, Webflow, Shopify, WordPress, Linktree, WhatsApp ou e-mail. Pas de développeur ? Un lien de paiement suffit. Il fonctionne partout : Wix, Webflow, Shopify, WordPress, Linktree, WhatsApp, e-mail. 1. Dans le tableau de bord, ouvrez [Liens de paiement](https://cartflox.com/payment-links/new) et créez un lien avec un titre et un montant. 2. Copiez l'adresse obtenue, de la forme `https://cartflox.com/pay/xxxxxxxx`. 3. Collez-la dans un bouton de votre site, ou envoyez-la directement à votre client. ## Bouton HTML prêt à coller ```html title="HTML" Payer 25 000 F ``` Le téléphone du client est demandé sur la page de paiement. Vous êtes prévenu de chaque paiement dans le tableau de bord, par e-mail et, si vous l'avez configuré, par [webhook](https://cartflox.com/docs/webhooks/configuration/) : la référence du lien figure dans `data.metadata.paymentLinkId`. --- # SoftPay : le paiement natif, avec votre design Source: https://cartflox.com/docs/integrations/softpay/ > Le formulaire de paiement dans votre page, stylable par variables CSS, ou entièrement dessiné par vous en mode headless. SoftPay affiche le choix de l'opérateur, le numéro, le code de confirmation, le lien d'application (Wave, Djamo) et le suivi du paiement directement dans votre page, sans passer par la page Cartflox. Vous gardez la main sur tout le style, ou vous dessinez chaque écran vous-même. Seule votre clé publique est utilisée côté navigateur. Le pied du formulaire porte toujours la mention « Propulsé par Cartflox » avec un lien vers cartflox.com : elle fait partie des conditions d'utilisation de SoftPay et ne se retire pas. Vous pouvez en changer la couleur, pas la masquer. Essayez tout de suite : [démo SoftPay](https://cartflox.com/softpay-demo) avec votre clé publique et vos couleurs. ## Option A : formulaire prêt, à vos couleurs ```html title="HTML"
``` ### Options de `mount` | Option | Type | Requis | Description | |---|---|---|---| | `container` | string ou élément | Oui | Sélecteur CSS ou élément qui reçoit le formulaire | | `amount` | number | Oui | Montant, entier pour le franc CFA | | `currency` | string | Non | `XOF` par défaut | | `country` | string | Non | Pays des moyens proposés (`ci`, `sn`, `bj`, `ml`, `bf`, `tg`, `cm`, `gn`...). Sans valeur : tous les opérateurs actifs | | `description`, `customer_name`, `customer_email`, `customer_phone`, `metadata`, `success_url`, `cancel_url` | | Non | Mêmes champs que [`POST /v1/checkout/sessions`](https://cartflox.com/docs/paiements/creer-une-session/) | | `merchant_name`, `merchant_logo` | string | Non | Nom et logo (adresse `https`) affichés sur la page Cartflox équivalente et les reçus, à la place de ceux de votre espace | | `title` | string | Non | Titre du formulaire (« Paiement » par défaut) | | `theme` | object | Non | Variables CSS sans le préfixe : `{ accent, "accent-text", bg, text, muted, border, field, radius, font }` | | `labels` | object | Non | Libellés à remplacer (voir Textes) | | `autoRedirect` | boolean | Non | `false` pour ne pas envoyer vous-même le client sur une page hébergée : vous recevez l'URL dans `onRedirect` | | `timeoutMs` | number | Non | Durée maximale de suivi d'un paiement en attente (180 000 ms par défaut) | | `onSession`, `onStatus`, `onRedirect`, `onSuccess`, `onError` | function | Non | Voir Événements | `configure` accepte `publicKey` (obligatoire), `country` (pays par défaut), `css: false` (aucun style injecté) et `baseUrl` (uniquement si vous servez le script depuis un autre domaine). ### Personnaliser le style Le formulaire est construit avec des variables CSS et des classes stables. Surchargez-les dans votre propre feuille de style, ou passez `{ css: false }` à `configure` pour n'injecter aucun style et tout écrire vous-même. | Variable | Rôle | |---|---| | `--cf-accent` | Couleur du bouton Payer et de la sélection | | `--cf-accent-text` | Couleur du texte du bouton | | `--cf-bg` | Fond du formulaire | | `--cf-text` | Texte principal | | `--cf-muted` | Texte secondaire | | `--cf-border` | Bordures | | `--cf-field` | Fond des champs | | `--cf-radius` | Arrondi général | | `--cf-font` | Police (`inherit` par défaut : celle de votre page) | ```css title="CSS, exemple : votre charte" .cf-sp { --cf-accent: #c01826; --cf-radius: 0; --cf-font: "Questrial", sans-serif; } .cf-sp-bouton { text-transform: uppercase; letter-spacing: .06em; } .cf-sp-methode.cf-sp-actif { box-shadow: none; background: #fff3f3; } ``` Classes disponibles : `.cf-sp`, `.cf-sp-entete`, `.cf-sp-titre`, `.cf-sp-montant`, `.cf-sp-methodes`, `.cf-sp-methode`, `.cf-sp-actif`, `.cf-sp-telephone`, `.cf-sp-indicatif`, `.cf-sp-input`, `.cf-sp-otp`, `.cf-sp-ussd`, `.cf-sp-bouton`, `.cf-sp-lien`, `.cf-sp-erreur`, `.cf-sp-attente`, `.cf-sp-spinner`, `.cf-sp-qr`, `.cf-sp-pied`, `.cf-sp-securise`, `.cf-sp-propulse` (la mention Cartflox : couleur libre, affichage imposé). Chaque écran porte aussi une classe `.cf-sp-vue-` (formulaire, attente, otp, application, redirection, succes, echec). ### Textes Tous les libellés se remplacent avec l'option `labels`, par exemple `labels: { payer: "Régler", telephone: "Votre numéro Mobile Money", succes: "Paiement reçu !" }`. ### Événements | Événement | Quand | |---|---| | `onSession(s)` | Session créée : `s.id`, `s.orderId`, `s.url` (la page Cartflox équivalente) | | `onStatus(r)` | À chaque réponse de paiement ou de suivi : `r.status` | | `onRedirect(url)` | Page hébergée (carte, certains agrégateurs) : l'URL vers laquelle le client part, `autoRedirect: false` pour gérer vous-même. Aussi appelé pour un lien d'application (Wave, Djamo) : le widget reste alors affiché avec le bouton « Ouvrir » et suit le paiement. | | `onSuccess(r)` | Paiement confirmé : `r.sessionId`, `r.orderId`, `r.amount`, `r.currency` | | `onError(e)` | Paiement refusé, délai dépassé ou erreur réseau : `e.message` | ## Option B : headless, vous dessinez tout Même moteur, aucune interface : vous appelez la session et affichez ce que vous voulez. En mode headless, c'est vous qui dessinez le pied de page : la mention « Propulsé par Cartflox » avec un lien vers `https://cartflox.com` reste due, au même titre que dans le formulaire prêt à l'emploi. ```js title="JavaScript" Cartflox.softpay.configure({ publicKey: "af_live_pub_VOTRE_CLE" }); const session = await Cartflox.softpay.createSession({ amount: 5000, currency: "XOF", metadata: { order_id: "1042" }, }); const methods = await session.methods("ci"); // [{ code, gatewayId, name, provider, country, type, logo, requiresPhone, dialCode }] const r = await session.pay({ method: methods[0], phone: "+2250700000000", name: "Awa Koné" }); switch (r.status) { case "SUCCESS": /* payé */ break; case "PENDING": /* demande poussée sur le téléphone : affichez r.instructions, puis session.waitForResult() */ break; case "REQUIRE_OTP": /* code de confirmation (Orange Money, Magma OnePay, Paystack...) : affichez r.ussdCode ou r.instructions, puis session.pay({ ..., otp }) */ break; case "APP_LINK": /* Wave, Djamo : bouton vers r.redirectUrl (cible _blank), r.qr en image sur ordinateur, puis session.waitForResult() */ break; case "REDIRECT": window.location.href = r.redirectUrl; break; // page hébergée du fournisseur case "INLINE_CARD": window.location.href = session.url; break; // carte : page sécurisée Cartflox default: /* r.message */ } const fin = await session.waitForResult({ timeoutMs: 180000, onTick: (d) => console.log(d.status) }); // fin.status : SUCCESS | FAILED | CANCELLED ``` | Méthode | Rôle | |---|---| | `createSession(options)` | Mêmes champs que `POST /v1/checkout/sessions`. Renvoie une session. | | `session.methods(pays?)` | Moyens de paiement disponibles, filtrés par pays (code ISO). | | `session.pay({ method, phone, name, email, country, otp })` | Lance le paiement. Renvoie `{ status, message, redirectUrl, application, qr, instructions, ussdCode, providerReference }`. `application` et `qr` (image en data URL) ne sont remplis que pour `APP_LINK` ; `instructions` porte la consigne du fournisseur quand il en donne une. | | `session.getStatus()` | Statut courant : `{ status, paid, order_id, provider }`. | | `session.waitForResult({ intervalMs, timeoutMs, onTick })` | Interroge le statut jusqu'à un état final. | > **Le navigateur n'est pas une preuve de paiement** `onSuccess` et `waitForResult` s'exécutent chez le client. Validez la commande à réception du webhook `payment.completed`, ou en interrogeant `GET /v1/checkout/sessions/{id}/status` depuis votre serveur avec votre clé secrète. Contrôlez aussi le montant reçu. --- # Widget JavaScript Source: https://cartflox.com/docs/integrations/widget/ > Deux lignes de code pour ouvrir la page de paiement Cartflox par-dessus votre site, avec votre clé publique. Deux lignes de code : le widget crée la session avec votre clé publique et ouvre la page de paiement par-dessus votre page (popup) ou en redirection. Aucun serveur nécessaire. ```html title="HTML" ``` ## Appel programmatique ```js title="JavaScript" Cartflox.checkout({ amount: 5000, currency: "XOF", customer_email: "awa@example.com", customer_phone: "+2250700000000", description: "Commande #1042", metadata: { order_id: "1042" }, success_url: "https://maboutique.com/merci", cancel_url: "https://maboutique.com/panier", onSuccess: function (data) { /* la fenêtre se ferme : affichez un message d'attente */ }, onClose: function () {}, onError: function (err) { console.error(err); } }); ``` ## Options | Option | Type | Requis | Description | |---|---|---|---| | `publicKey` | string | Oui | Votre clé publique `af_live_pub_...` (rubrique API et Logs). Elle ne permet que de créer des sessions. | | `mode` | string | Non | `popup` (fenêtre par-dessus votre page, défaut) ou `redirect` (le client quitte votre page) | | `currency` | string | Non | Devise par défaut des boutons | | `success_url`, `cancel_url` | string | Non | Adresses de retour, utilisées en mode `redirect` | | `metadata` | object | Non | Données renvoyées dans le webhook (numéro de commande, identifiant client...) | | `merchant_name` | string | Non | Nom affiché en tête de la page de paiement à la place du nom de votre espace | | `merchant_logo` | string | Non | Adresse `https` d'un logo affiché à la place de celui de votre espace | > **onSuccess n'est pas une preuve de paiement** Ce callback s'exécute dans le navigateur du client, qui peut le déclencher lui-même. Livrez uniquement à réception du webhook `payment.completed` ou après vérification du statut côté serveur, et contrôlez le montant reçu : avec une clé publique, c'est le navigateur qui l'envoie. --- # WordPress et WooCommerce Source: https://cartflox.com/docs/integrations/woocommerce/ > Le plugin officiel Cartflox pour WooCommerce, installation en cinq minutes, sans code. Le plugin officiel ajoute Cartflox comme moyen de paiement de votre boutique WooCommerce. Cinq minutes, sans code, et tout se règle depuis WooCommerce. - [Télécharger cartflox-payment.zip](https://cartflox.com/downloads/cartflox-payment.zip): Plugin officiel, version 4.0.0. WordPress 6.2+, WooCommerce 7.6+, PHP 7.4+. ## Installation 1. WordPress : Extensions, Ajouter, Téléverser une extension. Choisissez le fichier `.zip`, Installer maintenant, puis Activer. 2. WooCommerce : Réglages, Paiements, Cartflox, Gérer. 3. Collez votre clé secrète `af_live_sec_...` (tableau de bord Cartflox, rubrique API et journaux). Elle fonctionne pour un espace Cartflox comme pour un espace Cartflox Connect. 4. Enregistrez. Le plugin vérifie la clé et déclare lui-même l'adresse de webhook de votre boutique dans Cartflox. Le panneau « État de la connexion » vous dit où vous en êtes, et le bouton « Tester la connexion » le confirme à tout moment. ## Ce que fait le plugin - À la validation de la commande, crée une session Cartflox et envoie le client sur la page de paiement (Mobile Money, carte). Le client revient ensuite sur votre boutique. - Reçoit le webhook signé (HMAC-SHA256 vérifié avec votre clé secrète, horodatage contrôlé) et passe la commande en « payée », sans action du client. - Au retour du client, interroge le statut de la session si le webhook n'est pas encore arrivé ; la page de remerciement se met à jour toute seule pendant la confirmation. - Toutes les 5 minutes, vérifie les commandes encore en attente (24 h) : filet de sécurité si le webhook ne peut pas arriver. - Contrôle que le montant et la devise reçus correspondent à la commande ; sinon elle passe « en attente » avec une note explicative. - Sur chaque commande, une boîte Cartflox : statut du paiement, opérateur, référence, bouton « Vérifier maintenant », lien vers la transaction dans Cartflox. - Compatible tunnel de commande classique et en blocs, stockage des commandes HPOS. ## Réglages | Section | Ce que vous réglez | |---|---| | Connexion à Cartflox | Activation, clé secrète, état de la connexion, test | | Affichage au paiement | Titre, description, logos affichés (Orange Money, Wave, MTN MoMo, Moov Money, carte, M-Pesa, Airtel Money, Free Money, T-Money) et leur taille, texte du bouton de commande, message sur la page de remerciement | | Parcours de paiement | Pré-remplissage du nom, de l'e-mail et du téléphone, libellé de la session (variables `{numero}`, `{boutique}`, `{client}`, `{total}`), adresse de retour après paiement, retour après abandon | | Commandes | Statut en attendant le paiement, statut une fois payée, vérification automatique, annulation automatique après un délai, référence Cartflox dans les e-mails | | Frais et limites | Frais fixes ou en pourcentage quand le client choisit Cartflox, montant minimum et maximum, devises acceptées | | Avancé | Enregistrement automatique du webhook, journal détaillé, adresse de l'API | > **HTTPS obligatoire pour les webhooks** Cartflox n'envoie de webhooks qu'à des adresses `https`. Sur une boutique en `http`, les commandes sont confirmées au retour du client et par la vérification automatique. ## Montants Cartflox attend des montants entiers : les devises à décimales (USD, EUR, GHS...) sont arrondies à l'unité. Pour le XOF et le XAF, rien ne change. Le numéro de téléphone du client est converti au format international à partir de son pays de facturation. ## Pour les développeurs - Filtre `cartflox_session_donnees( $donnees, $order )` : modifiez les données envoyées à Cartflox avant la création de la session. - Filtre `cartflox_montant_frais( $montant, $cart )` : calculez vous-même les frais de paiement. - Action `cartflox_paiement_confirme( $order, $data )` : déclenchée dès qu'un paiement est confirmé. - Journal : WooCommerce, Statut, Journaux, source `cartflox`. --- # Créer une session de paiement Source: https://cartflox.com/docs/paiements/creer-une-session/ > POST /v1/checkout/sessions, paramètres, en-têtes, exemples curl, Node.js, PHP et Python, réponse. Une session représente une intention de paiement. Vous la créez côté serveur, puis redirigez votre client vers la page de paiement hébergée par Cartflox, qui gère le choix de l'opérateur, la redirection Wave, les codes OTP Orange Money et la carte bancaire. **POST /v1/checkout/sessions** : Créer une nouvelle session de paiement ## Paramètres | Paramètre | Type | Requis | Description | |---|---|---|---| | `amount` | number | Oui | Montant, entier positif (5000 pour 5 000 XOF) | | `currency` | string | Non | Code ISO 4217 : XOF (défaut), XAF, GHS, NGN, KES, TZS, UGX, RWF, ZMW, MWK, CDF, ETB, MZN, ZAR, EGP, MAD, GNF, SLE, LRD, GMD, MGA, MUR, USD, EUR, GBP. Les francs CFA, GNF, UGX, RWF, CDF et MGA n'ont pas de centimes. | | `customer_name` | string | Non | Nom du client, pré-rempli sur la page de paiement | | `customer_email` | string | Non | E-mail du client (reçu de paiement) | | `customer_phone` | string | Non | Téléphone au format international (+225...), pré-rempli pour le Mobile Money | | `description` | string | Non | Libellé affiché au client | | `success_url` | string | Non | Adresse de retour après un paiement réussi | | `cancel_url` | string | Non | Adresse de retour si le client abandonne | | `metadata` | object | Non | Vos données libres (numéro de commande, identifiant client...), renvoyées dans les webhooks | | `merchant_name` | string | Non | Nom affiché en tête de la page de paiement à la place du nom de votre espace (60 caractères au plus). Utile aux plateformes qui encaissent pour plusieurs marques. | | `merchant_logo` | string | Non | Adresse `https` d'un logo affiché à la place de celui de votre espace (PNG, JPG ou SVG). | ### En-têtes | En-tête | Valeur | |---|---| | `Authorization` | `Bearer af_live_sec_...` (ou `x-api-key`) ; une clé `af_test_sec_...` crée une session de test | | `Content-Type` | `application/json` | | `Idempotency-Key` | Facultatif. Une chaîne unique par commande : si le même appel est rejoué, la session existante est renvoyée (HTTP 200 avec l'en-tête `Idempotent-Replayed: true`) au lieu d'en créer une deuxième. | ## Exemples #### curl ```bash curl -X POST https://cartflox.com/api/v1/checkout/sessions \ -H "Authorization: Bearer af_live_sec_VOTRE_CLE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: commande-1042" \ -d '{ "amount": 5000, "currency": "XOF", "customer_name": "Awa Koné", "customer_email": "awa@example.com", "customer_phone": "+2250700000000", "description": "Commande #1042", "success_url": "https://maboutique.com/merci", "cancel_url": "https://maboutique.com/panier", "metadata": { "order_id": "1042" }, "merchant_name": "Ma Boutique", "merchant_logo": "https://maboutique.com/logo.png" }' ``` #### Node.js ```js const res = await fetch("https://cartflox.com/api/v1/checkout/sessions", { method: "POST", headers: { Authorization: `Bearer ${process.env.CARTFLOX_SECRET_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "commande-1042", }, body: JSON.stringify({ amount: 5000, currency: "XOF", customer_email: "awa@example.com", customer_phone: "+2250700000000", success_url: "https://maboutique.com/merci", cancel_url: "https://maboutique.com/panier", metadata: { order_id: "1042" }, }), }); const session = await res.json(); // Redirigez votre client vers la page de paiement : res.ok ? redirect(session.url) : console.error(session.error); ``` #### PHP ```php $ch = curl_init("https://cartflox.com/api/v1/checkout/sessions"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("CARTFLOX_SECRET_KEY"), "Content-Type: application/json", "Idempotency-Key: commande-1042", ], CURLOPT_POSTFIELDS => json_encode([ "amount" => 5000, "currency" => "XOF", "customer_email" => "awa@example.com", "customer_phone" => "+2250700000000", "success_url" => "https://maboutique.com/merci", "cancel_url" => "https://maboutique.com/panier", "metadata" => ["order_id" => "1042"], ]), ]); $session = json_decode(curl_exec($ch), true); header("Location: " . $session["url"]); exit; ``` #### Python ```python r = requests.post( "https://cartflox.com/api/v1/checkout/sessions", headers={ "Authorization": f"Bearer {os.environ['CARTFLOX_SECRET_KEY']}", "Idempotency-Key": "commande-1042", }, json={ "amount": 5000, "currency": "XOF", "customer_email": "awa@example.com", "customer_phone": "+2250700000000", "success_url": "https://maboutique.com/merci", "cancel_url": "https://maboutique.com/panier", "metadata": {"order_id": "1042"}, }, timeout=15, ) session = r.json() print(session["url"]) # redirigez votre client vers cette adresse ``` ## Réponse ```json title="201 Created" { "id": "cmf3k2p1x0001abcd9e8f7g6h", "object": "checkout.session", "url": "https://checkout.cartflox.com/cmf3k2p1x0001abcd9e8f7g6h", "order_id": "CS-MF3K2A-9X1QZ", "amount": 5000, "currency": "XOF", "status": "pending", "livemode": true, "created": "2026-09-02T10:15:00.000Z" } ``` | Champ | Description | |---|---| | `id` | Identifiant de la session, à conserver pour suivre le paiement | | `url` | Page de paiement : redirigez votre client ici | | `order_id` | Référence Cartflox lisible (`CS-...`) | | `status` | `pending` à la création, voir [les statuts](https://cartflox.com/docs/paiements/verifier-le-statut/#statuts-possibles) | | `livemode` | `true` avec une clé de production, `false` avec une clé de test ([mode test](https://cartflox.com/docs/demarrage/mode-test/)) | > **Après le paiement** Votre client est renvoyé vers `success_url` (ou `cancel_url`). Ne validez jamais une commande sur la seule foi de cette redirection : attendez le webhook `payment.completed` ou vérifiez le statut côté serveur. --- # Liens de paiement Source: https://cartflox.com/docs/paiements/liens-de-paiement/ > Créer un lien de paiement par l'API et savoir s'il a été payé. Un lien de paiement est une page hébergée à montant fixe, à partager par WhatsApp, e-mail ou réseaux sociaux. Vous pouvez les créer depuis [le tableau de bord](https://cartflox.com/payment-links) ou par l'API. **POST /v1/payment-links** : Créer un lien de paiement | Paramètre | Type | Requis | Description | |---|---|---|---| | `title` | string | Oui | Titre affiché au client | | `amount` | number | Oui | Montant fixe | | `currency` | string | Non | XOF par défaut | | `description` | string | Non | Détail du produit ou service | ```bash title="curl" curl -X POST https://cartflox.com/api/v1/payment-links \ -H "Authorization: Bearer af_live_sec_VOTRE_CLE" \ -H "Content-Type: application/json" \ -d '{ "title": "Formation Odoo", "amount": 25000, "currency": "XOF" }' ``` ```json title="200 OK" { "success": true, "id": "cmf3lz...", "url": "https://cartflox.com/pay/k3j9x2ab", "slug": "k3j9x2ab", "amount": 25000, "currency": "XOF" } ``` **GET /v1/payment-links/{id}/check** : Savoir si le lien a été payé ```json title="200 OK" { "success": true, "paid": true, "status": "SUCCESS", "transaction": { "id": "cmf3m1...", "amount": 25000, "customerName": "Awa Koné", "customerEmail": "awa@example.com" } } ``` Tant qu'aucun paiement n'a abouti, la réponse est `{ "success": true, "paid": false, "status": "WAITING" }` (ou le statut de la dernière tentative). Le téléphone du client est toujours demandé sur la page, pour le Mobile Money. --- # Moyens de paiement d'une session Source: https://cartflox.com/docs/paiements/moyens-de-paiement/ > GET /v1/checkout/sessions/{id}/methods, la liste des opérateurs disponibles pour une session, filtrée par pays. Si vous construisez votre propre écran de choix d'opérateur, cet endpoint liste ce qui est disponible pour une session, filtré par pays. Il accepte la clé publique comme la clé secrète. **GET /v1/checkout/sessions/{id}/methods?country=ci** : Opérateurs disponibles pour la session | Paramètre | Type | Requis | Description | |---|---|---|---| | `country` | string | Non | Code pays ISO alpha-2 (ci, sn, bj, ml, bf, tg, cm, gh, ng, ke...) ou nom du pays. Sans filtre : tous les opérateurs actifs. | ```bash title="curl" curl "https://cartflox.com/api/v1/checkout/sessions/cmf3k2p1x0001abcd9e8f7g6h/methods?country=ci" \ -H "Authorization: Bearer af_live_sec_VOTRE_CLE" ``` ```json title="200 OK" { "id": "cmf3k2p1x0001abcd9e8f7g6h", "currency": "XOF", "methods": [ { "code": "ORANGE_CIV", "gatewayId": "cm9x...", "name": "Orange Money", "provider": "PayDunya", "country": "Côte d'Ivoire", "type": "MOBILE_MONEY", "flag": "🇨🇮", "logo": "/logos/orange-money.svg" } ] } ``` Les cartes bancaires (UEMOA, international) apparaissent quel que soit le pays demandé. Les codes d'opérateur dépendent de la passerelle qui les sert : ne les codez pas en dur, lisez-les dans la réponse. --- # Passerelles prises en charge Source: https://cartflox.com/docs/paiements/passerelles/ > Les 22 agrégateurs que Cartflox sait piloter avec vos propres clés, pays par pays, et ce que voit votre client au moment de payer. Cartflox n'encaisse pas à votre place : vous connectez vos propres comptes agrégateurs dans [Passerelles](https://app.cartflox.com/gateways), et chaque paiement arrive directement chez eux. Cette page liste ce qui est pris en charge et comment votre client paie selon la passerelle. ## Connecter une passerelle 1. Dans [Passerelles](https://app.cartflox.com/gateways), choisissez la carte de l'agrégateur, puis **Connecter**. 2. Collez les clés de votre compte agrégateur. Le bouton **Vérifier** interroge l'agrégateur immédiatement : une clé refusée n'est jamais enregistrée. 3. Activez ensuite ses moyens de paiement dans [Moyens de paiement](https://app.cartflox.com/methods). Certains agrégateurs ouvrent **un compte marchand par pays** : Hub2 et Djamo Business se connectent donc pays par pays, avec les identifiants de ce pays sur chaque carte (« Hub2 Côte d'Ivoire », « Hub2 Sénégal »...). > **Test et production** Le mode test se règle passerelle par passerelle, avec les clés sandbox de l'agrégateur. Wave Business ne fournit que des clés de production : pas de mode test pour lui. PayTech accepte `test` mais prélève tout de même un montant symbolique. ## Où se passe le paiement Sauf mention contraire, votre client **reste sur la page de paiement Cartflox** (ou dans votre page avec [SoftPay](https://cartflox.com/docs/integrations/softpay/)) : la demande est poussée sur son téléphone, il la confirme avec son code secret, et la page se met à jour toute seule. Trois autres cas existent : - **Code de confirmation** : certains opérateurs exigent un code (Orange Money, Magma OnePay, Paystack Ghana et Kenya). Il se saisit sur la page. - **Lien d'application** (Wave Business, Djamo) : ces fournisseurs n'ont pas de demande poussée, le client confirme dans son application. La page affiche un bouton « Ouvrir Wave » ou « Ouvrir Djamo » (sur mobile, il ouvre l'application) et un QR code sur ordinateur, puis surveille le paiement. - **Page hébergée** : quelques agrégateurs et la plupart des paiements par carte passent par la page du fournisseur, puis reviennent. | Passerelle | Pays | Où se passe le paiement | Transferts sortants | |---|---|---|---| | PayDunya | Sénégal, Côte d'Ivoire, Bénin, Togo, Mali, Burkina Faso | Dans la page (demande poussée sur le téléphone) | Oui | | PawaPay | Bénin, Burkina Faso, Cameroun, Côte d'Ivoire, RD Congo, Éthiopie, Gabon, Ghana, Kenya, Lesotho, Malawi, Mozambique, Nigeria, Congo, Rwanda, Sénégal, Sierra Leone, Tanzanie, Ouganda, Zambie | Dans la page (demande poussée sur le téléphone) | Oui | | Hub2 (une passerelle par pays) | Côte d'Ivoire, Sénégal, Bénin, Mali, Burkina Faso, Togo, Cameroun | Dans la page (demande poussée, code si besoin) ; Wave : lien d'application | Oui | | Wave Business | Côte d'Ivoire, Sénégal, Mali, Burkina Faso | Lien d'application : bouton « Ouvrir Wave » et QR code sur la page, confirmation dans l'application | Non | | Djamo Business (une passerelle par pays) | Côte d'Ivoire, Sénégal | Lien d'application : bouton « Ouvrir Djamo » et QR code sur la page, confirmation dans l'application | Non | | PayTech | Sénégal, Côte d'Ivoire, Mali, Bénin | Page PayTech pré-remplie avec le numéro saisi sur la page, soumise automatiquement | Non | | Magma OnePay | Côte d'Ivoire, Sénégal, Bénin, Togo, Mali, Burkina Faso, Cameroun | Mobile Money dans la page (code de confirmation si l'opérateur l'exige) ; Wave et carte : page hébergée | Non | | iPay Money | Niger, Bénin | Mobile Money dans la page (demande poussée sur le téléphone) ; carte : page hébergée | Non | | FeexPay | Bénin, Togo, Côte d'Ivoire, Sénégal, Burkina Faso, Congo | Mobile Money dans la page (demande poussée sur le téléphone) ; carte : page hébergée | Oui | | MonetBill | Cameroun, Sénégal, RD Congo, Congo, Ouganda | Mobile Money dans la page (demande poussée sur le téléphone) ; carte : page hébergée | Oui | | FedaPay | Bénin, Togo, Côte d'Ivoire, Sénégal, Guinée, Mali, Burkina Faso, Niger | Dans la page (demande poussée sur le téléphone) | Oui | | NotchPay | Cameroun, Côte d'Ivoire, Sénégal, Bénin, Togo, Burkina Faso, Mali, Guinée, Nigeria | Dans la page (demande poussée sur le téléphone) | Oui | | Qosic | Bénin, Togo, Côte d'Ivoire | Dans la page (demande poussée sur le téléphone) | Non | | Paystack | Nigeria, Ghana, Afrique du Sud, Kenya, Côte d'Ivoire, Égypte, Rwanda | Mobile Money Ghana et Kenya dans la page (code par SMS) ; carte : page hébergée | Oui | | Flutterwave | Nigeria, Ghana, Kenya, Ouganda, Tanzanie, Rwanda, Afrique du Sud, Zambie, Côte d'Ivoire, Sénégal, Cameroun, Burkina Faso, Malawi, Égypte | Page hébergée du fournisseur | Oui | | CinetPay | Côte d'Ivoire, Sénégal, Mali, Togo, Bénin, Burkina Faso, Cameroun, RD Congo, Guinée, Niger | Page hébergée du fournisseur | Non | | Kkiapay | Bénin, Togo, Côte d'Ivoire, Sénégal | Page hébergée du fournisseur | Non | | PayPlus | Bénin, Togo, Burkina Faso, Côte d'Ivoire | Page hébergée du fournisseur | Non | | LengoPay | Guinée, Maroc, Sénégal, Côte d'Ivoire | Page hébergée du fournisseur | Non | | Stripe | Cartes bancaires, monde entier | Carte saisie dans la page (formulaire intégré) | Non | | Coinbase Commerce | Crypto, monde entier | Page hébergée du fournisseur | Non | | Cryptomus | Crypto, monde entier | Page hébergée du fournisseur | Non | Les transferts sortants (envoyer de l'argent vers un compte Mobile Money) utilisent les mêmes passerelles : voir [Envoyer de l'argent](https://cartflox.com/docs/transferts/envoyer-de-l-argent/). ## Webhooks à déclarer chez l'agrégateur La plupart des agrégateurs reçoivent l'adresse de rappel avec chaque paiement. Trois d'entre eux exigent qu'elle soit déclarée dans leur portail : le tiroir de configuration de la passerelle vous l'affiche, de la forme `https://cartflox.com/api/webhooks/`. | Passerelle | Où la déclarer | Secret à reporter dans Cartflox | |---|---|---| | Wave Business | Portail Wave Business, Développeurs, Webhooks (événements `checkout.session.completed` et `checkout.session.payment_failed`) | Le secret de signature `wave_xx_WHS_…` remis à la création (facultatif : sans lui, chaque succès est reconfirmé auprès de Wave) | | Djamo Business | Sujet `charge/events` auprès de Djamo Business | Le secret de signature, si vous en définissez un | | iPay Money | Menu Développeurs, Webhooks | Le `Secret-Hash` que vous y définissez | Dans tous les cas, un paiement n'est jamais crédité sur la seule foi d'un webhook non signé : Cartflox reconfirme le succès auprès de l'agrégateur avant de vous prévenir. --- # Vérifier le statut d'une session Source: https://cartflox.com/docs/paiements/verifier-le-statut/ > GET /v1/checkout/sessions/{id}/status et la liste des statuts possibles. Interrogez l'état d'une session à tout moment, par exemple au retour du client sur `success_url` ou pour rattraper un webhook manqué. **GET /v1/checkout/sessions/{id}/status** : Statut courant de la session ```bash title="curl" curl https://cartflox.com/api/v1/checkout/sessions/cmf3k2p1x0001abcd9e8f7g6h/status \ -H "Authorization: Bearer af_live_sec_VOTRE_CLE" ``` ```json title="200 OK" { "id": "cmf3k2p1x0001abcd9e8f7g6h", "status": "SUCCESS", "paid": true, "amount": 5000, "currency": "XOF", "order_id": "CS-MF3K2A-9X1QZ", "provider": "PayDunya", "provider_reference": "pd_7f3a9c" } ``` ## Statuts possibles | Statut | Signification | |---|---| | `PENDING` | Session créée, paiement pas encore confirmé (client sur la page, OTP en attente, redirection en cours...) | | `SUCCESS` | Paiement confirmé par l'agrégateur. Le champ `paid` vaut `true`. | | `FAILED` | Paiement refusé ou échoué chez l'opérateur. Le client peut réessayer sur la même session. | | `CANCELLED` | Session annulée, plus payable. | | `REFUNDED` | Paiement remboursé depuis le tableau de bord. | > **Fiez-vous au serveur** Seuls ce statut et le webhook font foi. La redirection vers `success_url` peut être déclenchée avant la confirmation de l'opérateur. --- # Changelog Source: https://cartflox.com/docs/reference/changelog/ > Les évolutions notables de la plateforme et de l'API Cartflox. | Date | Changement | |---|---| | 19 septembre 2026 | Mode test unifié : clés `af_test_...`, paiement simulé depuis la page, `livemode` sur toutes les réponses et tous les webhooks, données de test isolées et effacées après 90 jours. Webhooks sortants journalisés et rejoués jusqu'à 10 fois sur 72 heures, renvoi à la main (tableau de bord et API), en-têtes `X-Afriflow-Event` et `X-Afriflow-Delivery`, choix des événements reçus. Nouvel événement `payment.cancelled` (abandon ou annulation). Spécification OpenAPI, collection Postman, SDK Node.js, PHP et Python. Documentation en anglais. Vingt pays de plus reconnus dans le champ téléphone de la page de paiement (Zambie, Malawi, Mozambique, Éthiopie, Maroc, Égypte...) et numéros béninois à 10 chiffres. Plugin WooCommerce 4.0. | | 18 septembre 2026 | Cinq passerelles ajoutées : Wave Business, PayTech, Djamo Business, Magma OnePay, iPay Money (22 au total) ; Hub2 et Djamo se connectent pays par pays. Le paiement reste sur la page : Magma OnePay, Paystack (Ghana, Kenya), MonetBill, FeexPay et iPay Money poussent la demande sur le téléphone sans redirection ; Wave et Djamo se confirment dans leur application depuis la page (bouton et QR code). `merchant_name` et `merchant_logo` par session. SoftPay 1.1 : lien d'application, consignes du fournisseur, mention « Propulsé par Cartflox », et correction du bouton Payer du formulaire prêt à l'emploi, qui restait inactif après le choix de l'opérateur. Transferts d'argent sortants par API et depuis le tableau de bord. E-mail au marchand quand un agrégateur refuse ses clés. Déploiements sans interruption de service. | | 5 septembre 2026 | Nouvelle documentation, recherche intégrée. Relances WhatsApp automatiques depuis le numéro Cartflox (abonnement). Administration sur admin.cartflox.com. Service e-mail unifié. | | 2 septembre 2026 | Plugin WooCommerce v3 (webhook signé, vérification au retour, contrôle du montant). Le widget cartflox.js fonctionne avec la clé publique. SoftPay : paiement natif stylable ou headless. Page de paiement à la charte de cartflox.com. Page statut calculée sur l'activité réelle. Réinitialisation du mot de passe par e-mail. Parcours de démarrage guidé. | | 1er septembre 2026 | Vérification d'identité (KYC) des marchands. Fin du mode géré : chaque marchand encaisse avec ses propres clés d'agrégateurs, 0 F de commission Cartflox. | | 1er juillet 2026 | Paiement par carte embarqué (Stripe) directement sur la page de paiement, sans redirection. | | Juin 2026 | Audit de sécurité : secrets de passerelles chiffrés, webhooks entrants revérifiés auprès des agrégateurs, limites de taux, protection contre les accès croisés entre marchands. | Une question, un besoin qui n'apparaît pas ici ? [Contactez-nous](https://cartflox.com/contact) ou consultez [l'état du service](https://cartflox.com/status). --- # Codes d'erreur Source: https://cartflox.com/docs/reference/erreurs/ > Les codes HTTP renvoyés par l'API Cartflox et leur signification. Les erreurs sont signalées par le code HTTP et un message lisible (`error`). Les transferts, le rejeu des webhooks et le mode test ajoutent un champ `code` stable (`test_mode`, `rate_limited`, `solde_insuffisant`...). | Code | Signification | |---|---| | `400` | Paramètre manquant ou invalide (`amount` absent, `webhookUrl` non publique, lien incomplet...) | | `401` | Clé API absente ou invalide : `Missing API key` ou `Invalid API Key` | | `402` | Solde insuffisant pour un transfert (`solde_insuffisant`) | | `403` | Refusé : passerelle d'une autre application, action impossible avec une clé de test (`test_mode` : transferts, liens de paiement), ou clé de production d'un espace dont l'identité n'est pas encore vérifiée (`identity_unverified`) | | `404` | Session, lien ou passerelle introuvable | | `409` | La session n'est plus payable (déjà annulée ou remboursée) | | `429` | Trop de requêtes : respectez l'en-tête `Retry-After` (secondes) | | `500` | Erreur interne : réessayez, puis contactez-nous si elle persiste | | `502` | L'agrégateur n'a pas pu initier le paiement : le message contient sa réponse | ```json title="Exemple" { "error": "amount is required and must be > 0" } ``` Les endpoints de la page de paiement (`/checkout/initiate`, `/checkout/verify`) répondent `{ "success": false, "message": "..." }`. Ils sont utilisés par la page hébergée et n'ont pas besoin d'être appelés directement. --- # Feuille de route Source: https://cartflox.com/docs/reference/feuille-de-route/ > Ce qui arrive sur Cartflox, et ce qui n'existe pas encore. Ce qui n'est pas encore publié n'est pas documenté comme s'il l'était. Voici ce qui arrive. | Fonctionnalité | État | |---|---| | SDK officiels Node.js, PHP et Python | Disponibles depuis le 19 septembre 2026, avec la spécification OpenAPI et une collection Postman : [OpenAPI et SDK](https://cartflox.com/docs/reference/sdks/). | | Remboursements par API | En préparation. Aujourd'hui, un remboursement se fait depuis le tableau de bord, sur les passerelles qui le permettent. | | Transferts sortants (payouts) par API | Disponible depuis le 18 septembre 2026 : [Envoyer de l'argent](https://cartflox.com/docs/transferts/envoyer-de-l-argent/). | | Environnement de test Cartflox | Disponible depuis le 19 septembre 2026 : clés `af_test_...`, paiements simulés, données isolées. Voir [Mode test](https://cartflox.com/docs/demarrage/mode-test/). | > **Méfiez-vous des faux paquets** Les SDK officiels se téléchargent depuis [cette documentation](https://cartflox.com/docs/reference/sdks/). Tant qu'ils ne sont pas publiés sur npm, Composer ou PyPI, n'installez rien qui prétende venir de Cartflox sur ces registres. --- # Limites de taux Source: https://cartflox.com/docs/reference/limites/ > Les limites de requêtes par minute de l'API Cartflox. Les limites s'appliquent par adresse IP et par minute, sauf mention contraire. Au-delà, la réponse est `429` (avec `Retry-After` sur la création de session, et un champ `code: "rate_limited"` sur les transferts et le rejeu). | Endpoint | Limite | |---|---| | `POST /v1/checkout/sessions` | 60 par minute | | `POST /v1/payment-links` | 60 par minute | | `POST /v1/route` | 120 par minute | | `POST /checkout/initiate` (page de paiement) | 30 par minute | | `POST /checkout/verify` (page de paiement) | 60 par minute | | `POST /checkout/sandbox` (mode test) | 30 par minute | | `POST /v1/transfers` | 30 par minute et 200 par jour, par espace | | `POST /v1/webhooks/deliveries/{id}/resend` | 30 par minute, par espace | Besoin de plus pour un pic d'activité (lancement, billetterie) ? [Écrivez-nous](https://cartflox.com/contact) avant l'événement. --- # OpenAPI et SDK Source: https://cartflox.com/docs/reference/sdks/ > Spécification OpenAPI 3.1, collection Postman et SDK officiels Node.js, PHP et Python de l'API Cartflox, avec la vérification des webhooks. Toute l'API est décrite dans une spécification OpenAPI 3.1, à partir de laquelle sont fournis une collection Postman et trois SDK officiels : Node.js, PHP et Python. Les SDK couvrent chaque endpoint, passent `livemode` tel quel, et embarquent la vérification de signature des webhooks. ## Spécification et Postman | Fichier | Adresse | |---|---| | OpenAPI 3.1 (JSON) | [cartflox.com/docs/openapi.json](https://cartflox.com/docs/openapi.json) | | OpenAPI 3.1 (YAML) | [cartflox.com/docs/openapi.yaml](https://cartflox.com/docs/openapi.yaml) | | Collection Postman (v2.1) | [cartflox.com/docs/cartflox.postman_collection.json](https://cartflox.com/docs/cartflox.postman_collection.json) | | Index pour les assistants (llms.txt) | [cartflox.com/llms.txt](https://cartflox.com/llms.txt), [llms-full.txt](https://cartflox.com/llms-full.txt) | Dans Postman : Import, puis collez l'adresse de la collection. Renseignez la variable `bearerToken` avec votre clé secrète (de test pour commencer). Pour un autre langage (Go, Java, C#, Ruby...), générez un client depuis la spécification avec `openapi-generator`. ## Installer un SDK > **Publication sur les registres** Les SDK ne sont pas encore publiés sur npm, Packagist ni PyPI : installez-les depuis les archives ci-dessous. N'installez aucun paquet `cartflox` venu d'ailleurs. #### Node.js Node.js 18 ou plus, aucune dépendance, TypeScript inclus, ESM et CommonJS. ```bash curl -O https://cartflox.com/docs/sdk/cartflox-node.tgz npm install ./cartflox-node.tgz ``` #### PHP PHP 8.0 ou plus, extensions cURL et JSON. L'archive s'installe comme un dépôt « artifact » de Composer : ```bash mkdir -p vendor-cartflox && curl -o vendor-cartflox/cartflox-php.zip https://cartflox.com/docs/sdk/cartflox-php.zip composer config repositories.cartflox artifact ./vendor-cartflox composer require cartflox/cartflox ``` #### Python Python 3.9 ou plus, bibliothèque standard seulement. ```bash pip install https://cartflox.com/docs/sdk/cartflox-python.zip ``` ## Créer une session #### Node.js ```ts const cartflox = new Cartflox({ apiKey: process.env.CARTFLOX_SECRET_KEY }); const session = await cartflox.checkout.sessions.create( { amount: 5000, currency: "XOF", customer_email: "awa@example.com", metadata: { order_id: "1042" } }, { idempotencyKey: "commande-1042" }, ); // Redirigez le client vers session.url, puis attendez le webhook payment.completed const statut = await cartflox.checkout.sessions.retrieveStatus(session.id); if (statut.paid) { /* livrer */ } ``` #### PHP ```php use Cartflox\Cartflox; $cartflox = new Cartflox(getenv('CARTFLOX_SECRET_KEY')); $session = $cartflox->checkout->sessions->create([ 'amount' => 5000, 'currency' => 'XOF', 'customer_email' => 'awa@example.com', 'metadata' => ['order_id' => '1042'], ], ['idempotencyKey' => 'commande-1042']); header('Location: ' . $session['url']); ``` #### Python ```python from cartflox import Cartflox cartflox = Cartflox(os.environ["CARTFLOX_SECRET_KEY"]) session = cartflox.checkout.sessions.create( {"amount": 5000, "currency": "XOF", "customer_email": "awa@example.com", "metadata": {"order_id": "1042"}}, idempotency_key="commande-1042", ) statut = cartflox.checkout.sessions.retrieve_status(session["id"]) ``` ## Vérifier un webhook Chaque SDK vérifie la signature (`X-Afriflow-Signature`, `X-Afriflow-Timestamp`, tolérance de 5 minutes) et rend l'événement décodé. Passez le corps **brut** de la requête, jamais un JSON déjà analysé. #### Node.js ```ts app.post("/webhooks/cartflox", express.raw({ type: "application/json" }), (req, res) => { try { const event = Webhooks.constructEvent(req.body, req.get("X-Afriflow-Signature"), req.get("X-Afriflow-Timestamp"), process.env.CARTFLOX_SECRET_KEY); if (event.event === "payment.completed" && event.livemode) { /* livrer event.data.order_id */ } res.sendStatus(200); } catch (e) { if (e instanceof WebhookSignatureError) return res.sendStatus(400); throw e; } }); ``` #### PHP ```php use Cartflox\Webhook; $event = Webhook::constructEvent( file_get_contents('php://input'), $_SERVER['HTTP_X_AFRIFLOW_SIGNATURE'] ?? '', $_SERVER['HTTP_X_AFRIFLOW_TIMESTAMP'] ?? '', getenv('CARTFLOX_SECRET_KEY') ); if ($event['event'] === 'payment.completed' && $event['livemode']) { /* livrer */ } http_response_code(200); ``` #### Python ```python from cartflox import Webhook event = Webhook.construct_event( request.get_data(), request.headers.get("X-Afriflow-Signature"), request.headers.get("X-Afriflow-Timestamp"), os.environ["CARTFLOX_SECRET_KEY"], ) if event["event"] == "payment.completed" and event["livemode"]: ... # livrer ``` ## Mode test dans les SDK Donnez au client votre clé secrète de test (`af_test_sec_...`) : rien d'autre ne change. `isTestKey` (Node et PHP) ou `is_test_key` (Python) dit quelle clé le client porte, et `checkout.sessions.simulate(id, issue)` dénoue une session de test sans ouvrir la page, pour vos tests automatisés. Voir [Mode test](https://cartflox.com/docs/demarrage/mode-test/). ## Ce que couvrent les SDK Sessions (créer, statut, moyens, simuler), liens de paiement, transferts (créer, lire, lister, options), livraisons de webhooks (lister, lire, renvoyer), configuration du webhook, routage, export des transactions, et l'API partenaire. Chaque méthode rend le JSON complet de la réponse ; les erreurs HTTP lèvent `CartfloxError` (Node), `CartfloxException` (PHP) ou `CartfloxError` (Python) avec le statut, le `code` et le message. --- # Orchestration multi-passerelles Source: https://cartflox.com/docs/routage/ > Brancher plusieurs passerelles, choisir laquelle encaisse par opérateur et par pays, et laisser Cartflox basculer en cas d'échec. Vous pouvez brancher plusieurs passerelles et décider, opérateur par opérateur et pays par pays, laquelle encaisse. Si elle échoue, Cartflox bascule automatiquement sur la suivante. ## Dans le tableau de bord 1. [Passerelles](https://cartflox.com/gateways) : ajoutez chaque agrégateur avec ses clés, validées à l'enregistrement. 2. [Moyens de paiement](https://cartflox.com/methods) : pour chaque opérateur (Orange Money Côte d'Ivoire, Wave Sénégal, MTN Bénin...), choisissez la passerelle qui le sert, ou laissez Cartflox choisir. 3. Le moteur de routage mémorise le taux de succès de chaque passerelle et privilégie celles qui aboutissent le plus. ## Par l'API **GET /v1/route?country=SN&providers=paydunya,cinetpay¤cy=XOF** : Opérateurs disponibles et classement des passerelles pour un pays ```bash title="curl" curl "https://cartflox.com/api/v1/route?country=SN&providers=paydunya,cinetpay¤cy=XOF" ``` ```json title="200 OK" { "country": "SN", "currency": "XOF", "availableMethods": [ { "code": "wave", "name": "Wave", "providers": ["paydunya", "cinetpay"] } ], "rankedProviders": [ "paydunya", "cinetpay" ] } ``` **POST /v1/route** : Décision de routage pour un paiement précis (clé API requise) | Paramètre | Type | Requis | Description | |---|---|---|---| | `country` | string | Oui | Code pays ISO alpha-2 | | `amount` | number | Oui | Montant | | `availableProviders` | string[] | Oui | Passerelles candidates (paydunya, cinetpay, pawapay, fedapay, stripe...) | | `currency` | string | Non | Devise | | `paymentMethod` | string | Non | Opérateur souhaité (wave, orange, mtn, moov, card...) | | `phone` | string | Non | Numéro du client : l'opérateur est déduit du préfixe si `paymentMethod` est absent | | `preferredProvider` | string | Non | Forcer une passerelle | ```json title="200 OK" { "routing": { "provider": "paydunya", "method": "orange", "reason": "best_success_rate" }, "detectedOperator": "orange", "availableMethods": [ "..." ], "rankedProviders": [ "paydunya", "cinetpay" ] } ``` Les valeurs acceptées pour `providers` : `paydunya`, `pawapay`, `flutterwave`, `feexpay`, `paystack`, `cinetpay`, `stripe`, `kkiapay`, `coinbase`, `fedapay`, `notchpay`, `cryptomus`, `qosic`, `monetbill`, `payplus`, `hub2`, `lengopay`, `wave`, `paytech`, `onepay`, `djamo`, `ipay`. --- # Envoyer de l'argent Source: https://cartflox.com/docs/transferts/envoyer-de-l-argent/ > POST /v1/transfers, envoyer une somme vers un compte Mobile Money, pays et opérateurs, statuts, idempotence, webhooks transfer.succeeded et transfer.failed. Les transferts font sortir de l'argent : vous envoyez une somme vers le compte Mobile Money d'un fournisseur, d'un salarié ou d'un client à rembourser. Cartflox confie l'envoi à l'une de vos passerelles qui sait le faire, avec vos propres clés. Les espaces gérés par nyole (argent tenu par Cartflox) n'ont pas accès aux transferts pour le moment. > **Ouvert à tous les espaces** Les transferts utilisent les mêmes passerelles que l'encaissement : dès qu'une de vos passerelles sait envoyer de l'argent (PawaPay, PayDunya, Hub2, Notch Pay, Monetbil, Flutterwave, Paystack, FedaPay, FeexPay), vous pouvez envoyer, avec les clés déjà saisies. Dans votre tableau de bord, la rubrique Transferts d'argent a deux réglages : **Passerelles** (celles que Cartflox peut utiliser, et ce qui leur manque) et **Méthodes** (les opérateurs autorisés, pays par pays). L'administration Cartflox peut fermer les transferts d'un espace ; l'API répond alors `403` avec le code `transferts_inactifs`. **POST /v1/transfers** : Envoyer une somme vers un compte Mobile Money ## Paramètres | Paramètre | Type | Requis | Description | |---|---|---|---| | `amount` | number | Oui | Montant reçu par le bénéficiaire, entier pour les francs (5000 pour 5 000 XOF) | | `country` | string | Oui | Pays du bénéficiaire, ISO 3166-1 alpha-2 : CI, SN, BJ, BF, TG, ML, NE, GN, CM, GA, CG, CD, GH, NG, KE, TZ, UG, RW, ZM | | `operator` | string | Oui | Opérateur du bénéficiaire : `orange_money`, `mtn_money`, `moov_money`, `wave`, `free_money`, `tmoney`, `mpesa`, `airtel_money`... Les alias courants (`mtn`, `orange`, `MTN_MOMO_CIV`) sont acceptés. | | `phone` | string | Oui | Numéro du bénéficiaire, tel qu'on le compose dans le pays (`0712345678`) ou en international (`+2250712345678`) | | `currency` | string | Non | Devise, déduite du pays (XOF, XAF, GHS, KES...). Une devise qui ne correspond pas au pays est refusée. | | `recipient_name` | string | Non | Nom du bénéficiaire, pour vos relevés et pour le fournisseur | | `description` | string | Non | Motif, transmis quand l'opérateur l'affiche au bénéficiaire | | `idempotency_key` | string | Non | Même rôle que l'en-tête `Idempotency-Key` ci-dessous | | `metadata` | object | Non | Vos données libres, conservées avec le transfert | ### En-têtes | En-tête | Valeur | |---|---| | `Authorization` | `Bearer af_live_sec_...` (ou `x-api-key`). La clé **secrète** uniquement : la clé publique du widget est refusée, elle ne peut pas faire sortir d'argent. | | `Content-Type` | `application/json` | | `Idempotency-Key` | Fortement conseillé. Une chaîne unique par transfert (numéro de facture, identifiant de paie...) : si le même appel est rejoué, le transfert existant est renvoyé (HTTP 200, en-tête `Idempotent-Replayed: true`) au lieu d'envoyer une seconde fois l'argent. | ## Exemples #### curl ```bash curl -X POST https://cartflox.com/api/v1/transfers \ -H "Authorization: Bearer af_live_sec_VOTRE_CLE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: salaire-2026-09-awa" \ -d '{ "amount": 50000, "country": "CI", "operator": "orange_money", "phone": "0712345678", "recipient_name": "Awa Koné", "description": "Salaire septembre" }' ``` #### Node.js ```js const res = await fetch("https://cartflox.com/api/v1/transfers", { method: "POST", headers: { Authorization: `Bearer ${process.env.CARTFLOX_SECRET_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "salaire-2026-09-awa", }, body: JSON.stringify({ amount: 50000, country: "CI", operator: "orange_money", phone: "0712345678", recipient_name: "Awa Koné", description: "Salaire septembre", }), }); const transfert = await res.json(); console.log(transfert.status); // "processing" le plus souvent, "succeeded" ou "failed" ``` #### PHP ```php $ch = curl_init("https://cartflox.com/api/v1/transfers"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("CARTFLOX_SECRET_KEY"), "Content-Type: application/json", "Idempotency-Key: salaire-2026-09-awa", ], CURLOPT_POSTFIELDS => json_encode([ "amount" => 50000, "country" => "CI", "operator" => "orange_money", "phone" => "0712345678", "recipient_name" => "Awa Koné", "description" => "Salaire septembre", ]), ]); $transfert = json_decode(curl_exec($ch), true); echo $transfert["status"]; ``` #### Python ```python res = requests.post( "https://cartflox.com/api/v1/transfers", headers={ "Authorization": f"Bearer {os.environ['CARTFLOX_SECRET_KEY']}", "Idempotency-Key": "salaire-2026-09-awa", }, json={ "amount": 50000, "country": "CI", "operator": "orange_money", "phone": "0712345678", "recipient_name": "Awa Koné", "description": "Salaire septembre", }, ) print(res.json()["status"]) ``` ## Réponse ```json { "id": "cmfx1a2b3c4d5e6f7g8h9i0j", "reference": "ddf9f0bf-f310-4126-83d9-dc8bc92b6e79", "status": "processing", "amount": 50000, "currency": "XOF", "fee": 0, "country": "CI", "operator": "orange_money", "provider_operator": "ORANGE_CIV", "phone": "2250712345678", "recipient_name": "Awa Koné", "description": "Salaire septembre", "provider": "PawaPay", "provider_reference": null, "failure": null, "idempotency_key": "salaire-2026-09-awa", "created": "2026-09-18T09:12:41.000Z", "completed": null } ``` | Statut | Sens | |---|---| | `pending` | Créé, pas encore confié au fournisseur | | `processing` | Confié au fournisseur, l'opérateur traite l'envoi. C'est le cas le plus fréquent juste après l'appel. | | `succeeded` | L'argent est arrivé sur le compte du bénéficiaire. `completed` porte la date. | | `failed` | Refusé par l'opérateur ou le fournisseur. `failure.code` et `failure.message` disent pourquoi. Rien n'a été débité. | `fee` vaut `0` : Cartflox ne prélève rien sur un transfert, c'est votre agrégateur qui applique ses frais sur son portefeuille. ## Suivre un transfert Le statut évolue après l'appel. Deux façons de le suivre, à utiliser ensemble : **GET /v1/transfers/{id}** : Un transfert, par identifiant Cartflox ou par référence **GET /v1/transfers?status=processing&limit=50** : Vos derniers transferts, filtrables par statut Et les webhooks `transfer.succeeded` et `transfer.failed`, envoyés à l'adresse configurée pour vos paiements, signés de la même manière, avec le transfert complet dans `data`. Voir [les événements](https://cartflox.com/docs/webhooks/evenements/). ## Pays et opérateurs de votre espace Les opérateurs disponibles dépendent des passerelles que vous avez branchées. Pour les connaître sans deviner : **GET /v1/transfers?options=1** : Pays, devises et opérateurs ouverts à cet espace ```json { "countries": [ { "code": "CI", "name": "Côte d'Ivoire", "currency": "XOF", "operators": [ { "code": "orange_money", "name": "Orange Money", "provider": "PawaPay" }, { "code": "mtn_money", "name": "MTN Mobile Money", "provider": "PawaPay" }, { "code": "moov_money", "name": "Moov Money", "provider": "PawaPay" }, { "code": "wave", "name": "Wave", "provider": "PawaPay" } ] } ] } ``` > **D'où part l'argent** L'argent part du portefeuille de votre agrégateur (PawaPay, PayDunya, Hub2, Notch Pay, Monetbil, Flutterwave, Paystack, FedaPay ou FeexPay), qui le débite avec ses propres frais et à ses propres conditions (portefeuille de décaissement approvisionné, adresse IP autorisée chez certains). ## Erreurs | HTTP | `code` | Sens | |---|---|---| | 401 | `unauthorized` | Clé absente, invalide, ou clé publique | | 403 | `transferts_inactifs` | Les transferts ne sont pas ouverts sur cet espace | | 403 | `connect_non_disponible` | Les espaces gérés par nyole n'ont pas accès aux transferts | | 400 | `pays_non_desservi` | Pays hors de la liste | | 400 | `operateur_non_desservi` | Aucune de vos passerelles ne sait envoyer vers cet opérateur dans ce pays | | 400 | `methode_desactivee` | Vous avez coupé cet opérateur dans vos méthodes de transfert | | 400 | `telephone_invalide` | Le numéro n'est pas un numéro possible de ce pays | | 400 | `devise_invalide` | La devise ne correspond pas au pays | | 400 | `montant_invalide` | Montant nul ou négatif | | 429 | `plafond_journalier` | Le nombre de transferts par jour de l'espace est atteint | | 429 | `rate_limited` | Plus de 30 appels par minute | --- # Configurer votre webhook Source: https://cartflox.com/docs/webhooks/configuration/ > Renseigner l'URL de réception, par le tableau de bord ou par l'API, et comprendre les nouvelles tentatives. Un webhook est une requête HTTP POST que Cartflox envoie à votre serveur à chaque changement de statut d'un paiement. C'est la façon fiable de valider une commande. ## Depuis le tableau de bord Dans [API et Logs](https://cartflox.com/developer), renseignez l'URL de réception. Elle doit être publique et en HTTPS : les adresses locales et les réseaux privés sont refusés. ## Par l'API **GET /v1/config/webhook** : Lire l'URL configurée **PATCH /v1/config/webhook** : Définir ou changer l'adresse, choisir les événements | Champ | Type | Description | |---|---|---| | `webhookUrl` | string | Adresse publique en https. Une chaîne vide retire le webhook. | | `mode` | string | `live` (défaut avec une clé de production) ou `test` : quelle adresse on règle. Voir [Mode test](https://cartflox.com/docs/demarrage/mode-test/). | | `events` | string[] | Les événements à recevoir, parmi ceux listés dans [Événements](https://cartflox.com/docs/webhooks/evenements/). Absent ou complet : tous. | ```bash title="curl" curl -X PATCH https://cartflox.com/api/v1/config/webhook \ -H "Authorization: Bearer af_live_sec_VOTRE_CLE" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://maboutique.com/webhooks/cartflox", "events": ["payment.completed", "payment.failed", "payment.cancelled"] }' ``` ```json title="200 OK" { "success": true, "webhookUrl": "https://maboutique.com/webhooks/cartflox", "testWebhookUrl": null, "events": ["payment.completed", "payment.failed", "payment.cancelled"], "available_events": ["payment.completed", "payment.failed", "payment.cancelled", "payment.updated", "transfer.succeeded", "transfer.failed"] } ``` ## Livraison et nouvelles tentatives - Délai d'attente : 10 secondes. Répondez 2xx immédiatement et traitez ensuite. - En cas d'échec (erreur réseau, réponse non 2xx, délai dépassé) : jusqu'à 10 tentatives sur environ 72 heures (1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h, 24 h), puis une notification dans votre tableau de bord. - Chaque envoi est journalisé et se renvoie à la main : [Journal et rejeu](https://cartflox.com/docs/webhooks/livraisons/). - Le même événement peut donc arriver deux fois, par exemple si votre serveur a répondu trop tard : rendez votre traitement idempotent, en vous appuyant sur l'en-tête `X-Afriflow-Delivery`, ou sur `data.id` et `data.status`. - Un webhook manqué se rattrape aussi en interrogeant [le statut de la session](https://cartflox.com/docs/paiements/verifier-le-statut/). --- # Événements Source: https://cartflox.com/docs/webhooks/evenements/ > Les événements envoyés par Cartflox et le corps de la requête de webhook. Chaque requête contient un événement, les données du paiement, un horodatage et `livemode`. Vous choisissez les événements reçus dans [API et Logs](https://cartflox.com/developer) ou par [l'API](https://cartflox.com/docs/webhooks/configuration/). | Événement | Quand | |---|---| | `payment.completed` | Paiement confirmé (statut `SUCCESS`). C'est l'événement à attendre pour livrer. | | `payment.failed` | Paiement échoué ou refusé (statut `FAILED`). | | `payment.cancelled` | Paiement abandonné par le client (page quittée, session expirée) ou annulé par le marchand (statut `CANCELLED`). Événement ajouté le 19 septembre 2026 : les webhooks configurés avant cette date ne le reçoivent qu'après l'avoir coché dans API et Logs. | | `payment.updated` | Tout autre changement de statut : `REFUNDED`, retour à `PENDING`. | | `transfer.succeeded` | Transfert d'argent sortant arrivé sur le compte du bénéficiaire (`data` = le transfert, voir [Envoyer de l'argent](https://cartflox.com/docs/transferts/envoyer-de-l-argent/)). | | `transfer.failed` | Transfert d'argent refusé par l'opérateur ou le fournisseur, avec `data.failure`. | ## Corps de la requête ```json title="POST https://maboutique.com/webhooks/cartflox" { "event": "payment.completed", "livemode": true, "data": { "id": "cmf3k2p1x0001abcd9e8f7g6h", "order_id": "CS-MF3K2A-9X1QZ", "status": "SUCCESS", "amount": 5000, "currency": "XOF", "provider": "PayDunya", "provider_reference": "pd_7f3a9c", "customer_name": "Awa Koné", "customer_email": "awa@example.com", "customer_phone": "+2250700000000", "metadata": { "order_id": "1042", "source": "checkout_session" }, "completed_at": "2026-09-02T10:17:42.000Z" }, "timestamp": "2026-09-02T10:17:42.000Z" } ``` | Champ | Description | |---|---| | `data.id` | Identifiant de la session, le même que celui renvoyé à la création | | `data.order_id` | Référence Cartflox (`CS-...`) | | `data.status` | `SUCCESS`, `FAILED`, `PENDING`, `CANCELLED` ou `REFUNDED` | | `data.provider` | Passerelle qui a traité le paiement | | `data.provider_reference` | Référence chez l'agrégateur, utile pour vos rapprochements | | `data.metadata` | Vos métadonnées, enrichies de la source (`checkout_session`, `payment_link`...) | | `data.completed_at` | Date de confirmation, `null` tant que le paiement n'est pas réussi | | `livemode` | `true` en production, `false` pour un paiement fait avec une clé de test ([mode test](https://cartflox.com/docs/demarrage/mode-test/)) | --- # Journal et rejeu Source: https://cartflox.com/docs/webhooks/livraisons/ > Chaque webhook envoyé est journalisé, rejoué jusqu'à 10 fois sur 72 heures, et se renvoie à la main depuis le tableau de bord ou l'API. Chaque envoi vers votre serveur est une **livraison** : Cartflox garde ce qui a été envoyé, quand, la réponse reçue, et réessaie tant que votre serveur n'a pas répondu 2xx. ## Rejeu automatique Votre serveur a 10 secondes pour répondre. Sans réponse 2xx, la livraison est rejouée selon ce calendrier, soit 10 tentatives sur environ 72 heures : | Tentative | Délai après la précédente | |---|---| | 1 | immédiat | | 2 | 1 minute | | 3 | 5 minutes | | 4 | 15 minutes | | 5 | 1 heure | | 6 | 3 heures | | 7 | 6 heures | | 8 | 12 heures | | 9 | 24 heures | | 10 | 24 heures | Après la dernière tentative, la livraison est marquée `failed` et une notification apparaît dans votre tableau de bord. L'adresse et la clé de signature sont relues à chaque tentative : corrigez votre adresse dans [API et Logs](https://cartflox.com/developer), la prochaine tentative la prend. Chaque requête porte deux en-têtes utiles au dédoublonnage : `X-Afriflow-Event` (le nom de l'événement) et `X-Afriflow-Delivery` (l'identifiant de la livraison, identique d'une tentative à l'autre). Un traitement idempotent sur `X-Afriflow-Delivery`, ou sur `data.id` et `data.status`, vous protège des doublons. ## Renvoyer à la main Depuis [API et Logs](https://cartflox.com/developer), le bloc « Livraisons des webhooks » liste les 50 derniers envois avec la réponse reçue. « Renvoyer » repart tout de suite, avec une signature neuve, vers votre adresse actuelle, quel que soit l'état de la livraison. ## Par l'API **GET /v1/webhooks/deliveries** : Lister les livraisons (50 par défaut, 200 au plus) **GET /v1/webhooks/deliveries/{id}** : Lire une livraison **POST /v1/webhooks/deliveries/{id}/resend** : Renvoyer une livraison Filtres de la liste : `transaction_id`, `status` (`pending`, `delivered`, `failed`), `event`, `limit`. Une clé de test ne voit que les livraisons de test. ```bash title="Les livraisons d'une transaction" curl "https://cartflox.com/api/v1/webhooks/deliveries?transaction_id=cmf3k2p1x0001abcd9e8f7g6h" \ -H "Authorization: Bearer af_live_sec_VOTRE_CLE" ``` ```json title="200 OK" { "object": "list", "data": [ { "id": "cmu8d4e5f0003abc", "object": "webhook.delivery", "event": "payment.completed", "url": "https://maboutique.com/webhooks/cartflox", "status": "delivered", "attempts": 2, "max_attempts": 10, "next_attempt_at": null, "last_status_code": 200, "last_error": null, "response_ms": 412, "transaction_id": "cmf3k2p1x0001abcd9e8f7g6h", "transfer_id": null, "livemode": true, "created_at": "2026-09-19T08:00:00.000Z", "delivered_at": "2026-09-19T08:01:03.000Z" } ], "count": 1 } ``` ```bash title="Renvoyer" curl -X POST https://cartflox.com/api/v1/webhooks/deliveries/cmu8d4e5f0003abc/resend \ -H "Authorization: Bearer af_live_sec_VOTRE_CLE" ``` La réponse est la livraison mise à jour. Limite : 30 renvois par minute et par espace. | Champ | Description | |---|---| | `status` | `pending` (nouvelle tentative prévue), `delivered` (réponse 2xx reçue), `failed` (abandonnée après la dernière tentative) | | `attempts` / `max_attempts` | Tentatives faites, et plafond | | `next_attempt_at` | Prochaine tentative automatique, `null` sinon | | `last_status_code` / `last_error` | La dernière réponse de votre serveur, ou l'erreur rencontrée (délai dépassé, réseau) | | `livemode` | `false` pour un événement de test | --- # Vérifier la signature Source: https://cartflox.com/docs/webhooks/signature/ > Vérifier la signature HMAC-SHA256 des webhooks Cartflox en Node.js, PHP et Python. Chaque webhook est signé avec votre clé secrète. Vérifiez la signature avant de traiter quoi que ce soit : sans cela, n'importe qui connaissant votre URL pourrait vous faire croire à un paiement. ## En-têtes envoyés | En-tête | Contenu | |---|---| | `X-Afriflow-Timestamp` | Horodatage Unix (secondes) de l'envoi | | `X-Afriflow-Signature` | `t=,v1=` | ## Algorithme 1. Récupérez le corps de la requête **brut**, exactement tel que reçu (pas le JSON reparsé puis re-sérialisé). 2. Concaténez `.`. 3. Calculez le HMAC-SHA256 de cette chaîne avec votre clé secrète `af_live_sec_...`. 4. Comparez en temps constant avec la valeur `v1` de l'en-tête. Refusez si l'horodatage a plus de 5 minutes (protection contre le rejeu). ## Exemples #### Node.js ```js title="Express" const app = express(); function verifierSignature(corpsBrut, entetes, secret) { const ts = entetes["x-afriflow-timestamp"]; const sig = (entetes["x-afriflow-signature"] || "") .split(",").find((p) => p.startsWith("v1="))?.slice(3); if (!ts || !sig) return false; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // 5 minutes const attendu = crypto.createHmac("sha256", secret).update(`${ts}.${corpsBrut}`).digest("hex"); return attendu.length === sig.length && crypto.timingSafeEqual(Buffer.from(attendu), Buffer.from(sig)); } // Le corps doit rester BRUT : express.raw, pas express.json app.post("/webhooks/cartflox", express.raw({ type: "application/json" }), (req, res) => { const brut = req.body.toString("utf8"); if (!verifierSignature(brut, req.headers, process.env.CARTFLOX_SECRET_KEY)) { return res.status(400).send("signature invalide"); } const evt = JSON.parse(brut); if (evt.event === "payment.completed") { // Marquez la commande evt.data.metadata.order_id comme payée. // Rendez ce traitement idempotent : le même événement peut arriver deux fois. } res.sendStatus(200); // répondez vite, traitez le reste en tâche de fond }); ``` #### PHP ```php $brut = file_get_contents("php://input"); $ts = $_SERVER["HTTP_X_AFRIFLOW_TIMESTAMP"] ?? ""; $sig = ""; foreach (explode(",", $_SERVER["HTTP_X_AFRIFLOW_SIGNATURE"] ?? "") as $partie) { if (str_starts_with($partie, "v1=")) $sig = substr($partie, 3); } $attendu = hash_hmac("sha256", $ts . "." . $brut, getenv("CARTFLOX_SECRET_KEY")); if ($ts === "" || $sig === "" || abs(time() - (int) $ts) > 300 || !hash_equals($attendu, $sig)) { http_response_code(400); exit("signature invalide"); } $evt = json_decode($brut, true); if ($evt["event"] === "payment.completed") { // Commande payée : $evt["data"]["metadata"]["order_id"] } http_response_code(200); ``` #### Python ```python title="Flask" from flask import Flask, request, abort app = Flask(__name__) SECRET = os.environ["CARTFLOX_SECRET_KEY"].encode() @app.post("/webhooks/cartflox") def webhook(): brut = request.get_data() # corps brut, surtout pas request.json ts = request.headers.get("X-Afriflow-Timestamp", "") parties = dict(p.split("=", 1) for p in request.headers.get("X-Afriflow-Signature", "").split(",") if "=" in p) attendu = hmac.new(SECRET, f"{ts}.".encode() + brut, hashlib.sha256).hexdigest() if not ts or abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(attendu, parties.get("v1", "")): abort(400) evt = request.get_json(force=True) if evt["event"] == "payment.completed": pass # commande payée : evt["data"]["metadata"]["order_id"] return "", 200 ``` > **Ne validez jamais sans signature** Si votre framework ne donne pas accès au corps brut, désactivez son analyse JSON automatique sur cette route uniquement. --- # English # Authentication Source: https://cartflox.com/docs/en/authentification/ > Public key, secret key, accepted headers and rotation of Cartflox API keys. Every server-side call is authenticated with your secret key. ## Key types | Key | Role | |---|---| | `af_live_pub_...` | Public key. Identifies your application and may appear client-side (widget, SoftPay). It can only create sessions and read their payment methods and status. | | `af_live_sec_...` | Secret key. Signs your API calls and your webhooks. Server-side only. | | `af_test_pub_...` / `af_test_sec_...` | The same, in test mode: no aggregator called, simulated payments, isolated data. See [Test mode](https://cartflox.com/docs/en/demarrage/mode-test/). | ## Usage Pass the secret key in the `Authorization: Bearer ` header. The `x-api-key: ` header is accepted as an alternative. Every object response (session, transfer, webhook delivery) and every webhook carries `livemode`: `true` with a production key, `false` with a test key. ```bash title="curl" curl https://cartflox.com/api/v1/config/webhook \ -H "Authorization: Bearer af_live_sec_YOUR_KEY" ``` ## Rotation From [API and Logs](https://cartflox.com/developer), you can regenerate your keys. The old secret key stops working immediately: update your servers before confirming. > **Key exposed?** Regenerate it right away. A leaked secret key can be used to create payment sessions in your name and to forge webhooks. --- # Quickstart Source: https://cartflox.com/docs/en/demarrage/demarrage-rapide/ > Five minutes to accept your first payment with Cartflox. Five minutes is all it takes to accept a first payment. 1. **Create your account and connect your aggregator.** [Create an account](https://cartflox.com/auth/register), then in [Gateways](https://cartflox.com/gateways), add your aggregator's API keys. Cartflox checks the keys with the provider before saving them. In [Payment methods](https://cartflox.com/methods), enable the operators your customers will be able to use. 2. **Get your API keys.** In [API and Logs](https://cartflox.com/developer), you will find your public key `af_live_pub_...` and your secret key `af_live_sec_...`. The secret key authenticates your server-side calls: never put it in a web page or a mobile app. 3. **Create a payment session.** ```bash title="curl" curl -X POST https://cartflox.com/api/v1/checkout/sessions \ -H "Authorization: Bearer af_live_sec_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1042" \ -d '{ "amount": 5000, "currency": "XOF", "customer_name": "Awa Koné", "customer_email": "awa@example.com", "customer_phone": "+2250700000000", "description": "Order #1042", "success_url": "https://maboutique.com/merci", "cancel_url": "https://maboutique.com/panier", "metadata": { "order_id": "1042" } }' ``` The response contains the address of the payment page: redirect your customer there. ```json title="Response 201" { "id": "cmf3k2p1x0001abcd9e8f7g6h", "object": "checkout.session", "url": "https://checkout.cartflox.com/cmf3k2p1x0001abcd9e8f7g6h", "order_id": "CS-MF3K2A-9X1QZ", "amount": 5000, "currency": "XOF", "status": "pending", "created": "2026-09-02T10:15:00.000Z" } ``` 4. **Receive the confirmation.** Enter your server's URL in [API and Logs](https://cartflox.com/developer), or through the API (see [Webhooks](https://cartflox.com/docs/en/webhooks/configuration/)). As soon as the payment succeeds, Cartflox sends the signed `payment.completed` event. You can also check [the session status](https://cartflox.com/docs/en/paiements/verifier-le-statut/) at any time. 5. **Go live.** Replace your gateway's sandbox keys with its production keys. Complete your [identity verification](https://cartflox.com/settings?tab=kyc): it is mandatory for a payment platform. > No server? The [widget](https://cartflox.com/docs/en/integrations/widget/), [SoftPay](https://cartflox.com/docs/en/integrations/softpay/) and [payment links](https://cartflox.com/docs/en/integrations/sans-code/) work with your public key alone. --- # Introduction Source: https://cartflox.com/docs/en/demarrage/introduction/ > What the Cartflox API does, the base URL, the response format and the environments. Cartflox is a pan-African payment orchestrator: one integration to accept Mobile Money (Orange Money, Wave, MTN MoMo, Moov...) and card payments in more than 15 countries, through your own aggregator accounts: 22 supported gateways (PayDunya, PawaPay, Hub2, Wave Business, Djamo, CinetPay, Stripe, Paystack, Flutterwave, FedaPay, Magma OnePay, PayTech, iPay Money...). The full list is on the [Supported gateways](https://cartflox.com/docs/en/paiements/passerelles/) page. > **Your money never passes through Cartflox** You plug your own aggregator keys into the dashboard. Each payment is collected directly on your accounts. Cartflox handles routing, the payment page, tracking and webhooks. > **Cartflox is open source** The engine is published under the AGPL-3.0 licence: [github.com/uvan00/cartflox](https://github.com/uvan00/cartflox). You can read the code, check what happens to your keys, and host it yourself on your own server. The managed workspace, where Cartflox collects on your behalf, is not part of the open version. ## What the API does Create a session in one call, redirect your customer to the hosted payment page, and get the result through a webhook or by checking the status. Generate a link to share on WhatsApp, by email or on social media, without writing a single line of client-side code. Each operator and each country is routed to the gateway of your choice, with automatic failover if it fails. Every status change is pushed to your server with an HMAC-SHA256 signature that you verify. ## Base URL All requests are made over HTTPS to the address below. Versioned endpoints are prefixed with `/v1`. ```text title="Base URL" https://cartflox.com/api ``` ## Response format Responses are JSON. An error returns a 4xx or 5xx HTTP status code and an object `{ "error": "..." }` (or `{ "success": false, "message": "..." }` on the payment page endpoints). Amounts are integers in the smallest common unit of the currency: `5000` means 5,000 XOF. ## Environments There is no separate "sandbox" host: your keys decide the world. Each workspace has two sets of keys, `af_test_...` to build and verify your integration without calling any aggregator (the outcome of the payment is chosen on the page), `af_live_...` to collect real payments. Test data stays apart from production. Everything is described in [Test mode](https://cartflox.com/docs/en/demarrage/mode-test/). A gateway can also, on its side, be connected with your aggregator's sandbox keys: the dashboard flags it (orange banner). > **Test numbers** In Cartflox test mode, any valid number will do: the outcome is chosen on the page. With an aggregator's sandbox keys, use its test numbers and cards (PayDunya, CinetPay, Stripe...). --- # Test mode Source: https://cartflox.com/docs/en/demarrage/mode-test/ > af_test_... test keys, simulated payments from the payment page, webhooks with livemode false, isolated data deleted after 90 days. Every workspace has two sets of keys: one for production and one for testing. Same API, same routes, same payment page. With a test key, no aggregator is called and no money moves: you build and verify your integration before your identity is even verified. ## The two sets of keys | Production | Test | Role | |---|---|---| | `af_live_pub_...` | `af_test_pub_...` | Public key (widget, SoftPay): create a session and read its status | | `af_live_sec_...` | `af_test_sec_...` | Secret key (server): everything else, and the signing of webhooks | Test keys live in [API and Logs](https://cartflox.com/developer), below the production keys, and are rotated separately. Until your identity is verified, your workspace stays in sandbox and only test keys exist: a production key would answer `403` with the code `identity_unverified`. Once verified, production keys appear and the Sandbox switch unlocks. ## What happens with a test key 1. You create a session as usual. The response carries `"livemode": false`. 2. The payment page shows a "Test mode" banner and, once the method and phone number are chosen, offers two buttons: **simulate a successful payment** or **simulate a failure**. If the workspace has no gateway yet, a placeholder method "Mobile Money (test)" is offered so the flow can be completed. 3. The transaction moves to `SUCCESS` or `FAILED` with `provider: "sandbox"`, and your webhook receives `payment.completed` or `payment.failed` exactly as in production, with `"livemode": false`. ```bash title="Create a test session" curl -X POST https://cartflox.com/api/v1/checkout/sessions \ -H "Authorization: Bearer af_test_sec_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 5000, "currency": "XOF", "customer_email": "test@example.com" }' ``` ```json title="201 Created" { "id": "cmu8a1b2c0001xyz", "object": "checkout.session", "url": "https://checkout.cartflox.com/cmu8a1b2c0001xyz", "order_id": "CS-MU8A1B-2C0X1", "amount": 5000, "currency": "XOF", "status": "pending", "livemode": false, "created": "2026-09-19T08:00:00.000Z" } ``` ### Simulate the outcome without opening the page For automated tests, the outcome can also be triggered through the API, without a key: the session identifier, which cannot be guessed, is enough. **POST /api/checkout/sandbox** : Settle a test payment ```bash title="curl" curl -X POST https://cartflox.com/api/checkout/sandbox \ -H "Content-Type: application/json" \ -d '{ "transactionId": "cmu8a1b2c0001xyz", "issue": "succes" }' ``` `issue` is `succes` or `echec`. The route rejects a production transaction (HTTP 403) and a transaction that is already settled (HTTP 409). ## Test webhooks Test events go to your production address, with `livemode: false`, unless you register a separate test address: ```bash title="Test address" curl -X PATCH https://cartflox.com/api/v1/config/webhook \ -H "Authorization: Bearer af_test_sec_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://staging.mystore.com/webhooks/cartflox", "mode": "test" }' ``` The signature of a test event is computed with your **test secret key**. Verify the signature with the key of the same world as the event (`livemode`). ## Isolated data - Test payments never appear in your production lists, statistics, exports or notifications. A test key only reads test sessions, and the other way round. - To see them in the dashboard: the "Sandbox" switch at the top of the page. It never blocks your real payments: the keys decide the world of a payment. A workspace whose identity is not verified yet stays in sandbox, switch locked. - Test transactions are deleted after 90 days. ## Limits of test mode | Not available | Why | |---|---| | Money transfers (`POST /v1/transfers`) | They move money out: HTTP 403, code `test_mode`. | | Payment links (`POST /v1/payment-links`) | A link lives in production. Create a test session instead. | | Real bank card | The card form is not mounted in test mode: simulate the outcome. | > **What about a gateway's test mode?** It still exists. A gateway connected with your aggregator's sandbox keys stays in test mode on its side, regardless of your Cartflox keys. Cartflox test mode is simpler: nothing to configure at the aggregator, the outcome is chosen on the page. --- # Cartflox documentation Source: https://cartflox.com/docs/en/ > Accept Mobile Money and card payments across Africa, with your own gateways. One API, a widget, SoftPay, WooCommerce. ## Choose how you want to get paid A payment link to share on WhatsApp, by email or on your website. Not a single line of code. [Create a link](https://cartflox.com/docs/en/integrations/sans-code/) Two lines on your page: a button opens the Cartflox payment page on top of your site. [Install the widget](https://cartflox.com/docs/en/integrations/widget/) Payment inside your own page, in your colors or fully designed by you. Operator, phone number, OTP, tracking: everything stays on your site. [Discover SoftPay](https://cartflox.com/docs/en/integrations/softpay/) The official plugin: install it, paste your key, and your customers pay with Mobile Money. [Install the plugin](https://cartflox.com/docs/en/integrations/woocommerce/) Create sessions from your server and receive confirmations through signed webhooks. [View the reference](https://cartflox.com/docs/en/paiements/creer-une-session/) Route each operator and each country to the gateway of your choice, with automatic failover when one fails. [Configure routing](https://cartflox.com/docs/en/routage/) ## Three things to know - [Your money never passes through Cartflox](https://cartflox.com/docs/en/demarrage/introduction/): Each payment is collected directly on your aggregator accounts. Cartflox takes no commission. - [A signed webhook for every payment](https://cartflox.com/docs/en/webhooks/signature/): HMAC-SHA256 with your secret key, ten attempts over 72 hours, timestamp against replay. The only reliable proof of payment. - [Test keys from day one](https://cartflox.com/docs/en/demarrage/mode-test/): af_test_...: same API, simulated payments from the page, webhooks with livemode false, isolated data. --- # No code: payment link and button Source: https://cartflox.com/docs/en/integrations/sans-code/ > Get paid without a developer using a Cartflox payment link, on Wix, Webflow, Shopify, WordPress, Linktree, WhatsApp or email. No developer? A payment link is all you need. It works everywhere: Wix, Webflow, Shopify, WordPress, Linktree, WhatsApp, email. 1. In the dashboard, open [Payment links](https://cartflox.com/payment-links/new) and create a link with a title and an amount. 2. Copy the resulting address, in the form `https://cartflox.com/pay/xxxxxxxx`. 3. Paste it into a button on your website, or send it directly to your customer. ## Ready-to-paste HTML button ```html title="HTML" Pay 25,000 XOF ``` The customer's phone number is requested on the payment page. You are notified of each payment in the dashboard, by email and, if you have configured one, by [webhook](https://cartflox.com/docs/en/webhooks/configuration/): the link reference is in `data.metadata.paymentLinkId`. --- # SoftPay: native payment, with your design Source: https://cartflox.com/docs/en/integrations/softpay/ > The payment form inside your page, stylable with CSS variables, or fully designed by you in headless mode. SoftPay displays the operator selection, the phone number, the confirmation code, the app link (Wave, Djamo) and the payment tracking directly inside your page, without going through the Cartflox page. You keep control of the entire style, or you design every screen yourself. Only your public key is used in the browser. The footer of the form always carries the "Powered by Cartflox" mention with a link to cartflox.com: it is part of the SoftPay terms of use and cannot be removed. You can change its color, not hide it. Try it right now: [SoftPay demo](https://cartflox.com/softpay-demo) with your public key and your colors. ## Option A: ready-made form, in your colors ```html title="HTML"
``` ### `mount` options | Option | Type | Required | Description | |---|---|---|---| | `container` | string or element | Yes | CSS selector or element that receives the form | | `amount` | number | Yes | Amount, integer for CFA francs | | `currency` | string | No | `XOF` by default | | `country` | string | No | Country of the offered methods (`ci`, `sn`, `bj`, `ml`, `bf`, `tg`, `cm`, `gn`...). Without a value: all active operators | | `description`, `customer_name`, `customer_email`, `customer_phone`, `metadata`, `success_url`, `cancel_url` | | No | Same fields as [`POST /v1/checkout/sessions`](https://cartflox.com/docs/en/paiements/creer-une-session/) | | `merchant_name`, `merchant_logo` | string | No | Name and logo (`https` address) shown on the equivalent Cartflox page and on receipts, instead of your workspace's | | `title` | string | No | Form title ("Payment" by default) | | `theme` | object | No | CSS variables without the prefix: `{ accent, "accent-text", bg, text, muted, border, field, radius, font }` | | `labels` | object | No | Labels to replace (see Texts) | | `autoRedirect` | boolean | No | `false` so that the customer is not sent to a hosted page automatically: you receive the URL in `onRedirect` and handle it yourself | | `timeoutMs` | number | No | Maximum time to track a pending payment (180,000 ms by default) | | `onSession`, `onStatus`, `onRedirect`, `onSuccess`, `onError` | function | No | See Events | `configure` accepts `publicKey` (required), `country` (default country), `css: false` (no injected styles) and `baseUrl` (only if you serve the script from another domain). ### Customize the style The form is built with CSS variables and stable classes. Override them in your own stylesheet, or pass `{ css: false }` to `configure` to inject no styles at all and write everything yourself. | Variable | Role | |---|---| | `--cf-accent` | Color of the Pay button and of the selection | | `--cf-accent-text` | Button text color | | `--cf-bg` | Form background | | `--cf-text` | Main text | | `--cf-muted` | Secondary text | | `--cf-border` | Borders | | `--cf-field` | Field background | | `--cf-radius` | Overall border radius | | `--cf-font` | Font (`inherit` by default: your page's font) | ```css title="CSS example: your brand style" .cf-sp { --cf-accent: #c01826; --cf-radius: 0; --cf-font: "Questrial", sans-serif; } .cf-sp-bouton { text-transform: uppercase; letter-spacing: .06em; } .cf-sp-methode.cf-sp-actif { box-shadow: none; background: #fff3f3; } ``` Available classes: `.cf-sp`, `.cf-sp-entete`, `.cf-sp-titre`, `.cf-sp-montant`, `.cf-sp-methodes`, `.cf-sp-methode`, `.cf-sp-actif`, `.cf-sp-telephone`, `.cf-sp-indicatif`, `.cf-sp-input`, `.cf-sp-otp`, `.cf-sp-ussd`, `.cf-sp-bouton`, `.cf-sp-lien`, `.cf-sp-erreur`, `.cf-sp-attente`, `.cf-sp-spinner`, `.cf-sp-qr`, `.cf-sp-pied`, `.cf-sp-securise`, `.cf-sp-propulse` (the Cartflox mention: free color, mandatory display). Each screen also carries a `.cf-sp-vue-` class, where `` is one of `formulaire` (form), `attente` (waiting), `otp`, `application`, `redirection`, `succes` (success), `echec` (failure). ### Texts All labels can be replaced with the `labels` option, for example `labels: { payer: "Pay now", telephone: "Your Mobile Money number", succes: "Payment received!" }`. ### Events | Event | When | |---|---| | `onSession(s)` | Session created: `s.id`, `s.orderId`, `s.url` (the equivalent Cartflox page) | | `onStatus(r)` | On each payment or tracking response: `r.status` | | `onRedirect(url)` | Hosted page (cards, some aggregators): the URL the customer is leaving for, `autoRedirect: false` to handle it yourself. Also called for an app link (Wave, Djamo): the widget then stays displayed with the "Open" button and tracks the payment. | | `onSuccess(r)` | Payment confirmed: `r.sessionId`, `r.orderId`, `r.amount`, `r.currency` | | `onError(e)` | Payment declined, timeout or network error: `e.message` | ## Option B: headless, you design everything Same engine, no interface: you call the session and display whatever you want. In headless mode, you draw the footer yourself: the "Powered by Cartflox" mention with a link to `https://cartflox.com` is still required, just as in the ready-made form. ```js title="JavaScript" Cartflox.softpay.configure({ publicKey: "af_live_pub_YOUR_KEY" }); const session = await Cartflox.softpay.createSession({ amount: 5000, currency: "XOF", metadata: { order_id: "1042" }, }); const methods = await session.methods("ci"); // [{ code, gatewayId, name, provider, country, type, logo, requiresPhone, dialCode }] const r = await session.pay({ method: methods[0], phone: "+2250700000000", name: "Awa Koné" }); switch (r.status) { case "SUCCESS": /* paid */ break; case "PENDING": /* request pushed to the phone: show r.instructions, then session.waitForResult() */ break; case "REQUIRE_OTP": /* confirmation code (Orange Money, Magma OnePay, Paystack...): show r.ussdCode or r.instructions, then session.pay({ ..., otp }) */ break; case "APP_LINK": /* Wave, Djamo: button to r.redirectUrl (target _blank), r.qr as an image on desktop, then session.waitForResult() */ break; case "REDIRECT": window.location.href = r.redirectUrl; break; // provider's hosted page case "INLINE_CARD": window.location.href = session.url; break; // cards: secure Cartflox page default: /* r.message */ } const fin = await session.waitForResult({ timeoutMs: 180000, onTick: (d) => console.log(d.status) }); // fin.status: SUCCESS | FAILED | CANCELLED ``` | Method | Role | |---|---| | `createSession(options)` | Same fields as `POST /v1/checkout/sessions`. Returns a session. | | `session.methods(country?)` | Available payment methods, filtered by country (ISO code). | | `session.pay({ method, phone, name, email, country, otp })` | Starts the payment. Returns `{ status, message, redirectUrl, application, qr, instructions, ussdCode, providerReference }`. `application` and `qr` (image as a data URL) are only filled for `APP_LINK`; `instructions` carries the provider's instructions when it gives any. | | `session.getStatus()` | Current status: `{ status, paid, order_id, provider }`. | | `session.waitForResult({ intervalMs, timeoutMs, onTick })` | Polls the status until a final state. | > **The browser is not proof of payment** `onSuccess` and `waitForResult` run on the customer's side. Validate the order when you receive the `payment.completed` webhook, or by calling `GET /v1/checkout/sessions/{id}/status` from your server with your secret key. Also check the amount received. --- # JavaScript widget Source: https://cartflox.com/docs/en/integrations/widget/ > Two lines of code to open the Cartflox payment page on top of your website, with your public key. Two lines of code: the widget creates the session with your public key and opens the payment page on top of your page (popup) or as a redirect. No server needed. ```html title="HTML" ``` ## Programmatic call ```js title="JavaScript" Cartflox.checkout({ amount: 5000, currency: "XOF", customer_email: "awa@example.com", customer_phone: "+2250700000000", description: "Order #1042", metadata: { order_id: "1042" }, success_url: "https://maboutique.com/merci", cancel_url: "https://maboutique.com/panier", onSuccess: function (data) { /* the window closes: show a waiting message */ }, onClose: function () {}, onError: function (err) { console.error(err); } }); ``` ## Options | Option | Type | Required | Description | |---|---|---|---| | `publicKey` | string | Yes | Your public key `af_live_pub_...` (API and Logs section). It can only create sessions. | | `mode` | string | No | `popup` (window on top of your page, default) or `redirect` (the customer leaves your page) | | `currency` | string | No | Default currency for buttons | | `success_url`, `cancel_url` | string | No | Return addresses, used in `redirect` mode | | `metadata` | object | No | Data returned in the webhook (order number, customer ID...) | | `merchant_name` | string | No | Name shown at the top of the payment page instead of your workspace name | | `merchant_logo` | string | No | `https` address of a logo shown instead of your workspace logo | > **onSuccess is not proof of payment** This callback runs in the customer's browser, and the customer can trigger it themselves. Fulfill only once you receive the `payment.completed` webhook or after checking the status server-side, and check the amount received: with a public key, the browser is the one sending it. --- # WordPress and WooCommerce Source: https://cartflox.com/docs/en/integrations/woocommerce/ > The official Cartflox plugin for WooCommerce, installed in five minutes, no code. The official plugin adds Cartflox as a payment method in your WooCommerce store. Five minutes, no code, and everything is configured from WooCommerce. - [Download cartflox-payment.zip](https://cartflox.com/downloads/cartflox-payment.zip): Official plugin, version 4.0.0. WordPress 6.2+, WooCommerce 7.6+, PHP 7.4+. ## Installation 1. WordPress: Plugins, Add New, Upload Plugin. Choose the `.zip` file, Install Now, then Activate. 2. WooCommerce: Settings, Payments, Cartflox, Manage. 3. Paste your secret key `af_live_sec_...` (Cartflox dashboard, API and Logs section). It works for a Cartflox workspace as well as for a Cartflox Connect workspace. 4. Save. The plugin verifies the key and registers your store's webhook address in Cartflox by itself. The "Connection status" panel tells you where you stand, and the "Test connection" button confirms it at any time. ## What the plugin does - When the order is placed, creates a Cartflox session and sends the customer to the payment page (Mobile Money, cards). The customer then returns to your store. - Receives the signed webhook (HMAC-SHA256 verified with your secret key, timestamp checked) and marks the order as "paid", with no action from the customer. - When the customer returns, checks the session status if the webhook has not arrived yet; the thank-you page updates on its own while the confirmation is pending. - Every 5 minutes, checks orders still pending (24 h): a safety net if the webhook cannot reach you. - Checks that the amount and currency received match the order; otherwise the order goes "on hold" with an explanatory note. - On each order, a Cartflox box: payment status, operator, reference, "Check now" button, link to the transaction in Cartflox. - Compatible with the classic and block-based checkout, and with HPOS order storage. ## Settings | Section | What you configure | |---|---| | Cartflox connection | Enable, secret key, connection status, test | | Checkout display | Title, description, displayed logos (Orange Money, Wave, MTN MoMo, Moov Money, cards, M-Pesa, Airtel Money, Free Money, T-Money) and their size, order button text, message on the thank-you page | | Payment flow | Prefilling of the name, email and phone number, session label (`{numero}`, `{boutique}`, `{client}`, `{total}` variables), return address after payment, return after abandonment | | Orders | Status while awaiting payment, status once paid, automatic check, automatic cancellation after a delay, Cartflox reference in emails | | Fees and limits | Fixed or percentage fees when the customer chooses Cartflox, minimum and maximum amount, accepted currencies | | Advanced | Automatic webhook registration, detailed log, API address | > **HTTPS required for webhooks** Cartflox only sends webhooks to `https` addresses. On an `http` store, orders are confirmed when the customer returns and by the automatic check. ## Amounts Cartflox expects integer amounts: decimal currencies (USD, EUR, GHS...) are rounded to the unit. For XOF and XAF, nothing changes. The customer's phone number is converted to international format based on their billing country. ## For developers - `cartflox_session_donnees( $donnees, $order )` filter: modify the data sent to Cartflox before the session is created. - `cartflox_montant_frais( $montant, $cart )` filter: compute the payment fees yourself. - `cartflox_paiement_confirme( $order, $data )` action: fired as soon as a payment is confirmed. - Log: WooCommerce, Status, Logs, `cartflox` source. --- # Create a payment session Source: https://cartflox.com/docs/en/paiements/creer-une-session/ > POST /v1/checkout/sessions, parameters, headers, curl, Node.js, PHP and Python examples, response. A session represents a payment intent. You create it server-side, then redirect your customer to the payment page hosted by Cartflox, which handles the operator selection, the Wave redirect, Orange Money OTP codes and card payments. **POST /v1/checkout/sessions** : Create a new payment session ## Parameters | Parameter | Type | Required | Description | |---|---|---|---| | `amount` | number | Yes | Amount, positive integer (5000 for 5,000 XOF) | | `currency` | string | No | ISO 4217 code: XOF (default), XAF, GHS, NGN, KES, TZS, UGX, RWF, ZMW, MWK, CDF, ETB, MZN, ZAR, EGP, MAD, GNF, SLE, LRD, GMD, MGA, MUR, USD, EUR, GBP. CFA francs, GNF, UGX, RWF, CDF and MGA have no minor unit. | | `customer_name` | string | No | Customer name, prefilled on the payment page | | `customer_email` | string | No | Customer email (payment receipt) | | `customer_phone` | string | No | Phone number in international format (+225...), prefilled for Mobile Money | | `description` | string | No | Label shown to the customer | | `success_url` | string | No | Return address after a successful payment | | `cancel_url` | string | No | Return address if the customer gives up | | `metadata` | object | No | Your own data (order number, customer ID...), returned in webhooks | | `merchant_name` | string | No | Name shown at the top of the payment page instead of your workspace name (60 characters max). Useful for platforms collecting payments for several brands. | | `merchant_logo` | string | No | `https` address of a logo shown instead of your workspace logo (PNG, JPG or SVG). | ### Headers | Header | Value | |---|---| | `Authorization` | `Bearer af_live_sec_...` (or `x-api-key`); an `af_test_sec_...` key creates a test session | | `Content-Type` | `application/json` | | `Idempotency-Key` | Optional. A unique string per order: if the same call is replayed, the existing session is returned (HTTP 200 with the `Idempotent-Replayed: true` header) instead of creating a second one. | ## Examples #### curl ```bash curl -X POST https://cartflox.com/api/v1/checkout/sessions \ -H "Authorization: Bearer af_live_sec_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1042" \ -d '{ "amount": 5000, "currency": "XOF", "customer_name": "Awa Koné", "customer_email": "awa@example.com", "customer_phone": "+2250700000000", "description": "Order #1042", "success_url": "https://maboutique.com/merci", "cancel_url": "https://maboutique.com/panier", "metadata": { "order_id": "1042" }, "merchant_name": "My Store", "merchant_logo": "https://maboutique.com/logo.png" }' ``` #### Node.js ```js const res = await fetch("https://cartflox.com/api/v1/checkout/sessions", { method: "POST", headers: { Authorization: `Bearer ${process.env.CARTFLOX_SECRET_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "order-1042", }, body: JSON.stringify({ amount: 5000, currency: "XOF", customer_email: "awa@example.com", customer_phone: "+2250700000000", success_url: "https://maboutique.com/merci", cancel_url: "https://maboutique.com/panier", metadata: { order_id: "1042" }, }), }); const session = await res.json(); // Redirect your customer to the payment page: res.ok ? redirect(session.url) : console.error(session.error); ``` #### PHP ```php $ch = curl_init("https://cartflox.com/api/v1/checkout/sessions"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("CARTFLOX_SECRET_KEY"), "Content-Type: application/json", "Idempotency-Key: order-1042", ], CURLOPT_POSTFIELDS => json_encode([ "amount" => 5000, "currency" => "XOF", "customer_email" => "awa@example.com", "customer_phone" => "+2250700000000", "success_url" => "https://maboutique.com/merci", "cancel_url" => "https://maboutique.com/panier", "metadata" => ["order_id" => "1042"], ]), ]); $session = json_decode(curl_exec($ch), true); header("Location: " . $session["url"]); exit; ``` #### Python ```python r = requests.post( "https://cartflox.com/api/v1/checkout/sessions", headers={ "Authorization": f"Bearer {os.environ['CARTFLOX_SECRET_KEY']}", "Idempotency-Key": "order-1042", }, json={ "amount": 5000, "currency": "XOF", "customer_email": "awa@example.com", "customer_phone": "+2250700000000", "success_url": "https://maboutique.com/merci", "cancel_url": "https://maboutique.com/panier", "metadata": {"order_id": "1042"}, }, timeout=15, ) session = r.json() print(session["url"]) # redirect your customer to this address ``` ## Response ```json title="201 Created" { "id": "cmf3k2p1x0001abcd9e8f7g6h", "object": "checkout.session", "url": "https://checkout.cartflox.com/cmf3k2p1x0001abcd9e8f7g6h", "order_id": "CS-MF3K2A-9X1QZ", "amount": 5000, "currency": "XOF", "status": "pending", "livemode": true, "created": "2026-09-02T10:15:00.000Z" } ``` | Field | Description | |---|---| | `id` | Session identifier, keep it to track the payment | | `url` | Payment page: redirect your customer here | | `order_id` | Human-readable Cartflox reference (`CS-...`) | | `status` | `pending` at creation, see [the statuses](https://cartflox.com/docs/en/paiements/verifier-le-statut/#possible-statuses) | | `livemode` | `true` with a production key, `false` with a test key ([test mode](https://cartflox.com/docs/en/demarrage/mode-test/)) | > **After the payment** Your customer is sent back to `success_url` (or `cancel_url`). Never fulfill an order on the strength of this redirect alone: wait for the `payment.completed` webhook or check the status server-side. --- # Payment links Source: https://cartflox.com/docs/en/paiements/liens-de-paiement/ > Create a payment link through the API and find out whether it has been paid. A payment link is a hosted page with a fixed amount, to share on WhatsApp, by email or on social media. You can create them from [the dashboard](https://cartflox.com/payment-links) or through the API. **POST /v1/payment-links** : Create a payment link | Parameter | Type | Required | Description | |---|---|---|---| | `title` | string | Yes | Title shown to the customer | | `amount` | number | Yes | Fixed amount | | `currency` | string | No | XOF by default | | `description` | string | No | Details of the product or service | ```bash title="curl" curl -X POST https://cartflox.com/api/v1/payment-links \ -H "Authorization: Bearer af_live_sec_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Odoo training", "amount": 25000, "currency": "XOF" }' ``` ```json title="200 OK" { "success": true, "id": "cmf3lz...", "url": "https://cartflox.com/pay/k3j9x2ab", "slug": "k3j9x2ab", "amount": 25000, "currency": "XOF" } ``` **GET /v1/payment-links/{id}/check** : Find out whether the link has been paid ```json title="200 OK" { "success": true, "paid": true, "status": "SUCCESS", "transaction": { "id": "cmf3m1...", "amount": 25000, "customerName": "Awa Koné", "customerEmail": "awa@example.com" } } ``` As long as no payment has succeeded, the response is `{ "success": true, "paid": false, "status": "WAITING" }` (or the status of the latest attempt). The customer's phone number is always requested on the page, for Mobile Money. --- # Payment methods of a session Source: https://cartflox.com/docs/en/paiements/moyens-de-paiement/ > GET /v1/checkout/sessions/{id}/methods, the list of operators available for a session, filtered by country. If you build your own operator selection screen, this endpoint lists what is available for a session, filtered by country. It accepts the public key as well as the secret key. **GET /v1/checkout/sessions/{id}/methods?country=ci** : Operators available for the session | Parameter | Type | Required | Description | |---|---|---|---| | `country` | string | No | ISO alpha-2 country code (ci, sn, bj, ml, bf, tg, cm, gh, ng, ke...) or country name. Without a filter: all active operators. | ```bash title="curl" curl "https://cartflox.com/api/v1/checkout/sessions/cmf3k2p1x0001abcd9e8f7g6h/methods?country=ci" \ -H "Authorization: Bearer af_live_sec_YOUR_KEY" ``` ```json title="200 OK" { "id": "cmf3k2p1x0001abcd9e8f7g6h", "currency": "XOF", "methods": [ { "code": "ORANGE_CIV", "gatewayId": "cm9x...", "name": "Orange Money", "provider": "PayDunya", "country": "Côte d'Ivoire", "type": "MOBILE_MONEY", "flag": "🇨🇮", "logo": "/logos/orange-money.svg" } ] } ``` Cards (UEMOA, international) appear whatever country is requested. Operator codes depend on the gateway that serves them: do not hardcode them, read them from the response. --- # Supported gateways Source: https://cartflox.com/docs/en/paiements/passerelles/ > The 22 aggregators Cartflox can drive with your own keys, country by country, and what your customer sees when paying. Cartflox does not collect payments on your behalf: you connect your own aggregator accounts in [Gateways](https://app.cartflox.com/gateways), and each payment lands directly with them. This page lists what is supported and how your customer pays depending on the gateway. ## Connect a gateway 1. In [Gateways](https://app.cartflox.com/gateways), pick the aggregator's card, then **Connect**. 2. Paste the keys of your aggregator account. The **Verify** button queries the aggregator immediately: a rejected key is never saved. 3. Then enable its payment methods in [Payment methods](https://app.cartflox.com/methods). Some aggregators open **one merchant account per country**: Hub2 and Djamo Business are therefore connected country by country, with that country's credentials on each card ("Hub2 Côte d'Ivoire", "Hub2 Senegal"...). > **Test and production** Test mode is set gateway by gateway, with the aggregator's sandbox keys. Wave Business only provides production keys: no test mode for it. PayTech accepts `test` but still charges a token amount. ## Where the payment happens Unless stated otherwise, your customer **stays on the Cartflox payment page** (or on your own page with [SoftPay](https://cartflox.com/docs/en/integrations/softpay/)): the request is pushed to their phone, they confirm it with their PIN, and the page updates on its own. Three other cases exist: - **Confirmation code**: some operators require a code (Orange Money, Magma OnePay, Paystack Ghana and Kenya). It is entered on the page. - **App link** (Wave Business, Djamo): these providers have no push request, the customer confirms in their app. The page shows an "Open Wave" or "Open Djamo" button (on mobile, it opens the app) and a QR code on desktop, then monitors the payment. - **Hosted page**: a few aggregators and most card payments go through the provider's page, then come back. | Gateway | Countries | Where the payment happens | Outgoing transfers | |---|---|---|---| | PayDunya | Senegal, Côte d'Ivoire, Benin, Togo, Mali, Burkina Faso | On the page (request pushed to the phone) | Yes | | PawaPay | Benin, Burkina Faso, Cameroon, Côte d'Ivoire, DR Congo, Ethiopia, Gabon, Ghana, Kenya, Lesotho, Malawi, Mozambique, Nigeria, Congo, Rwanda, Senegal, Sierra Leone, Tanzania, Uganda, Zambia | On the page (request pushed to the phone) | Yes | | Hub2 (one gateway per country) | Côte d'Ivoire, Senegal, Benin, Mali, Burkina Faso, Togo, Cameroon | On the page (push request, code if needed); Wave: app link | Yes | | Wave Business | Côte d'Ivoire, Senegal, Mali, Burkina Faso | App link: "Open Wave" button and QR code on the page, confirmation in the app | No | | Djamo Business (one gateway per country) | Côte d'Ivoire, Senegal | App link: "Open Djamo" button and QR code on the page, confirmation in the app | No | | PayTech | Senegal, Côte d'Ivoire, Mali, Benin | PayTech page prefilled with the number entered on the page, submitted automatically | No | | Magma OnePay | Côte d'Ivoire, Senegal, Benin, Togo, Mali, Burkina Faso, Cameroon | Mobile Money on the page (confirmation code if the operator requires it); Wave and cards: hosted page | No | | iPay Money | Niger, Benin | Mobile Money on the page (request pushed to the phone); cards: hosted page | No | | FeexPay | Benin, Togo, Côte d'Ivoire, Senegal, Burkina Faso, Congo | Mobile Money on the page (request pushed to the phone); cards: hosted page | Yes | | MonetBill | Cameroon, Senegal, DR Congo, Congo, Uganda | Mobile Money on the page (request pushed to the phone); cards: hosted page | Yes | | FedaPay | Benin, Togo, Côte d'Ivoire, Senegal, Guinea, Mali, Burkina Faso, Niger | On the page (request pushed to the phone) | Yes | | NotchPay | Cameroon, Côte d'Ivoire, Senegal, Benin, Togo, Burkina Faso, Mali, Guinea, Nigeria | On the page (request pushed to the phone) | Yes | | Qosic | Benin, Togo, Côte d'Ivoire | On the page (request pushed to the phone) | No | | Paystack | Nigeria, Ghana, South Africa, Kenya, Côte d'Ivoire, Egypt, Rwanda | Mobile Money Ghana and Kenya on the page (code by SMS); cards: hosted page | Yes | | Flutterwave | Nigeria, Ghana, Kenya, Uganda, Tanzania, Rwanda, South Africa, Zambia, Côte d'Ivoire, Senegal, Cameroon, Burkina Faso, Malawi, Egypt | Provider's hosted page | Yes | | CinetPay | Côte d'Ivoire, Senegal, Mali, Togo, Benin, Burkina Faso, Cameroon, DR Congo, Guinea, Niger | Provider's hosted page | No | | Kkiapay | Benin, Togo, Côte d'Ivoire, Senegal | Provider's hosted page | No | | PayPlus | Benin, Togo, Burkina Faso, Côte d'Ivoire | Provider's hosted page | No | | LengoPay | Guinea, Morocco, Senegal, Côte d'Ivoire | Provider's hosted page | No | | Stripe | Cards, worldwide | Card entered on the page (embedded form) | No | | Coinbase Commerce | Crypto, worldwide | Provider's hosted page | No | | Cryptomus | Crypto, worldwide | Provider's hosted page | No | Outgoing transfers (sending money to a Mobile Money account) use the same gateways: see [Send money](https://cartflox.com/docs/en/transferts/envoyer-de-l-argent/). ## Webhooks to declare with the aggregator Most aggregators receive the callback address with each payment. Three of them require it to be declared in their portal: the gateway's configuration drawer shows it to you, in the form `https://cartflox.com/api/webhooks/`. | Gateway | Where to declare it | Secret to copy into Cartflox | |---|---|---| | Wave Business | Wave Business portal, Developers, Webhooks (`checkout.session.completed` and `checkout.session.payment_failed` events) | The `wave_xx_WHS_...` signing secret provided at creation (optional: without it, each success is reconfirmed with Wave) | | Djamo Business | `charge/events` topic with Djamo Business | The signing secret, if you set one | | iPay Money | Developers menu, Webhooks | The `Secret-Hash` you set there | In all cases, a payment is never credited on the sole basis of an unsigned webhook: Cartflox reconfirms the success with the aggregator before notifying you. --- # Check the status of a session Source: https://cartflox.com/docs/en/paiements/verifier-le-statut/ > GET /v1/checkout/sessions/{id}/status and the list of possible statuses. Check the state of a session at any time, for example when the customer returns to `success_url` or to catch up on a missed webhook. **GET /v1/checkout/sessions/{id}/status** : Current status of the session ```bash title="curl" curl https://cartflox.com/api/v1/checkout/sessions/cmf3k2p1x0001abcd9e8f7g6h/status \ -H "Authorization: Bearer af_live_sec_YOUR_KEY" ``` ```json title="200 OK" { "id": "cmf3k2p1x0001abcd9e8f7g6h", "status": "SUCCESS", "paid": true, "amount": 5000, "currency": "XOF", "order_id": "CS-MF3K2A-9X1QZ", "provider": "PayDunya", "provider_reference": "pd_7f3a9c" } ``` ## Possible statuses | Status | Meaning | |---|---| | `PENDING` | Session created, payment not confirmed yet (customer on the page, OTP pending, redirect in progress...) | | `SUCCESS` | Payment confirmed by the aggregator. The `paid` field is `true`. | | `FAILED` | Payment declined or failed at the operator. The customer can retry on the same session. | | `CANCELLED` | Session cancelled, no longer payable. | | `REFUNDED` | Payment refunded from the dashboard. | > **Trust the server** Only this status and the webhook are authoritative. The redirect to `success_url` can be triggered before the operator's confirmation. --- # Changelog Source: https://cartflox.com/docs/en/reference/changelog/ > Notable changes to the Cartflox platform and API. | Date | Change | |---|---| | September 19, 2026 | Unified test mode: `af_test_...` keys, simulated payment from the page, `livemode` on every response and webhook, isolated test data deleted after 90 days. Outgoing webhooks logged and retried up to 10 times over 72 hours, manual resend (dashboard and API), `X-Afriflow-Event` and `X-Afriflow-Delivery` headers, choice of received events. New `payment.cancelled` event (abandonment or cancellation). OpenAPI specification, Postman collection, Node.js, PHP and Python SDKs. English documentation. Twenty more countries recognised in the phone field of the payment page (Zambia, Malawi, Mozambique, Ethiopia, Morocco, Egypt...) and Benin 10-digit numbers. WooCommerce plugin 4.0. | | September 18, 2026 | Five gateways added: Wave Business, PayTech, Djamo Business, Magma OnePay, iPay Money (22 in total); Hub2 and Djamo are connected country by country. Payment stays on the page: Magma OnePay, Paystack (Ghana, Kenya), MonetBill, FeexPay and iPay Money push the request to the phone without a redirect; Wave and Djamo are confirmed in their app from the page (button and QR code). `merchant_name` and `merchant_logo` per session. SoftPay 1.1: app link, provider instructions, "Powered by Cartflox" mention, and a fix for the Pay button of the ready-made form, which stayed inactive after choosing the operator. Outgoing money transfers through the API and from the dashboard. Email to the merchant when an aggregator rejects their keys. Zero-downtime deployments. | | September 5, 2026 | New documentation, built-in search. Automatic WhatsApp reminders from the Cartflox number (subscription). Administration on admin.cartflox.com. Unified email service. | | September 2, 2026 | WooCommerce plugin v3 (signed webhook, check on return, amount check). The cartflox.js widget works with the public key. SoftPay: stylable or headless native payment. Payment page in the cartflox.com brand style. Status page computed from real activity. Password reset by email. Guided onboarding. | | September 1, 2026 | Merchant identity verification (KYC). End of managed mode: each merchant collects payments with their own aggregator keys, 0 F Cartflox commission. | | July 1, 2026 | Embedded card payment (Stripe) directly on the payment page, without a redirect. | | June 2026 | Security audit: gateway secrets encrypted, incoming webhooks re-verified with the aggregators, rate limits, protection against cross-merchant access. | A question, a need that does not appear here? [Contact us](https://cartflox.com/contact) or check [the service status](https://cartflox.com/status). --- # Error codes Source: https://cartflox.com/docs/en/reference/erreurs/ > The HTTP codes returned by the Cartflox API and their meaning. Errors are reported with the HTTP status code and a human-readable message (`error`). Transfers, webhook resends and test mode add a stable `code` field (`test_mode`, `rate_limited`, `solde_insuffisant`...). | Code | Meaning | |---|---| | `400` | Missing or invalid parameter (`amount` missing, `webhookUrl` not public, incomplete link...) | | `401` | Missing or invalid API key: `Missing API key` or `Invalid API Key` | | `402` | Insufficient balance for a transfer (`solde_insuffisant`) | | `403` | Refused: gateway of another application, action not possible with a test key (`test_mode`: transfers, payment links), or production key of a workspace whose identity is not verified yet (`identity_unverified`) | | `404` | Session, link or gateway not found | | `409` | The session is no longer payable (already cancelled or refunded) | | `429` | Too many requests: honor the `Retry-After` header (seconds) | | `500` | Internal error: retry, then contact us if it persists | | `502` | The aggregator could not initiate the payment: the message contains its response | ```json title="Example" { "error": "amount is required and must be > 0" } ``` The payment page endpoints (`/checkout/initiate`, `/checkout/verify`) respond with `{ "success": false, "message": "..." }`. They are used by the hosted page and do not need to be called directly. --- # Roadmap Source: https://cartflox.com/docs/en/reference/feuille-de-route/ > What is coming to Cartflox, and what does not exist yet. What is not published yet is not documented as if it were. Here is what is coming. | Feature | Status | |---|---| | Official Node.js, PHP and Python SDKs | Available since September 19, 2026, with the OpenAPI specification and a Postman collection: [OpenAPI and SDKs](https://cartflox.com/docs/en/reference/sdks/). | | Refunds through the API | In preparation. Today, a refund is made from the dashboard, on the gateways that support it. | | Outgoing transfers (payouts) through the API | Available since September 18, 2026: [Send money](https://cartflox.com/docs/en/transferts/envoyer-de-l-argent/). | | Cartflox test environment | Available since September 19, 2026: `af_test_...` keys, simulated payments, isolated data. See [Test mode](https://cartflox.com/docs/en/demarrage/mode-test/). | > **Beware of fake packages** The official SDKs are downloaded from [this documentation](https://cartflox.com/docs/en/reference/sdks/). Until they are published on npm, Composer or PyPI, do not install anything on those registries that claims to come from Cartflox. --- # Rate limits Source: https://cartflox.com/docs/en/reference/limites/ > The per-minute request limits of the Cartflox API. Limits apply per IP address and per minute, unless stated otherwise. Beyond them, the response is `429` (with `Retry-After` on session creation, and a `code: "rate_limited"` field on transfers and resends). | Endpoint | Limit | |---|---| | `POST /v1/checkout/sessions` | 60 per minute | | `POST /v1/payment-links` | 60 per minute | | `POST /v1/route` | 120 per minute | | `POST /checkout/initiate` (payment page) | 30 per minute | | `POST /checkout/verify` (payment page) | 60 per minute | | `POST /checkout/sandbox` (test mode) | 30 per minute | | `POST /v1/transfers` | 30 per minute and 200 per day, per workspace | | `POST /v1/webhooks/deliveries/{id}/resend` | 30 per minute, per workspace | Need more for a peak of activity (launch, ticketing)? [Write to us](https://cartflox.com/contact) before the event. --- # OpenAPI and SDKs Source: https://cartflox.com/docs/en/reference/sdks/ > OpenAPI 3.1 specification, Postman collection and official Node.js, PHP and Python SDKs for the Cartflox API, with webhook verification. The whole API is described in an OpenAPI 3.1 specification, from which a Postman collection and three official SDKs are provided: Node.js, PHP and Python. The SDKs cover every endpoint, pass `livemode` through untouched, and ship with webhook signature verification. ## Specification and Postman | File | Address | |---|---| | OpenAPI 3.1 (JSON) | [cartflox.com/docs/openapi.json](https://cartflox.com/docs/openapi.json) | | OpenAPI 3.1 (YAML) | [cartflox.com/docs/openapi.yaml](https://cartflox.com/docs/openapi.yaml) | | Postman collection (v2.1) | [cartflox.com/docs/cartflox.postman_collection.json](https://cartflox.com/docs/cartflox.postman_collection.json) | | Index for AI assistants (llms.txt) | [cartflox.com/llms.txt](https://cartflox.com/llms.txt), [llms-full.txt](https://cartflox.com/llms-full.txt) | In Postman: Import, then paste the collection address. Set the `bearerToken` variable to your secret key (the test one to start with). For another language (Go, Java, C#, Ruby...), generate a client from the specification with `openapi-generator`. ## Install an SDK > **Registry publication** The SDKs are not published on npm, Packagist or PyPI yet: install them from the archives below. Do not install any `cartflox` package from anywhere else. #### Node.js Node.js 18 or newer, no dependency, TypeScript included, ESM and CommonJS. ```bash curl -O https://cartflox.com/docs/sdk/cartflox-node.tgz npm install ./cartflox-node.tgz ``` #### PHP PHP 8.0 or newer, cURL and JSON extensions. The archive installs as a Composer "artifact" repository: ```bash mkdir -p vendor-cartflox && curl -o vendor-cartflox/cartflox-php.zip https://cartflox.com/docs/sdk/cartflox-php.zip composer config repositories.cartflox artifact ./vendor-cartflox composer require cartflox/cartflox ``` #### Python Python 3.9 or newer, standard library only. ```bash pip install https://cartflox.com/docs/sdk/cartflox-python.zip ``` ## Create a session #### Node.js ```ts const cartflox = new Cartflox({ apiKey: process.env.CARTFLOX_SECRET_KEY }); const session = await cartflox.checkout.sessions.create( { amount: 5000, currency: "XOF", customer_email: "awa@example.com", metadata: { order_id: "1042" } }, { idempotencyKey: "order-1042" }, ); // Redirect the customer to session.url, then wait for the payment.completed webhook const status = await cartflox.checkout.sessions.retrieveStatus(session.id); if (status.paid) { /* fulfil */ } ``` #### PHP ```php use Cartflox\Cartflox; $cartflox = new Cartflox(getenv('CARTFLOX_SECRET_KEY')); $session = $cartflox->checkout->sessions->create([ 'amount' => 5000, 'currency' => 'XOF', 'customer_email' => 'awa@example.com', 'metadata' => ['order_id' => '1042'], ], ['idempotencyKey' => 'order-1042']); header('Location: ' . $session['url']); ``` #### Python ```python from cartflox import Cartflox cartflox = Cartflox(os.environ["CARTFLOX_SECRET_KEY"]) session = cartflox.checkout.sessions.create( {"amount": 5000, "currency": "XOF", "customer_email": "awa@example.com", "metadata": {"order_id": "1042"}}, idempotency_key="order-1042", ) status = cartflox.checkout.sessions.retrieve_status(session["id"]) ``` ## Verify a webhook Each SDK verifies the signature (`X-Afriflow-Signature`, `X-Afriflow-Timestamp`, 5-minute tolerance) and returns the decoded event. Pass the **raw** request body, never an already parsed JSON. #### Node.js ```ts app.post("/webhooks/cartflox", express.raw({ type: "application/json" }), (req, res) => { try { const event = Webhooks.constructEvent(req.body, req.get("X-Afriflow-Signature"), req.get("X-Afriflow-Timestamp"), process.env.CARTFLOX_SECRET_KEY); if (event.event === "payment.completed" && event.livemode) { /* fulfil event.data.order_id */ } res.sendStatus(200); } catch (e) { if (e instanceof WebhookSignatureError) return res.sendStatus(400); throw e; } }); ``` #### PHP ```php use Cartflox\Webhook; $event = Webhook::constructEvent( file_get_contents('php://input'), $_SERVER['HTTP_X_AFRIFLOW_SIGNATURE'] ?? '', $_SERVER['HTTP_X_AFRIFLOW_TIMESTAMP'] ?? '', getenv('CARTFLOX_SECRET_KEY') ); if ($event['event'] === 'payment.completed' && $event['livemode']) { /* fulfil */ } http_response_code(200); ``` #### Python ```python from cartflox import Webhook event = Webhook.construct_event( request.get_data(), request.headers.get("X-Afriflow-Signature"), request.headers.get("X-Afriflow-Timestamp"), os.environ["CARTFLOX_SECRET_KEY"], ) if event["event"] == "payment.completed" and event["livemode"]: ... # fulfil ``` ## Test mode in the SDKs Give the client your test secret key (`af_test_sec_...`): nothing else changes. `isTestKey` (Node and PHP) or `is_test_key` (Python) tells you which key the client holds, and `checkout.sessions.simulate(id, issue)` settles a test session without opening the page, for your automated tests. See [Test mode](https://cartflox.com/docs/en/demarrage/mode-test/). ## What the SDKs cover Sessions (create, status, methods, simulate), payment links, transfers (create, retrieve, list, options), webhook deliveries (list, retrieve, resend), webhook configuration, routing, transaction export, and the partner API. Each method returns the full JSON of the response; HTTP errors raise `CartfloxError` (Node), `CartfloxException` (PHP) or `CartfloxError` (Python) with the status, the `code` and the message. --- # Multi-gateway orchestration Source: https://cartflox.com/docs/en/routage/ > Connect several gateways, choose which one collects payments per operator and per country, and let Cartflox fail over when one fails. You can connect several gateways and decide, operator by operator and country by country, which one collects the payment. If it fails, Cartflox automatically fails over to the next one. ## In the dashboard 1. [Gateways](https://cartflox.com/gateways): add each aggregator with its keys, validated when saved. 2. [Payment methods](https://cartflox.com/methods): for each operator (Orange Money Côte d'Ivoire, Wave Senegal, MTN Benin...), choose the gateway that serves it, or let Cartflox choose. 3. The routing engine remembers the success rate of each gateway and favors the ones that succeed the most. ## Through the API **GET /v1/route?country=SN&providers=paydunya,cinetpay¤cy=XOF** : Available operators and gateway ranking for a country ```bash title="curl" curl "https://cartflox.com/api/v1/route?country=SN&providers=paydunya,cinetpay¤cy=XOF" ``` ```json title="200 OK" { "country": "SN", "currency": "XOF", "availableMethods": [ { "code": "wave", "name": "Wave", "providers": ["paydunya", "cinetpay"] } ], "rankedProviders": [ "paydunya", "cinetpay" ] } ``` **POST /v1/route** : Routing decision for a specific payment (API key required) | Parameter | Type | Required | Description | |---|---|---|---| | `country` | string | Yes | ISO alpha-2 country code | | `amount` | number | Yes | Amount | | `availableProviders` | string[] | Yes | Candidate gateways (paydunya, cinetpay, pawapay, fedapay, stripe...) | | `currency` | string | No | Currency | | `paymentMethod` | string | No | Desired operator (wave, orange, mtn, moov, card...) | | `phone` | string | No | Customer's number: the operator is inferred from the prefix if `paymentMethod` is absent | | `preferredProvider` | string | No | Force a gateway | ```json title="200 OK" { "routing": { "provider": "paydunya", "method": "orange", "reason": "best_success_rate" }, "detectedOperator": "orange", "availableMethods": [ "..." ], "rankedProviders": [ "paydunya", "cinetpay" ] } ``` Accepted values for `providers`: `paydunya`, `pawapay`, `flutterwave`, `feexpay`, `paystack`, `cinetpay`, `stripe`, `kkiapay`, `coinbase`, `fedapay`, `notchpay`, `cryptomus`, `qosic`, `monetbill`, `payplus`, `hub2`, `lengopay`, `wave`, `paytech`, `onepay`, `djamo`, `ipay`. --- # Send money Source: https://cartflox.com/docs/en/transferts/envoyer-de-l-argent/ > POST /v1/transfers, send money to a Mobile Money account, countries and operators, statuses, idempotency, transfer.succeeded and transfer.failed webhooks. Transfers move money out: you send an amount to the Mobile Money account of a supplier, an employee or a customer to refund. Cartflox hands the transfer to one of your gateways that supports it, with your own keys. Workspaces managed by nyole (money held by Cartflox) do not have access to transfers for now. > **Open to all workspaces** Transfers use the same gateways as payment collection: as soon as one of your gateways can send money (PawaPay, PayDunya, Hub2, Notch Pay, Monetbil, Flutterwave, Paystack, FedaPay, FeexPay), you can send, with the keys you have already entered. In your dashboard, the Transfers section has two settings: **Gateways** (the ones Cartflox can use, and what they are missing) and **Methods** (the allowed operators, country by country). Cartflox administration can close transfers for a workspace; the API then responds `403` with the code `transferts_inactifs`. **POST /v1/transfers** : Send money to a Mobile Money account ## Parameters | Parameter | Type | Required | Description | |---|---|---|---| | `amount` | number | Yes | Amount received by the recipient, integer for francs (5000 for 5,000 XOF) | | `country` | string | Yes | Recipient's country, ISO 3166-1 alpha-2: CI, SN, BJ, BF, TG, ML, NE, GN, CM, GA, CG, CD, GH, NG, KE, TZ, UG, RW, ZM | | `operator` | string | Yes | Recipient's operator: `orange_money`, `mtn_money`, `moov_money`, `wave`, `free_money`, `tmoney`, `mpesa`, `airtel_money`... Common aliases (`mtn`, `orange`, `MTN_MOMO_CIV`) are accepted. | | `phone` | string | Yes | Recipient's number, as dialed in the country (`0712345678`) or in international format (`+2250712345678`) | | `currency` | string | No | Currency, inferred from the country (XOF, XAF, GHS, KES...). A currency that does not match the country is rejected. | | `recipient_name` | string | No | Recipient's name, for your statements and for the provider | | `description` | string | No | Reason, passed on when the operator shows it to the recipient | | `idempotency_key` | string | No | Same role as the `Idempotency-Key` header below | | `metadata` | object | No | Your own data, stored with the transfer | ### Headers | Header | Value | |---|---| | `Authorization` | `Bearer af_live_sec_...` (or `x-api-key`). The **secret** key only: the widget's public key is rejected, it cannot move money out. | | `Content-Type` | `application/json` | | `Idempotency-Key` | Strongly recommended. A unique string per transfer (invoice number, payroll ID...): if the same call is replayed, the existing transfer is returned (HTTP 200, `Idempotent-Replayed: true` header) instead of sending the money a second time. | ## Examples #### curl ```bash curl -X POST https://cartflox.com/api/v1/transfers \ -H "Authorization: Bearer af_live_sec_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: salary-2026-09-awa" \ -d '{ "amount": 50000, "country": "CI", "operator": "orange_money", "phone": "0712345678", "recipient_name": "Awa Koné", "description": "September salary" }' ``` #### Node.js ```js const res = await fetch("https://cartflox.com/api/v1/transfers", { method: "POST", headers: { Authorization: `Bearer ${process.env.CARTFLOX_SECRET_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "salary-2026-09-awa", }, body: JSON.stringify({ amount: 50000, country: "CI", operator: "orange_money", phone: "0712345678", recipient_name: "Awa Koné", description: "September salary", }), }); const transfert = await res.json(); console.log(transfert.status); // "processing" most of the time, "succeeded" or "failed" ``` #### PHP ```php $ch = curl_init("https://cartflox.com/api/v1/transfers"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("CARTFLOX_SECRET_KEY"), "Content-Type: application/json", "Idempotency-Key: salary-2026-09-awa", ], CURLOPT_POSTFIELDS => json_encode([ "amount" => 50000, "country" => "CI", "operator" => "orange_money", "phone" => "0712345678", "recipient_name" => "Awa Koné", "description" => "September salary", ]), ]); $transfert = json_decode(curl_exec($ch), true); echo $transfert["status"]; ``` #### Python ```python res = requests.post( "https://cartflox.com/api/v1/transfers", headers={ "Authorization": f"Bearer {os.environ['CARTFLOX_SECRET_KEY']}", "Idempotency-Key": "salary-2026-09-awa", }, json={ "amount": 50000, "country": "CI", "operator": "orange_money", "phone": "0712345678", "recipient_name": "Awa Koné", "description": "September salary", }, ) print(res.json()["status"]) ``` ## Response ```json { "id": "cmfx1a2b3c4d5e6f7g8h9i0j", "reference": "ddf9f0bf-f310-4126-83d9-dc8bc92b6e79", "status": "processing", "amount": 50000, "currency": "XOF", "fee": 0, "country": "CI", "operator": "orange_money", "provider_operator": "ORANGE_CIV", "phone": "2250712345678", "recipient_name": "Awa Koné", "description": "September salary", "provider": "PawaPay", "provider_reference": null, "failure": null, "idempotency_key": "salary-2026-09-awa", "created": "2026-09-18T09:12:41.000Z", "completed": null } ``` | Status | Meaning | |---|---| | `pending` | Created, not yet handed to the provider | | `processing` | Handed to the provider, the operator is processing the transfer. This is the most common case right after the call. | | `succeeded` | The money has reached the recipient's account. `completed` carries the date. | | `failed` | Rejected by the operator or the provider. `failure.code` and `failure.message` say why. Nothing has been debited. | `fee` is `0`: Cartflox takes nothing on a transfer, your aggregator applies its own fees to its wallet. ## Track a transfer The status changes after the call. Two ways to track it, to be used together: **GET /v1/transfers/{id}** : One transfer, by Cartflox identifier or by reference **GET /v1/transfers?status=processing&limit=50** : Your latest transfers, filterable by status And the `transfer.succeeded` and `transfer.failed` webhooks, sent to the address configured for your payments, signed the same way, with the full transfer in `data`. See [the events](https://cartflox.com/docs/en/webhooks/evenements/). ## Countries and operators of your workspace The available operators depend on the gateways you have connected. To know them without guessing: **GET /v1/transfers?options=1** : Countries, currencies and operators open to this workspace ```json { "countries": [ { "code": "CI", "name": "Côte d'Ivoire", "currency": "XOF", "operators": [ { "code": "orange_money", "name": "Orange Money", "provider": "PawaPay" }, { "code": "mtn_money", "name": "MTN Mobile Money", "provider": "PawaPay" }, { "code": "moov_money", "name": "Moov Money", "provider": "PawaPay" }, { "code": "wave", "name": "Wave", "provider": "PawaPay" } ] } ] } ``` > **Where the money comes from** The money leaves your aggregator's wallet (PawaPay, PayDunya, Hub2, Notch Pay, Monetbil, Flutterwave, Paystack, FedaPay or FeexPay), which debits it with its own fees and under its own conditions (funded disbursement wallet, allowed IP address for some). ## Errors | HTTP | `code` | Meaning | |---|---|---| | 401 | `unauthorized` | Missing or invalid key, or public key | | 403 | `transferts_inactifs` | Transfers are not open on this workspace | | 403 | `connect_non_disponible` | Workspaces managed by nyole do not have access to transfers | | 400 | `pays_non_desservi` | Country not in the list | | 400 | `operateur_non_desservi` | None of your gateways can send to this operator in this country | | 400 | `methode_desactivee` | You have disabled this operator in your transfer methods | | 400 | `telephone_invalide` | The number is not a possible number for this country | | 400 | `devise_invalide` | The currency does not match the country | | 400 | `montant_invalide` | Zero or negative amount | | 429 | `plafond_journalier` | The workspace's daily number of transfers has been reached | | 429 | `rate_limited` | More than 30 calls per minute | --- # Configure your webhook Source: https://cartflox.com/docs/en/webhooks/configuration/ > Set the receiving URL, from the dashboard or through the API, and understand retries. A webhook is an HTTP POST request that Cartflox sends to your server on every payment status change. It is the reliable way to validate an order. ## From the dashboard In [API and Logs](https://cartflox.com/developer), enter the receiving URL. It must be public and use HTTPS: local addresses and private networks are rejected. ## Through the API **GET /v1/config/webhook** : Read the configured URL **PATCH /v1/config/webhook** : Set or change the address, choose the events | Field | Type | Description | |---|---|---| | `webhookUrl` | string | Public https address. An empty string removes the webhook. | | `mode` | string | `live` (default with a production key) or `test`: which address is being set. See [Test mode](https://cartflox.com/docs/en/demarrage/mode-test/). | | `events` | string[] | The events to receive, among those listed in [Events](https://cartflox.com/docs/en/webhooks/evenements/). Absent or complete: all of them. | ```bash title="curl" curl -X PATCH https://cartflox.com/api/v1/config/webhook \ -H "Authorization: Bearer af_live_sec_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://mystore.com/webhooks/cartflox", "events": ["payment.completed", "payment.failed", "payment.cancelled"] }' ``` ```json title="200 OK" { "success": true, "webhookUrl": "https://mystore.com/webhooks/cartflox", "testWebhookUrl": null, "events": ["payment.completed", "payment.failed", "payment.cancelled"], "available_events": ["payment.completed", "payment.failed", "payment.cancelled", "payment.updated", "transfer.succeeded", "transfer.failed"] } ``` ## Delivery and retries - Timeout: 10 seconds. Respond 2xx immediately and process afterwards. - On failure (network error, non-2xx response, timeout): up to 10 attempts over roughly 72 hours (1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h, 24 h), then a notification in your dashboard. - Every request is logged and can be resent manually: [Log and retries](https://cartflox.com/docs/en/webhooks/livraisons/). - The same event can therefore arrive twice, for example if your server responded too late: make your processing idempotent, relying on the `X-Afriflow-Delivery` header, or on `data.id` and `data.status`. - A missed webhook can also be caught up by checking [the session status](https://cartflox.com/docs/en/paiements/verifier-le-statut/). --- # Events Source: https://cartflox.com/docs/en/webhooks/evenements/ > The events sent by Cartflox and the body of the webhook request. Each request contains an event, the payment data, a timestamp and `livemode`. You choose which events you receive in [API and Logs](https://cartflox.com/developer) or through [the API](https://cartflox.com/docs/en/webhooks/configuration/). | Event | When | |---|---| | `payment.completed` | Payment confirmed (`SUCCESS` status). This is the event to wait for before fulfilling. | | `payment.failed` | Payment failed or declined (`FAILED` status). | | `payment.cancelled` | Payment abandoned by the customer (page left, session expired) or cancelled by the merchant (`CANCELLED` status). Event added on September 19, 2026: webhooks configured before that date only receive it once ticked in API and Logs. | | `payment.updated` | Any other status change: `REFUNDED`, back to `PENDING`. | | `transfer.succeeded` | Outgoing transfer credited to the recipient's account (`data` = the transfer, see [Send money](https://cartflox.com/docs/en/transferts/envoyer-de-l-argent/)). | | `transfer.failed` | Transfer rejected by the operator or the provider, with `data.failure`. | ## Request body ```json title="POST https://maboutique.com/webhooks/cartflox" { "event": "payment.completed", "livemode": true, "data": { "id": "cmf3k2p1x0001abcd9e8f7g6h", "order_id": "CS-MF3K2A-9X1QZ", "status": "SUCCESS", "amount": 5000, "currency": "XOF", "provider": "PayDunya", "provider_reference": "pd_7f3a9c", "customer_name": "Awa Koné", "customer_email": "awa@example.com", "customer_phone": "+2250700000000", "metadata": { "order_id": "1042", "source": "checkout_session" }, "completed_at": "2026-09-02T10:17:42.000Z" }, "timestamp": "2026-09-02T10:17:42.000Z" } ``` | Field | Description | |---|---| | `data.id` | Session identifier, the same one returned at creation | | `data.order_id` | Cartflox reference (`CS-...`) | | `data.status` | `SUCCESS`, `FAILED`, `PENDING`, `CANCELLED` or `REFUNDED` | | `data.provider` | Gateway that processed the payment | | `data.provider_reference` | Reference at the aggregator, useful for reconciliation | | `data.metadata` | Your metadata, enriched with the source (`checkout_session`, `payment_link`...) | | `data.completed_at` | Confirmation date, `null` until the payment succeeds | | `livemode` | `true` in production, `false` for a payment made with a test key ([test mode](https://cartflox.com/docs/en/demarrage/mode-test/)) | --- # Log and retries Source: https://cartflox.com/docs/en/webhooks/livraisons/ > Every webhook sent is logged, retried up to 10 times over 72 hours, and can be resent manually from the dashboard or the API. Every request to your server is a **delivery**: Cartflox keeps what was sent, when, the response received, and retries until your server responds 2xx. ## Automatic retries Your server has 10 seconds to respond. Without a 2xx response, the delivery is retried on this schedule, 10 attempts over roughly 72 hours: | Attempt | Delay after the previous one | |---|---| | 1 | immediate | | 2 | 1 minute | | 3 | 5 minutes | | 4 | 15 minutes | | 5 | 1 hour | | 6 | 3 hours | | 7 | 6 hours | | 8 | 12 hours | | 9 | 24 hours | | 10 | 24 hours | After the last attempt, the delivery is marked `failed` and a notification shows up in your dashboard. The address and the signing key are read again at every attempt: fix your address in [API and Logs](https://cartflox.com/developer) and the next attempt picks it up. Each request carries two headers that help with deduplication: `X-Afriflow-Event` (the event name) and `X-Afriflow-Delivery` (the delivery identifier, identical from one attempt to the next). Idempotent processing keyed on `X-Afriflow-Delivery`, or on `data.id` and `data.status`, protects you from duplicates. ## Resend manually In [API and Logs](https://cartflox.com/developer), the "Webhook deliveries" block lists the last 50 requests with the response received. "Resend" fires immediately, with a fresh signature, to your current address, whatever the state of the delivery. ## Through the API **GET /v1/webhooks/deliveries** : List deliveries (50 by default, 200 at most) **GET /v1/webhooks/deliveries/{id}** : Read a delivery **POST /v1/webhooks/deliveries/{id}/resend** : Resend a delivery List filters: `transaction_id`, `status` (`pending`, `delivered`, `failed`), `event`, `limit`. A test key only sees test deliveries. ```bash title="Deliveries of a transaction" curl "https://cartflox.com/api/v1/webhooks/deliveries?transaction_id=cmf3k2p1x0001abcd9e8f7g6h" \ -H "Authorization: Bearer af_live_sec_YOUR_KEY" ``` ```json title="200 OK" { "object": "list", "data": [ { "id": "cmu8d4e5f0003abc", "object": "webhook.delivery", "event": "payment.completed", "url": "https://mystore.com/webhooks/cartflox", "status": "delivered", "attempts": 2, "max_attempts": 10, "next_attempt_at": null, "last_status_code": 200, "last_error": null, "response_ms": 412, "transaction_id": "cmf3k2p1x0001abcd9e8f7g6h", "transfer_id": null, "livemode": true, "created_at": "2026-09-19T08:00:00.000Z", "delivered_at": "2026-09-19T08:01:03.000Z" } ], "count": 1 } ``` ```bash title="Resend" curl -X POST https://cartflox.com/api/v1/webhooks/deliveries/cmu8d4e5f0003abc/resend \ -H "Authorization: Bearer af_live_sec_YOUR_KEY" ``` The response is the updated delivery. Limit: 30 resends per minute per workspace. | Field | Description | |---|---| | `status` | `pending` (another attempt is scheduled), `delivered` (2xx response received), `failed` (given up after the last attempt) | | `attempts` / `max_attempts` | Attempts made, and the cap | | `next_attempt_at` | Next automatic attempt, `null` otherwise | | `last_status_code` / `last_error` | Your server's last response, or the error met (timeout, network) | | `livemode` | `false` for a test event | --- # Verify the signature Source: https://cartflox.com/docs/en/webhooks/signature/ > Verify the HMAC-SHA256 signature of Cartflox webhooks in Node.js, PHP and Python. Every webhook is signed with your secret key. Verify the signature before processing anything: otherwise, anyone who knows your URL could make you believe a payment happened. ## Headers sent | Header | Content | |---|---| | `X-Afriflow-Timestamp` | Unix timestamp (seconds) of the delivery | | `X-Afriflow-Signature` | `t=,v1=` | ## Algorithm 1. Get the **raw** request body, exactly as received (not the JSON parsed and then re-serialized). 2. Concatenate `.`. 3. Compute the HMAC-SHA256 of that string with your secret key `af_live_sec_...`. 4. Compare in constant time with the `v1` value of the header. Reject if the timestamp is more than 5 minutes old (replay protection). ## Examples #### Node.js ```js title="Express" const app = express(); function verifierSignature(corpsBrut, entetes, secret) { const ts = entetes["x-afriflow-timestamp"]; const sig = (entetes["x-afriflow-signature"] || "") .split(",").find((p) => p.startsWith("v1="))?.slice(3); if (!ts || !sig) return false; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // 5 minutes const attendu = crypto.createHmac("sha256", secret).update(`${ts}.${corpsBrut}`).digest("hex"); return attendu.length === sig.length && crypto.timingSafeEqual(Buffer.from(attendu), Buffer.from(sig)); } // The body must stay RAW: express.raw, not express.json app.post("/webhooks/cartflox", express.raw({ type: "application/json" }), (req, res) => { const brut = req.body.toString("utf8"); if (!verifierSignature(brut, req.headers, process.env.CARTFLOX_SECRET_KEY)) { return res.status(400).send("invalid signature"); } const evt = JSON.parse(brut); if (evt.event === "payment.completed") { // Mark order evt.data.metadata.order_id as paid. // Make this processing idempotent: the same event can arrive twice. } res.sendStatus(200); // respond quickly, process the rest in the background }); ``` #### PHP ```php $brut = file_get_contents("php://input"); $ts = $_SERVER["HTTP_X_AFRIFLOW_TIMESTAMP"] ?? ""; $sig = ""; foreach (explode(",", $_SERVER["HTTP_X_AFRIFLOW_SIGNATURE"] ?? "") as $partie) { if (str_starts_with($partie, "v1=")) $sig = substr($partie, 3); } $attendu = hash_hmac("sha256", $ts . "." . $brut, getenv("CARTFLOX_SECRET_KEY")); if ($ts === "" || $sig === "" || abs(time() - (int) $ts) > 300 || !hash_equals($attendu, $sig)) { http_response_code(400); exit("invalid signature"); } $evt = json_decode($brut, true); if ($evt["event"] === "payment.completed") { // Order paid: $evt["data"]["metadata"]["order_id"] } http_response_code(200); ``` #### Python ```python title="Flask" from flask import Flask, request, abort app = Flask(__name__) SECRET = os.environ["CARTFLOX_SECRET_KEY"].encode() @app.post("/webhooks/cartflox") def webhook(): brut = request.get_data() # raw body, never request.json ts = request.headers.get("X-Afriflow-Timestamp", "") parties = dict(p.split("=", 1) for p in request.headers.get("X-Afriflow-Signature", "").split(",") if "=" in p) attendu = hmac.new(SECRET, f"{ts}.".encode() + brut, hashlib.sha256).hexdigest() if not ts or abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(attendu, parties.get("v1", "")): abort(400) evt = request.get_json(force=True) if evt["event"] == "payment.completed": pass # order paid: evt["data"]["metadata"]["order_id"] return "", 200 ``` > **Never validate without a signature** If your framework does not give access to the raw body, disable its automatic JSON parsing on this route only. ---