Skip to content

Development environment: content is being written, nothing points to production.

Documentation contents

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 to successUrl or failUrl.
  • 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_ref can 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:

  1. Read the raw body, before any JSON decoding (re-encoded JSON does not give the same bytes).
  2. Compute the HMAC again and compare it in constant time.
  3. Refuse when t is more than 5 minutes away from your clock (replay protection).
  4. Skip an event id you 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 2xx within 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 in GET /payments/{reference}/timeline (webhooks.tries); the payment's result.merchantConfirmation says whether your server confirmed (confirmed, pending, failed, not_configured) and why.
  • POST /organisations/{id}/webhooks/deliveries/{deliveryId}/retry replays 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 createdAt and 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.