Webhooks
Un webhook est une notification que Tchin envoie automatiquement à votre serveur, en temps réel, dès que l'état d'un paiement change. Concrètement, Tchin effectue une requête POST vers votre callback_url (ou vers l'URL webhook configurée sur votre application) pour vous dire, par exemple, « le paiement a1b2c3d4e5f6 est réussi ». C'est le mécanisme central de toute intégration fiable : plutôt que d'interroger l'API en boucle, vous êtes prévenu au bon moment.
Pourquoi le webhook est la source de vérité
Après un paiement, le client est renvoyé sur votre site (voir Encaissement). Ce retour navigateur est pratique pour l'affichage (« Merci pour votre commande »), mais il n'est pas fiable pour valider une vente : le client peut fermer son onglet, perdre sa connexion ou couper le réseau juste avant de revenir. Dans ce cas, le paiement a bien eu lieu mais votre page de retour n'est jamais appelée.
Le webhook, lui, part de serveur à serveur, indépendamment du navigateur du client. Il arrive donc même si le client a disparu. Règle d'or : validez, livrez et créditez une commande uniquement à la réception d'un webhook status = completed authentifié. Ne vous reposez jamais sur le seul retour navigateur.
ℹ️ Le webhook est envoyé pour tous les états finaux : completed, failed et cancelled (et éventuellement pending pour les paiements asynchrones). Vous savez ainsi toujours ce qu'est devenue chaque transaction.
Format de l'envoi
Le webhook est une requête POST de type application/x-www-form-urlencoded (et non du JSON). Tous les champs sont regroupés sous une clé unique data. Côté serveur, vous lisez donc d'abord data, puis chaque sous-champ (ex. data['status'], data['reference']). La plupart des frameworks (Laravel, Express avec urlencoded, Flask) décodent ce format automatiquement.
Exemple de payload (paiement réussi)
POST https://monsite.com/tchin/webhook
Content-Type: application/x-www-form-urlencoded
data[response_code]=00
data[response_text]=Transaction completed
data[status]=completed
data[hash]=7f3b9e2c1a...d84f0b (SHA-512, 128 caractères hexadécimaux)
data[reference]=a1b2c3d4e5f6 (= token du paiement)
data[token]=a1b2c3d4e5f6
data[amount]=5000
data[fee]=250
data[net]=4750
data[currency]=XOF
data[country]=SN
data[method]=wave-senegal
data[method_name]=Wave
data[mode]=live
data[fail_reason]=
data[customer][name]=Awa Diop
data[customer][email]=awa@exemple.com
data[customer][phone]=771234567
data[custom_data]={"order_id":"1024"}
Exemple de payload (paiement échoué)
data[response_code]=1001
data[response_text]=Transaction failed
data[status]=failed
data[hash]=7f3b9e2c1a...d84f0b
data[reference]=a1b2c3d4e5f6
data[amount]=5000
data[currency]=XOF
data[country]=SN
data[method]=wave-senegal
data[method_name]=Wave
data[mode]=live
data[fail_reason]=Solde insuffisant du client
data[customer][name]=Awa Diop
data[customer][phone]=771234567
Champs du webhook
Voici l'ensemble des champs transmis sous la clé data. Selon le statut, certains champs peuvent être vides (par exemple fee et net sur un paiement échoué, ou fail_reason sur un paiement réussi).
Champ (dans data) | Type | Description |
|---|---|---|
response_code | texte | 00 en cas de succès, sinon 1001. Indicateur brut du résultat. |
response_text | texte | Message lisible associé au code de réponse. |
status | texte | État final du paiement : completed, failed, cancelled ou pending. Champ à tester en priorité. |
hash | texte | Empreinte SHA-512 de votre clé secrète. Sert à authentifier l'origine de la notification (voir plus bas). |
reference | texte | Le token du paiement, à rapprocher de votre commande. C'est votre clé d'idempotence. |
token | texte | Token du paiement (identique à reference). |
amount | entier | Montant payé par le client, en FCFA (XOF). |
fee | entier | Commission Tchin retenue sur la transaction. |
net | entier | Montant net crédité sur votre solde : net = amount − fee. |
currency | texte | Devise, toujours XOF (le Cameroun est réglé en XAF, de même valeur faciale). |
country | texte | Code ISO du pays du paiement (ex. SN, CI, TG). |
method | texte | Code de l'opérateur utilisé (ex. wave-senegal, orange-money-senegal). |
method_name | texte | Nom lisible de l'opérateur (ex. Wave, Orange Money). |
mode | texte | live (paiement réel) ou test (webhook émis depuis la sandbox). |
fail_reason | texte | Motif de l'échec ou de l'annulation. Renseigné pour failed / cancelled, vide sinon. |
customer[name] | texte | Nom du payeur. |
customer[email] | texte | Email du payeur (s'il a été fourni). |
customer[phone] | texte | Numéro Mobile Money du payeur. |
custom_data | texte | Données libres que vous aviez éventuellement attachées au paiement (renvoyées telles quelles). |
Vérifier l'origine (le hash)
N'importe qui pouvant deviner votre callback_url pourrait tenter de vous envoyer un faux webhook « paiement réussi ». Pour l'empêcher, chaque webhook Tchin inclut un champ hash : c'est l'empreinte SHA-512 de votre clé secrète (la même que l'en-tête TCHIN-PRIVATE-KEY). Comme cette clé n'est connue que de vous et de Tchin, un tiers ne peut pas produire le bon hash.
La vérification se fait en trois temps :
- Recalculez de votre côté
SHA-512de votre clé secrète. - Comparez le résultat à
data['hash'], avec une comparaison à temps constant (hash_equalsen PHP,hmac.compare_digesten Python,timingSafeEqualen Node). - Si les valeurs diffèrent, ignorez la requête (répondez 401) et n'appliquez aucun effet.
<?php
$expected = hash('sha512', getenv('TCHIN_SECRET_KEY')); // votre clé secrète
if (! hash_equals($expected, $_POST['data']['hash'] ?? '')) {
http_response_code(401);
exit('Signature invalide'); // requête ignorée : ne provient pas de Tchin
}
⚠️ Le hash ne dépend pas du contenu du message : il authentifie l'émetteur, pas chaque champ individuellement. Après l'avoir validé, traitez tout de même les montants avec bon sens (par exemple, comparez amount à celui attendu pour la commande reference).
Recevoir et traiter le webhook
Un bon gestionnaire de webhook fait toujours les mêmes quatre choses : (1) lire data, (2) vérifier le hash, (3) agir de façon idempotente si status = completed, (4) répondre 200. Voici une implémentation complète dans les trois écosystèmes les plus courants.
<?php
// webhook.php — appelé par Tchin sur votre callback_url.
// Les champs arrivent en application/x-www-form-urlencoded, tous sous la clé "data".
$data = $_POST['data'] ?? [];
// 1) AUTHENTIFIER l'origine : data[hash] = SHA-512 de VOTRE clé secrète (TCHIN-PRIVATE-KEY).
$secret = getenv('TCHIN_SECRET_KEY'); // ex: tchin_sk_xxxxxxxx
$expected = hash('sha512', $secret);
if (! isset($data['hash']) || ! hash_equals($expected, $data['hash'])) {
http_response_code(401);
exit('Signature invalide');
}
// 2) AGIR selon le statut
$reference = $data['reference'] ?? null; // = token du paiement (clé d'idempotence)
$status = $data['status'] ?? null;
if ($status === 'completed') {
$net = (int) ($data['net'] ?? 0); // montant réellement crédité = amount - fee
// IDEMPOTENCE : ne traiter la référence qu'une seule fois.
// (Exemple : SELECT ... WHERE reference = ? AND paid = 0)
$commande = trouver_commande_par_reference($reference);
if ($commande && ! $commande['paid']) {
marquer_payee($reference, $net); // marquer PAYÉE + livrer / activer le service
}
} elseif ($status === 'failed' || $status === 'cancelled') {
// Optionnel : consigner l'échec, motif dans $data['fail_reason'].
journaliser_echec($reference, $data['fail_reason'] ?? '');
}
// 3) ACCUSER RÉCEPTION rapidement.
http_response_code(200);
echo 'OK';
// Node.js / Express
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.urlencoded({ extended: true })); // indispensable : form-urlencoded
app.post('/tchin/webhook', (req, res) => {
const d = req.body.data || {};
// 1) Authentifier : hash = SHA-512 de la clé secrète.
const expected = crypto
.createHash('sha512')
.update(process.env.TCHIN_SECRET_KEY)
.digest('hex');
const ok =
typeof d.hash === 'string' &&
d.hash.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(d.hash), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
// 2) Agir selon le statut (idempotent sur d.reference).
if (d.status === 'completed') {
const net = parseInt(d.net, 10); // = amount - fee
const commande = trouverCommande(d.reference);
if (commande && !commande.paid) {
marquerPayee(d.reference, net); // marquer PAYÉE + livrer, une seule fois
}
} else if (d.status === 'failed' || d.status === 'cancelled') {
journaliserEchec(d.reference, d.fail_reason || '');
}
// 3) Accuser réception.
res.sendStatus(200);
});
app.listen(3000);
# Python (Flask)
import os
import hashlib
import hmac
from flask import Flask, request
app = Flask(__name__)
@app.post('/tchin/webhook')
def tchin_webhook():
# Les champs arrivent en form-urlencoded, aplatis : data[status], data[hash]...
data = {
'status': request.form.get('data[status]'),
'hash': request.form.get('data[hash]'),
'reference': request.form.get('data[reference]'),
'net': request.form.get('data[net]'),
'fail_reason': request.form.get('data[fail_reason]'),
}
# 1) Authentifier : hash = SHA-512 de la clé secrète.
expected = hashlib.sha512(os.environ['TCHIN_SECRET_KEY'].encode()).hexdigest()
if not data['hash'] or not hmac.compare_digest(expected, data['hash']):
return 'Signature invalide', 401
# 2) Agir selon le statut (idempotent sur reference).
if data['status'] == 'completed':
net = int(data['net'] or 0) # = amount - fee
commande = trouver_commande(data['reference'])
if commande and not commande['paid']:
marquer_payee(data['reference'], net) # PAYÉE + livrer, une seule fois
elif data['status'] in ('failed', 'cancelled'):
journaliser_echec(data['reference'], data['fail_reason'] or '')
# 3) Accuser réception.
return 'OK', 200
Idempotence
Pour garantir la livraison, Tchin peut réémettre un webhook (par exemple si votre serveur a mis trop de temps à répondre, ou a renvoyé un code différent de 200). Vous pouvez donc recevoir plusieurs fois la notification d'un même paiement. Sans précaution, cela livrerait deux fois la commande ou créditerait deux fois un compte.
La parade est l'idempotence : utilisez data['reference'] (le token du paiement) comme clé unique et n'appliquez l'effet métier qu'une seule fois par référence. En pratique :
- Stockez la
referenceavec un indicateur « déjà traité ». - À chaque webhook, vérifiez d'abord si la référence a déjà été traitée ; si oui, répondez simplement 200 sans rien refaire.
- Idéalement, protégez l'opération par une contrainte d'unicité en base ou une transaction, pour rester correct même en cas d'appels concurrents.
Webhooks de test
En environnement de test (paiement créé avec "env": "test"), Tchin envoie un webhook identique en structure à celui de production, mais avec data['mode'] = "test". Aucun argent réel n'est déplacé et votre solde n'est pas crédité. C'est le moyen idéal de valider tout votre flux (réception, vérification du hash, idempotence, mise à jour de commande) avant de passer en live.
Pendant vos tests, un service comme un tunnel HTTPS local (par ex. un reverse-proxy de développement) vous permet d'exposer votre callback_url sur Internet et d'inspecter en direct le contenu reçu.
Bonnes pratiques
- Vérifiez toujours le
hashavant d'agir. Une requête sans hash valide doit être rejetée. - Répondez 200 rapidement. Si votre traitement est long (email, génération de facture…), accusez d'abord réception puis traitez en tâche de fond, ou déclenchez le travail en asynchrone.
- Restez idempotent sur
reference: un webreçu deux fois ne doit jamais produire deux livraisons. - Exposez une
callback_urlen HTTPS, publiquement accessible, qui accepte lePOSTenx-www-form-urlencoded. - Ne faites pas confiance aux montants à l'aveugle : après validation du hash, contrôlez que
amountcorrespond bien à la commandereference. - Journalisez chaque webhook reçu (statut, référence, mode) pour faciliter le support et le rapprochement comptable.
- En cas d'erreur (réponse ≠ 200), Tchin pourra réémettre : gardez l'opération rejouable sans effet de bord.
- Rappel : le montant net crédité est
net = amount − fee(la commission Tchin varie selon le pays et votre volume).
Et ensuite ?
Une fois vos encaissements confirmés par webhook, consultez votre solde par pays, initiez des retraits (déboursements) vers vos bénéficiaires, ou parcourez la liste des moyens de paiement disponibles. En cas de problème, la page Erreurs détaille tous les codes retournés par l'API.