Webhooks
Receive an HTTP POST notification whenever a transaction status changes. The most reliable way to know a payment succeeded.
Overview
- Your customer pays via a provider (CinetPay, PayPal...).
- The provider notifies GerminoPay.
- GerminoPay updates the transaction status and credits your balance.
- GerminoPay sends a POST to your webhook URL with the details.
- Your server acknowledges with HTTP 200.
Configuration
Set your webhook URL from your merchant dashboard:
👉 Dashboard → API keys → Webhook URL
HTTPS required in production
Your webhook URL must be HTTPS. HTTP URLs are rejected to protect customer data.
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"
}
}
Events
| Event | Triggered when |
|---|---|
payment.initiated | A payment was just created. |
payment.processing | Provider is processing. |
payment.succeeded | Payment is confirmed. Balance credited. |
payment.failed | Payment failed. |
payment.refunded | Refund completed. |
payout.processed | A payout was sent. |
payout.rejected | A payout was rejected. |
Signature
Every webhook is signed with your secret key so you can verify authenticity. The signature is in the header:
HTTP Headers
POST /webhook/germinopay HTTP/1.1 Host: monsite.com Content-Type: application/json X-GerminoPay-Signature: sha256=9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c X-GerminoPay-Timestamp: 1730000000
To verify, compute:
Pseudo-code
expected = "sha256=" + HMAC_SHA256(
key = secret_key,
message = timestamp + "." + raw_body
)
valid = hmac.compare(expected, header_signature)
Handler example
<?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
If your server doesn't respond with HTTP 200 within 10 seconds, we retry with exponential backoff:
- Attempt 1: immediate
- Attempt 2: after 30 seconds
- Attempt 3: after 2 minutes
- Attempt 4: after 10 minutes
- Attempt 5: after 1 hour
- Attempt 6 (final): after 6 hours
Idempotency
Because we may retry, your handler must be idempotent. Use the reference to detect duplicates.
Best practices
- ✅ Respond 200 immediately, process asynchronously
- ✅ Verify the signature every time
- ✅ Use raw body for signature (not parsed)
- ✅ Log all webhooks for auditing
- ❌ Don't trust the return_url alone
- ❌ Don't do long operations in the handler