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)

Webhook (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é)

Webhook (échec)
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)TypeDescription
response_codetexte00 en cas de succès, sinon 1001. Indicateur brut du résultat.
response_texttexteMessage lisible associé au code de réponse.
statustexteÉtat final du paiement : completed, failed, cancelled ou pending. Champ à tester en priorité.
hashtexteEmpreinte SHA-512 de votre clé secrète. Sert à authentifier l'origine de la notification (voir plus bas).
referencetexteLe token du paiement, à rapprocher de votre commande. C'est votre clé d'idempotence.
tokentexteToken du paiement (identique à reference).
amountentierMontant payé par le client, en FCFA (XOF).
feeentierCommission Tchin retenue sur la transaction.
netentierMontant net crédité sur votre solde : net = amount − fee.
currencytexteDevise, toujours XOF (le Cameroun est réglé en XAF, de même valeur faciale).
countrytexteCode ISO du pays du paiement (ex. SN, CI, TG).
methodtexteCode de l'opérateur utilisé (ex. wave-senegal, orange-money-senegal).
method_nametexteNom lisible de l'opérateur (ex. Wave, Orange Money).
modetextelive (paiement réel) ou test (webhook émis depuis la sandbox).
fail_reasontexteMotif de l'échec ou de l'annulation. Renseigné pour failed / cancelled, vide sinon.
customer[name]texteNom du payeur.
customer[email]texteEmail du payeur (s'il a été fourni).
customer[phone]texteNuméro Mobile Money du payeur.
custom_datatexteDonné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 :

  1. Recalculez de votre côté SHA-512 de votre clé secrète.
  2. Comparez le résultat à data['hash'], avec une comparaison à temps constant (hash_equals en PHP, hmac.compare_digest en Python, timingSafeEqual en Node).
  3. Si les valeurs diffèrent, ignorez la requête (répondez 401) et n'appliquez aucun effet.
PHP — vérification du hash
<?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 reference avec 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 hash avant 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_url en HTTPS, publiquement accessible, qui accepte le POST en x-www-form-urlencoded.
  • Ne faites pas confiance aux montants à l'aveugle : après validation du hash, contrôlez que amount correspond bien à la commande reference.
  • 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.

Base API : https://tchin.tech/api/v1 · Tchin Documentation

Connexion / Inscription

En vous inscrivant, vous acceptez notre politique de confidentialité.

Entrez le code

Code envoyé à .