Guides
Webhooks
Être prévenu à la fin de chaque paiement : les événements v3 signés en HMAC SHA-256, et le webhook v2 actuel.
Un webhook est une requête que Konnect envoie à votre serveur quand un paiement change. La v3 livre des événements signés aux URL que vous enregistrez ; le webhook v2 reste pour les intégrations pas encore migrées.
Webhook v2 (actuel)
Le champ webhook de init-payment donne l'URL à appeler. À la fin du paiement, Konnect l'appelle en GET en ajoutant payment_ref :
GET /konnect/webhook?payment_ref=665f1c2e8b3a4d0012ab34cd HTTP/1.1
Host: shop.example.com
- Sans
silentWebhook, c'est le navigateur de l'acheteur qui est redirigé vers cette URL. - Avec
silentWebhook: true, Konnect l'appelle de serveur à serveur et renvoie l'acheteur verssuccessUrloufailUrl. - L'appel n'est pas signé et n'indique pas le statut. Traitez-le comme un signal : relisez toujours le paiement avec
GET /api/v2/payments/{paymentId}et votre clé API (voir l'étape 5 du démarrage rapide). - Répondez vite (moins de 10 secondes) et de façon idempotente : le même
payment_refpeut arriver plusieurs fois.
Événements v3 signés
Enregistrez une URL avec POST /organisations/{id}/webhooks (url, paymentAccountId facultatif pour la limiter à un compte de paiement, events facultatif pour filtrer) : la réponse contient son secret de signature (whsec_...), affiché une seule fois. Konnect envoie un POST JSON par événement :
{
"id": "0f7a1c2e-6b1d-4c7e-9a3f-2d4e5f6a7b8c",
"type": "payment.succeeded",
"createdAt": "2026-10-03T09:31:12.000Z",
"data": {
"payment": {
"id": "1b2c3d4e-5f60-4718-8293-a4b5c6d7e8f9",
"reference": "665f1c2e8b3a4d0012ab34cd",
"status": "succeeded",
"amount": 120000,
"currency": "TND",
"orderId": "order-1001",
"provider": "clictopay",
"paymentAccountId": "9c8b7a6f-5e4d-4c3b-8a29-18f7e6d5c4b3"
}
}
}
Événements : payment.succeeded, payment.failed, payment.expired, payment.canceled, et attempt.duplicate_success (le montant a été encaissé une seconde fois ; data.attempt désigne la tentative, le remboursement est votre décision). Les en-têtes Konnect-Event-Id et Konnect-Event-Type répètent id et type.
Une webhookUrl donnée sur POST /payments reçoit aussi les événements de ce paiement ; elle doit être sur l'origine d'une de vos URL enregistrées, dont le secret la signe.
Vérifier la signature
L'en-tête Konnect-Signature contient un horodatage et une signature : t=1759311072,v1=<hex>. La signature est un HMAC-SHA256, avec votre secret, de la chaîne <t>.<corps brut>. Pour l'accepter :
- Lisez le corps brut, avant tout décodage JSON (un JSON ré-encodé ne donne pas les mêmes octets).
- Recalculez le HMAC et comparez-le en temps constant.
- Refusez si
ta plus de 5 minutes d'écart avec votre horloge (protection contre le rejeu). - Ignorez un
idd'événement déjà traité.
<?php
function konnect_verify_webhook(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
$parts = [];
foreach (explode(',', $header) as $item) {
[$key, $value] = array_pad(explode('=', trim($item), 2), 2, '');
$parts[$key] = $value;
}
if (!isset($parts['t'], $parts['v1']) || !ctype_digit($parts['t'])) {
return false;
}
if (abs(time() - (int) $parts['t']) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_KONNECT_SIGNATURE'] ?? '';
if (!konnect_verify_webhook($rawBody, $header, getenv('KONNECT_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true);
// Skip $event['id'] if already processed, then handle $event['type'].
http_response_code(200);Réponses et nouvelles tentatives
- Répondez
2xxen moins de 10 secondes ; faites le travail long après avoir répondu. - Toute autre réponse, ou un délai dépassé, déclenche une nouvelle tentative après 1, 5, 15, 30 puis 60 minutes (six envois au total). Les redirections ne sont jamais suivies. L'envoi est ensuite marqué en échec.
- Chaque appel HTTP est conservé : la requête telle qu'envoyée, le résultat (
delivered,http_4xx,http_5xx,timeout,connection_refused,dns_error,tls_error,redirect_not_followed,response_too_large,invalid_url_or_blocked,other), le code HTTP, un extrait masqué de votre réponse et la prochaine tentative. Lisez-les dansGET /payments/{reference}/timeline(webhooks.tries) ;result.merchantConfirmationdu paiement indique si votre serveur a confirmé (confirmed,pending,failed,not_configured) et pourquoi. POST /organisations/{id}/webhooks/deliveries/{deliveryId}/retryrejoue un envoi une fois (en échec ou livré). Le rejeu est enregistré comme un essai manuel, avec la personne qui l'a demandé.- L'ordre d'arrivée n'est pas garanti : fiez-vous à
createdAtet au statut relu par l'API.
Changer de secret
Enregistrez une seconde URL identique, déployez son secret, puis désactivez la première (DELETE /organisations/{id}/webhooks/{endpointId}). Acceptez un événement si l'une des signatures v1 de l'en-tête est valide : le format en autorise plusieurs.