Webhooks
Recevez une notification HTTP POST à chaque changement de statut d'une transaction. Le moyen le plus fiable de savoir qu'un paiement a réussi.
Principe
- Votre client paie via un fournisseur (CinetPay, PayPal...).
- Le fournisseur notifie GerminoPay.
- GerminoPay met à jour le statut et crédite votre solde.
- GerminoPay envoie un POST à votre URL webhook avec les détails.
- Votre serveur répond en HTTP 200.
Configuration
Configurez votre URL webhook depuis votre tableau de bord marchand :
👉 Tableau de bord → Clés API → URL Webhook
HTTPS obligatoire en production
Votre URL webhook doit être en HTTPS. Les URL HTTP sont rejetées pour protéger les données clients.
Payload
POST → votre URL
{
"event": "payment.succeeded",
"created_at": "2025-10-01T13:32:15Z",
"data": {
"reference": "GP-20251001-ABC123",
"transaction_id": "txn_9f8e7d6c5b4a",
"fournisseur": "cinetpay",
"montant": 5000,
"devise": "XOF",
"frais": 75,
"montant_net": 4925,
"statut": "succes",
"client_email": "client@example.com",
"metadata": { "commande_id": 1042 },
"paid_at": "2025-10-01T13:32:15Z"
}
}
Événements
| Event | Déclenché quand |
|---|---|
payment.initiated | Un paiement vient d'être créé. |
payment.processing | Le fournisseur traite le paiement. |
payment.succeeded | Paiement confirmé. Solde crédité. |
payment.failed | Paiement échoué. |
payment.refunded | Remboursement effectué. |
payout.processed | Un retrait a été envoyé. |
payout.rejected | Un retrait a été rejeté. |
Signature
Chaque webhook est signé avec votre clé secrète pour vérifier l'authenticité. La signature est dans l'en-tête :
HTTP Headers
POST /webhook/germinopay HTTP/1.1 Host: monsite.com Content-Type: application/json X-GerminoPay-Signature: sha256=9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c X-GerminoPay-Timestamp: 1730000000
Pour vérifier, calculez :
Pseudo-code
expected = "sha256=" + HMAC_SHA256(
key = secret_key,
message = timestamp + "." + raw_body
)
valid = hmac.compare(expected, header_signature)
Exemple de handler
<?php
// webhook.php
$secret = getenv("GERMINOPAY_SECRET");
$rawBody = file_get_contents("php://input");
$timestamp = $_SERVER["HTTP_X_GERMINOPAY_TIMESTAMP"] ?? "";
$signature = $_SERVER["HTTP_X_GERMINOPAY_SIGNATURE"] ?? "";
// 1. Vérifier la signature
$expected = "sha256=" . hash_hmac("sha256", $timestamp . "." . $rawBody, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit("Invalid signature");
}
// 2. Protection anti-replay (5 minutes max)
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(400);
exit("Timestamp expired");
}
// 3. Traiter l'événement
$payload = json_decode($rawBody, true);
$event = $payload["event"];
$data = $payload["data"];
switch ($event) {
case "payment.succeeded":
// Marquer la commande comme payée
$pdo->prepare("UPDATE commandes SET statut = ? WHERE reference = ?")
->execute(["payee", $data["reference"]]);
break;
case "payment.failed":
// Libérer la commande
break;
case "payment.refunded":
// Journaliser le remboursement
break;
}
// 4. Répondre 200 pour acquitter
http_response_code(200);
echo json_encode(["received" => true]);
Retry
Si votre serveur ne répond pas avec HTTP 200 dans les 10 secondes, nous retentons avec un backoff exponentiel :
- Tentative 1 : immédiate
- Tentative 2 : après 30 secondes
- Tentative 3 : après 2 minutes
- Tentative 4 : après 10 minutes
- Tentative 5 : après 1 heure
- Tentative 6 (finale) : après 6 heures
Idempotence
Comme nous pouvons retenter, votre handler doit être idempotent. Utilisez la référence pour détecter les doublons.
Bonnes pratiques
- ✅ Répondez 200 immédiatement, traitez en asynchrone
- ✅ Vérifiez la signature à chaque fois
- ✅ Utilisez le corps brut pour la signature (non parsé)
- ✅ Journalisez tous les webhooks pour audit
- ❌ Ne faites pas confiance à la seule return_url
- ❌ Ne faites pas d'opérations longues dans le handler