Accepter un paiement, guide complet
Le cycle de vie d'une commande autour d'un paiement : l'enregistrer, le vérifier et la livrer une seule fois.
Appeler l'API est la partie facile. Quatre requêtes suffisent pour obtenir un paiement. Le Démarrage rapide Paiements les présente ligne par ligne. Ce guide traite de l'autre moitié : la commande dans votre propre base de données, qui doit rester correcte quelles que soient les actions du client, du navigateur et du réseau.
Tout ce qui suit répond à une seule règle. Votre commande est livrée exactement une fois, et uniquement après la confirmation des fonds par Wajub.
Les deux enregistrements et leur lien unique
Vous conservez deux enregistrements. Le vôtre est la commande : ce qui a été acheté, par qui et si la livraison a eu lieu. Wajub conserve le paiement : le montant encaissé, l'opérateur utilisé et le résultat.
Une seule valeur relie les deux enregistrements : l'id du paiement. Enregistrez-le sur la commande dès la création du paiement. Vous pourrez ainsi répondre à toutes les questions ultérieures.
Le lien inverse existe aussi. Vous définissez le champ reference sur le paiement, généralement avec votre numéro de commande, et Wajub le renvoie à chaque lecture. Il aide une personne à lire le Dashboard, mais ne possède aucune contrainte d'unicité ni endpoint de recherche. Ne construisez donc aucune logique dessus.
Vous ne pouvez pas récupérer un paiement avec votre propre référence
GET /payments/{id} recherche uniquement l'id Wajub. L'envoi de votre numéro de commande renvoie 404. C'est l'erreur la plus fréquente lors d'une première intégration. Enregistrez donc l'id avant la redirection, pas après.
1. Attribuer un statut à la commande avant tout appel
La commande existe avant le paiement. Créez-la d'abord avec un état qui indique clairement qu'aucun paiement ni aucune livraison n'a eu lieu.
La séparation entre paid et fulfilled dans le schéma rend le reste de ce guide possible. La confirmation des fonds et la livraison des marchandises sont deux événements distincts qui peuvent échouer séparément. Les regrouper dans une seule colonne vous empêche de savoir ce qui a déjà été effectué si un plantage survient pendant la livraison.
Voici la structure de la table. Deux colonnes assurent l'essentiel : payment_id, qui établit le lien avec Wajub, et fulfilled_at, qui empêche une double livraison.
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
reference VARCHAR(64) NOT NULL UNIQUE,
amount NUMERIC(12, 2) NOT NULL,
currency CHAR(3) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'awaiting_payment',
payment_id VARCHAR(64) UNIQUE,
attempts INTEGER NOT NULL DEFAULT 0,
fulfilled_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX orders_payment_id_idx ON orders (payment_id);2. Créer le paiement et enregistrer son identifiant
Passez maintenant à l'appel d'API. Envoyez le montant, la devise, le client et l'adresse vers laquelle le navigateur doit revenir.
https://api.wajub.com/paymentscurl https://api.wajub.com/payments \
-H "Authorization: sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
-H "Idempotency-Key: order-4172" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"currency": "XAF",
"customer": { "email": "amina@example.com" },
"reference": "order-4172",
"callback": "https://shop.example.com/orders/4172/return"
}'L'en-tête Idempotency-Key empêche le double clic d'un client de créer deux paiements. Créez-le à partir d'une valeur stable, comme la référence de la commande, au lieu de laisser le SDK générer une nouvelle valeur aléatoire. Pendant vingt-quatre heures, la même clé renvoie le paiement initial. La même clé associée à un payload différent est refusée.
Enregistrez payment.id, puis redirigez le navigateur vers payment.authorization_url. Si votre processus s'arrête entre ces deux actions, une commande sans payment_id reste récupérable. Une commande payée impossible à relier à un paiement ne l'est pas.
3. Deux canaux vous annoncent le résultat
Après la redirection, le client paie. Deux canaux indépendants vous transmettent le résultat. Ils peuvent arriver dans n'importe quel ordre, seuls ou pas du tout.
| Canal | Moment de réception | Fiable à lui seul |
|---|---|---|
Retour du navigateur vers votre callback | Le client revient sur votre site | Non |
Webhook payment.succeeded | Toujours, avec ou sans navigateur | Oui, après vérification de la signature |
Le retour du navigateur ne constitue pas une preuve. N'importe qui peut ouvrir cette URL manuellement. Un client qui paie puis ferme l'onglet ne l'ouvre jamais. Le webhook se déclenche toujours, y compris lorsque la confirmation de l'opérateur arrive dix minutes plus tard.
Aucun des deux ne contrôle donc la livraison. Ils appellent tous deux la même fonction, qui prend la décision.
4. Écrire une livraison unique et répétable
Tout le reste repose sur cette partie : une seule fonction qui accepte un identifiant de paiement et peut être appelée autant de fois que nécessaire sans risque.
Trois éléments la rendent sûre. Elle récupère le paiement auprès de Wajub au lieu de faire confiance à l'appelant, vérifie le montant en plus du statut et réserve la commande avec une mise à jour conditionnelle avant tout traitement.
# The check the function makes, by hand. Fulfil on this and nothing else.
curl https://api.wajub.com/payments/trx_test_CSUGajfv9xh0XQ5wu2lx \
-H "Authorization: sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…"La réservation constitue la ligne importante. Il s'agit d'un seul UPDATE conditionnel qui réussit uniquement pour le premier appelant. Deux livraisons simultanées deviennent ainsi impossibles sans verrou.
UPDATE orders
SET status = 'paid'
WHERE id = $1
AND fulfilled_at IS NULL
AND status <> 'paid';Une ligne mise à jour signifie que vous pouvez effectuer la livraison. Zéro ligne signifie qu'un autre processus a déjà réservé la commande. La bonne réponse consiste alors à ne rien faire.
Vérifiez le montant, pas seulement le statut
Un paiement de 500 XAF avec le statut succeeded pour une commande de 5 000 XAF reste un paiement réussi. Si votre checkout permet un jour au client de proposer un prix, la comparaison du montant ci-dessus sera votre seule protection contre la livraison de marchandises pour un dixième de leur valeur.
5. Le handler du webhook
En plus d'appeler la fonction que vous venez d'écrire, le handler doit prouver que la requête provient réellement de Wajub. La vérification utilise le corps brut de la requête, la signature et l'horodatage transmis dans les en-têtes.
| En-tête | Contenu |
|---|---|
X-Wajub-Signature | v1= suivi d'un HMAC-SHA256 de l'horodatage et du corps |
X-Wajub-Timestamp | La seconde Unix de signature de l'événement, refusée au-delà d'un écart de 300 secondes |
X-Wajub-Event | Le nom de l'événement, également présent dans le corps |
X-Wajub-Delivery-Id | Valeur stable pendant les nouvelles tentatives de la même livraison, à utiliser pour la déduplication |
Le SDK effectue toute la vérification, y compris la protection contre les répétitions. Transmettez-lui les octets intacts du corps, répondez 200, puis lancez le traitement.
# What Wajub sends you. Every SDK below verifies these three things.
POST /webhooks/wajub HTTP/1.1
X-Wajub-Signature: v1=8f3c…
X-Wajub-Timestamp: 1789012345
X-Wajub-Event: payment.succeeded
X-Wajub-Delivery-Id: whd_7Yh2MpL4tRb3nP8sZcXv
Content-Type: application/jsonRépondez rapidement avec un code 200. Une livraison qui ne reçoit pas de code 2xx en dix secondes est considérée comme échouée. Elle est tentée cinq nouvelles fois après trente secondes, une minute, cinq minutes, dix minutes et une heure. Les tâches lentes doivent être placées dans une file d'attente, pas exécutées dans la requête.
Utilisez le corps brut, pas le corps analysé
La vérification de signature utilise exactement les octets signés par Wajub. Un analyseur de corps JSON ne réordonne rien, mais sérialise à nouveau les données, ce qui suffit à modifier le hash. Configurez express.raw() uniquement sur cette route ou lisez la requête brute avec l'équivalent de votre framework. Consultez Vérification de signature.
6. Le contenu réel de l'événement
Après vérification, l'événement est un objet simple avec une enveloppe fixe. event contient le nom et data l'objet concerné. Pour un paiement, cet objet est le paiement lui-même.
data.id correspond donc à l'identifiant du paiement, précisément la valeur acceptée par fulfillPayment. Deux événements sont importants pour une commande. Les autres doivent seulement être enregistrés dans les logs.
| Événement | Action du worker |
|---|---|
payment.succeeded | fulfillPayment(data.id) |
payment.failed | Marquer la commande comme échouée, enregistrer data.failure_reason et proposer une nouvelle tentative |
payment.expired | Même action, la session a expiré avant la confirmation du client |
payment.cancelled | Même action, le client ou vous-même avez arrêté le paiement |
payment.created, payment.processing | Les enregistrer dans les logs. Ils aident le support, mais ne déclenchent rien |
7. La page de retour
Le client revient vers votre callback. Cette page explique ce qui s'est passé. Elle doit afficher une information exacte plutôt qu'optimiste.
Wajub ajoute ses propres paramètres à cette URL. Leurs noms peuvent prêter à confusion : reference contient l'identifiant du paiement Wajub, trxref la référence envoyée et status la valeur communiquée au navigateur. Ne vous appuyez sur aucun d'eux pour une décision importante.
Interrogez plutôt l'API avec le payment_id enregistré sur la commande.
curl https://api.wajub.com/payments/trx_test_CSUGajfv9xh0XQ5wu2lx \
-H "Authorization: sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…"La branche de réussite appelle le même fulfillPayment. Le premier des deux canaux effectue le travail. Le second trouve une commande déjà réservée et se termine silencieusement. C'est tout l'intérêt d'une implémentation unique.
Un paiement pending n'est pas un échec. Le client n'a simplement pas terminé la confirmation sur son téléphone. Affichez une page qui l'explique et conservez la commande ouverte.
8. Quand aucun résultat n'arrive
Certaines commandes n'atteignent jamais un état final. Le client a ouvert la page puis l'a quittée, ou l'opérateur n'a plus répondu et la session a expiré sans être remarquée.
Vérifiez-les régulièrement. Pour toute commande encore awaiting_payment après une heure, effectuez une requête directe unique.
curl "https://api.wajub.com/payments?status=pending&per_page=100" \
-H "Authorization: sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…"Cette tâche ne remplace pas les webhooks et ne devrait presque jamais trouver de résultat. Elle sert de filet de sécurité. Le jour où un endpoint de webhook est mal configuré, elle empêche le problème de passer inaperçu.
9. Tout tester dans la sandbox
Dans la sandbox, les six derniers chiffres du numéro de téléphone du client déterminent le résultat. Vous pouvez donc parcourir toutes les branches ci-dessus sans opérateur. Le préfixe sélectionne le pays et l'opérateur, tandis que le suffixe détermine le résultat.
| Suffixe | Résultat | Vérification dans votre code |
|---|---|---|
000000 | Réussite | La commande atteint fulfilled exactement une fois |
000001 | Fonds insuffisants | La commande atteint payment_failed et le client connaît la raison |
000002 | Refus de l'opérateur | Même parcours, autre raison |
000003 | Délai de l'opérateur dépassé | La page d'attente reste affichée, puis la vérification résout la commande |
000004 | Le client refuse l'invite | La commande reste récupérable, elle n'est pas supprimée |
Le préfixe de MTN Cameroun est +23767. Le numéro de test +237670000000 réussit donc, tandis que +237670000001 produit une erreur de fonds insuffisants. Tous les préfixes de pays et d'opérateurs figurent dans Scénarios de test.
Relancez un webhook au lieu de payer à nouveau
Après la réussite d'un paiement de sandbox, renvoyez ses événements depuis Konsole autant de fois que nécessaire. C'est le moyen le plus rapide de prouver l'idempotence réelle de votre handler, car le même événement est volontairement livré deux fois.
Pages associées
- Démarrage rapide PaiementsLes quatre appels d'API sur lesquels repose ce guide.
- Cycle de vie d'un paiementChaque statut possible d'un paiement et les transitions entre eux.
- Checkout Mobile MoneyLe déroulement sur le téléphone du client et les causes d'échec.
- Catalogue des événementsLe catalogue complet, les nouvelles tentatives et la vérification de signature.
- IdempotenceLes clés qui assurent réellement la déduplication et celles qui ne le font pas.
- Gérer les remboursementsLe parcours inverse après le paiement d'une commande.