Le format
Un POST en application/x-www-form-urlencoded, avec un champ data (les sous-champs arrivent en data[reference], data[status]…) et deux en-têtes :
| En-tête | Valeur |
|---|---|
Tchin-Signature | v1= suivi de la signature (aussi dans data[signature]) |
Tchin-Timestamp | l’heure de l’envoi, en secondes Unix (aussi dans data[timestamp]) |
Le contenu de data (paiement)
{
"response_code": "00",
"response_text": "Transaction completed",
"reference": "jtkkbphr7azsn3",
"token": "tch_9w2kq…",
"status": "completed",
"amount": 26042,
"fee": 1042,
"net": 25000,
"currency": "XOF",
"country": "TG",
"method": "t-money-togo",
"method_name": "Yas",
"mode": "live",
"customer": { "name": "Koffi", "email": "koffi@example.com", "phone": "+22890112233" },
"timestamp": 1792432544,
"signature": "3f1a…"
}- Paiement :
referenceest le token du paiement ; payé en crypto, un bloccrypto(devise, quantité envoyée, quantité créditée, transaction). - Abonnement : le champ
event(subscription.charged…) et le blocsubscription. - Recevoir :
kindvautpayout_link;recipient,payout_currency. - Vérification d'identité :
kindvautkyceteventkyc.approved…
Vérifier la signature
La signature est un HMAC-SHA256, avec votre clé secrète, de cette chaîne :
timestamp.reference.token.status.amount.net.modeRefusez aussi un webhook trop ancien (plus de 10 minutes) : un message rejoué ne passera pas. Les kits le font pour vous :
Avec un kit
$d = $tchin->verifierWebhook($_POST['data'] ?? []); // null si la signature est fausse ou ancienneconst d = tchin.verifierWebhook(req.body.data); // null si la signature est fausse ou anciennedata = {k[5:-1]: v for k, v in request.form.items() if k.startswith('data[')}
d = tchin.verifier_webhook(data) # None si la signature est fausse ou ancienneboolean ok = tchin.verifierWebhook(data); // data : les champs data[xxx] → xxxok := c.VerifierWebhook(r.PostForm) // après r.ParseForm()Sans kit, dans n'importe quel langage :
$d = $_POST['data'];
$chaine = implode('.', [$d['timestamp'], $d['reference'], $d['token'], $d['status'], $d['amount'], $d['net'], $d['mode']]);
$ok = hash_equals(hash_hmac('sha256', $chaine, $cleSecrete), $d['signature'])
&& abs(time() - (int) $d['timestamp']) <= 600;const crypto = require('crypto');
const d = req.body.data;
const chaine = [d.timestamp, d.reference, d.token, d.status, d.amount, d.net, d.mode].join('.');
const attendue = crypto.createHmac('sha256', cleSecrete).update(chaine).digest('hex');
const ok = attendue === d.signature && Math.abs(Date.now() / 1000 - d.timestamp) <= 600;import hmac, hashlib, time
chaine = '.'.join(str(d.get(k, '')) for k in ['timestamp', 'reference', 'token', 'status', 'amount', 'net', 'mode'])
attendue = hmac.new(cle_secrete.encode(), chaine.encode(), hashlib.sha256).hexdigest()
ok = hmac.compare_digest(attendue, d['signature']) and abs(time.time() - int(d['timestamp'])) <= 600Répondre
- Répondez
200vite (avant 10 secondes) ; faites le travail long ensuite. - Sans réponse
2xx, Tchin réessaie plusieurs fois. - Un même événement peut arriver deux fois : rendez votre traitement idempotent (ne livrez pas deux fois la même
reference). - Votre adresse doit être en HTTPS et publique.
Tester
En mode test, chaque paiement simulé envoie un vrai webhook, avec "mode": "test". La boutique de démonstration de Tchin montre les webhooks reçus et leur signature vérifiée.