Guides
Webhooks
Be told when each payment ends: the v3 events signed with HMAC SHA-256, and the current v2 webhook.
A webhook is a request Konnect sends to your server when a payment changes. v3 delivers signed events to the endpoints you register; the v2 webhook stays for integrations that are not migrated yet.
v2 webhook (current)
The webhook field of init-payment gives the URL to call. When the payment ends, Konnect calls it with GET and appends payment_ref:
GET /konnect/webhook?payment_ref=665f1c2e8b3a4d0012ab34cd HTTP/1.1
Host: shop.example.com
- Without
silentWebhook, the buyer's browser is redirected to this URL. - With
silentWebhook: true, Konnect calls it server to server and sends the buyer tosuccessUrlorfailUrl. - The call is not signed and does not carry the status. Treat it as a signal: always read the payment again with
GET /api/v2/payments/{paymentId}and your API key (see step 5 of the quick start). - Answer quickly (under 10 seconds) and idempotently: the same
payment_refcan arrive more than once.
Signed v3 events
Register an endpoint with POST /organisations/{id}/webhooks (url, optional paymentAccountId to scope it to one payment account, optional events to filter): the answer carries its signing secret (whsec_...), shown once. Konnect sends one JSON POST per event:
{
"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"
}
}
}
Events: payment.succeeded, payment.failed, payment.expired, payment.canceled, and attempt.duplicate_success (the amount was collected a second time; data.attempt names the attempt, the refund is your decision). The headers Konnect-Event-Id and Konnect-Event-Type repeat id and type.
A webhookUrl given on POST /payments receives the events of that payment too; it must be on the origin of one of your registered endpoints, whose secret signs it.
Verify the signature
The Konnect-Signature header holds a timestamp and a signature: t=1759311072,v1=<hex>. The signature is an HMAC-SHA256, keyed with your secret, of the string <t>.<raw body>. To accept it:
- Read the raw body, before any JSON decoding (re-encoded JSON does not give the same bytes).
- Compute the HMAC again and compare it in constant time.
- Refuse when
tis more than 5 minutes away from your clock (replay protection). - Skip an event
idyou already processed.
<?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);Responses and retries
- Answer
2xxwithin 10 seconds; do long work after answering. - Any other answer, or a timeout, triggers a retry after 1, 5, 15, 30 then 60 minutes (six deliveries in total). Redirects are never followed. The delivery is then marked failed.
- Every HTTP try is kept: the request as sent, the outcome (
delivered,http_4xx,http_5xx,timeout,connection_refused,dns_error,tls_error,redirect_not_followed,response_too_large,invalid_url_or_blocked,other), the status code, a redacted excerpt of your answer and the next retry. Read them inGET /payments/{reference}/timeline(webhooks.tries); the payment'sresult.merchantConfirmationsays whether your server confirmed (confirmed,pending,failed,not_configured) and why. POST /organisations/{id}/webhooks/deliveries/{deliveryId}/retryreplays a delivery once (failed or delivered). The replay is recorded as a manual try with who asked for it.- Delivery order is not guaranteed: rely on
createdAtand on the status read from the API.
Rotate the secret
Register a second endpoint on the same URL, deploy its secret, then disable the first one (DELETE /organisations/{id}/webhooks/{endpointId}). Accept an event when one of the v1 signatures of the header is valid: the format allows several.