Aller plus loin
Webhooks & signature
Un paiement mobile money n'est jamais instantané : vous envoyez la demande, le client valide sur son téléphone. C'est le webhook qui vous dit ce qui s'est réellement passé. C'est la seule source à laquelle vous devez faire confiance pour livrer une commande.
Ne livrez jamais sur la seule réponse d'un appel API. Cette réponse dit « la demande est
partie », pas « l'argent est arrivé ». Attendez status: completed dans un webhook
signé, ou vérifiez le statut vous-même.
À quoi ressemble un appel
Nous envoyons un POST en application/x-www-form-urlencoded vers votre
callback_url, avec les données sous la clé data. La signature est présente
deux fois — en en-tête et dans le corps — utilisez celle qui vous arrange.
POST /votre-webhook HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Tchin-Timestamp: 1786000000
Tchin-Signature: v1=b7ff5b2a4992bfb4855ae788d1c0a3f9e6c24b81f0d7a5e39c4b2168ad0f7e52
data[reference]=8f2ad91c&data[token]=inv_9d3f…&data[status]=completed
&data[amount]=18500&data[fee]=740&data[net]=17760&data[mode]=live
&data[timestamp]=1786000000&data[signature]=b7ff5b2a…
Vérifier la signature
Votre URL de webhook est publique : n'importe qui peut vous envoyer un faux « paiement réussi ». La signature est ce qui distingue un appel venant de nous d'un appel venant d'un inconnu. Elle est calculée avec votre clé privée, que vous seul et nous connaissons.
La chaîne signée est la concaténation, séparée par des points, de sept valeurs dans cet ordre exact :
timestamp . reference . token . status . amount . net . mode
Puis : HMAC-SHA256(chaîne, votre_clé_privée), en hexadécimal minuscule.
Modifier un seul de ces champs — le montant, le statut, la référence — invalide la signature.
En PHP
<?php
// Vérification d'un webhook Tchin — à faire AVANT toute écriture en base.
$secret = getenv('TCHIN_SECRET_KEY'); // votre clé privée, jamais dans le code
$d = $_POST['data'] ?? [];
// 1. L'appel est-il récent ? (on refuse au-delà de 5 minutes)
$ts = (int) ($_SERVER['HTTP_TCHIN_TIMESTAMP'] ?? $d['timestamp'] ?? 0);
if (abs(time() - $ts) > 300) {
http_response_code(400);
exit('horodatage hors fenêtre');
}
// 2. La signature correspond-elle au contenu reçu ?
$chaine = implode('.', [
$ts,
$d['reference'] ?? '',
$d['token'] ?? '',
$d['status'] ?? '',
$d['amount'] ?? '',
$d['net'] ?? '',
$d['mode'] ?? '',
]);
$attendue = hash_hmac('sha256', $chaine, $secret);
// hash_equals : comparaison à temps constant, indispensable ici
$recue = $d['signature'] ?? '';
if (! hash_equals($attendue, $recue)) {
http_response_code(403);
exit('signature invalide');
}
// 3. Seulement maintenant, on traite — et une seule fois par token.
if (($d['status'] ?? '') === 'completed' && ! commandeDejaPayee($d['token'])) {
marquerPayee($d['token'], (int) $d['net']);
}
http_response_code(200);
echo 'ok';
En Node.js
// Express — vérification d'un webhook Tchin
const crypto = require('crypto');
app.post('/webhook/tchin', express.urlencoded({ extended: true }), (req, res) => {
const d = req.body.data || {};
const secret = process.env.TCHIN_SECRET_KEY;
const ts = Number(req.get('Tchin-Timestamp') || d.timestamp || 0);
if (Math.abs(Date.now() / 1000 - ts) > 300) return res.status(400).end('horodatage hors fenêtre');
const chaine = [ts, d.reference, d.token, d.status, d.amount, d.net, d.mode].join('.');
const attendue = crypto.createHmac('sha256', secret).update(chaine).digest('hex');
const a = Buffer.from(attendue), b = Buffer.from(String(d.signature || ''));
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(403).end('signature invalide');
if (d.status === 'completed') livrerCommande(d.token, Number(d.net));
res.status(200).end('ok');
});
Trois réflexes. Comparez la signature avec une fonction à temps constant
(hash_equals, timingSafeEqual) — un == ordinaire laisse
fuir l'information. Refusez les horodatages de plus de 5 minutes, sinon un appel intercepté
peut être rejoué. Et traitez chaque token une seule fois : un webhook peut arriver
en double, c'est normal.
Le champ hash
Les anciennes intégrations lisaient un champ hash. Il est toujours envoyé, mais
il ne prouve rien : c'est une valeur constante, identique sur tous vos webhooks, donc
reproductible par quiconque en a vu un seul. Ne l'utilisez plus comme preuve d'authenticité —
basculez sur signature.
Ce que nous envoyons
| Champ | Contenu |
|---|---|
status | completed · pending · failed · cancelled |
reference | Le token du lien ou de l'abonnement concerné. |
token | Identifiant unique de la transaction. Votre clé d'idempotence. |
amount · fee · net | Payé par le client · frais Tchin · crédité sur votre solde. |
country · method | Pays et opérateur utilisés. |
customer | Nom, email et téléphone, quand ils sont connus. |
mode | live ou test. Ne livrez rien sur du test. |
timestamp · signature | Ce qui permet la vérification ci-dessus. |
Les abonnements ajoutent un champ event et un objet subscription —
voir la page Abonnements.
Si le webhook n'arrive pas
Un webhook peut se perdre : votre serveur redémarre, le réseau coupe, votre pare-feu bloque. Nous ne vous laissons pas seul face à ça.
De notre côté : une vérification toutes les 5 minutes
Un service interroge PayDunya toutes les cinq minutes sur toutes les transactions restées en attente, met leur statut à jour, crédite ce qui doit l'être et vous renvoie le webhook. Vous n'avez rien à demander : si un paiement a réellement abouti, vous finirez par être prévenu.
Un panier jamais payé — le client a ouvert la page et n'a rien validé — est clôturé au bout de
deux heures et passe en cancelled.
De votre côté : interrogez le statut
C'est la ceinture en plus des bretelles, et c'est recommandé : quand un client revient sur votre site après un paiement, vérifiez vous-même plutôt que de faire confiance à l'URL de retour.
curl -H "TCHIN-PUBLIC-KEY: tchin_pk_xxxxxxxx" \
-H "TCHIN-PRIVATE-KEY: tchin_sk_xxxxxxxx" \
https://tchin.tech/api/v1/payments/inv_9d3f7a21c4/status
/api/v1/payments/REMPLACEZ_PAR_VOTRE_TOKEN/status
Essayer
Utilisez vos clés de test : en mode test aucun argent ne circule. Vos clés restent dans ce navigateur — elles ne sont ni enregistrées ni transmises à un tiers. Dans votre intégration réelle, les clés doivent rester sur votre serveur, jamais dans une page.
Ce que nous attendons de votre endpoint
- En HTTPS et joignable publiquement. Nous refusons les adresses internes et le HTTP simple.
- Répondez 200 rapidement. Faites le travail lourd après avoir répondu.
- Supportez les doublons : même token deux fois = un seul traitement.
- Ne vous fiez pas à l'ordre d'arrivée : un
pendingpeut suivre uncompleted.
Tester sans argent
En mode test, créez un paiement puis réglez-le avec un compte PayDunya de test :
vous recevez les mêmes webhooks, avec la même signature, et GET /payments/{token}/status
suit le paiement exactement comme en réel. Simplement, aucun solde n'est crédité. Le champ
mode vaut test — votre code doit s'en servir pour ne rien livrer pour de vrai.
https://tchin.tech/api/v1
Retour au sommaire