Skip to main content

Webhooks

Webhooks deliver transfer lifecycle events to your server in real time. Each event is an HTTP POST, signed with HMAC-SHA256 so you can verify it came from Kora.

Events

EventWhen
transfer.createdA transfer was created from a quote
transfer.fundedThe sender's funds were secured
transfer.processingThe payout is in flight to the provider
transfer.completedThe recipient has been paid
transfer.failedThe transfer failed
transfer.refundedFunds were returned to the sender

Payload

Every delivery is a signed envelope. data is a full snapshot of the transfer at that moment, so a late, duplicate, or out-of-order delivery is safe to apply — overwrite your record on each event.

{
"event_type": "transfer.completed",
"event_id": "7f1c9a2e-3b8d-4e10-9c44-2a6b0f5d7e83",
"occurred_at": 1754849992,
"api_version": "2026-08-01",
"livemode": true,
"data": {
"transaction_id": "3c8a1f7e-2b44-4c9a-a1d0-7e5b6f0c2a91",
"reference": "TXN-8ZK3QP",
"status": "completed",
"send_amount": "100.00",
"send_currency": "USD",
"receive_amount": "5600.00",
"receive_currency": "PHP",
"fx_rate": "56.0000",
"fee_total": "1.50",
"recipient_id": "rcp_7f3a9c1e5b2d8046a1c4",
"destination_country": "PH",
"created_at": "2026-08-10T14:10:02Z",
"completed_at": "2026-08-10T14:12:40Z"
}
}

Notes:

  • event_id equals the X-Event-ID header — use it as your idempotency / dedupe key.
  • recipient_id is a pseudonymised identifier; recipient PII is masked, never sent in full.
  • livemode is false for sandbox/test traffic so you can keep it out of your production analytics.

Headers

HeaderDescription
X-Webhook-EventThe event type, e.g. transfer.completed
X-Event-IDUnique delivery id (matches event_id in the body)
X-TimestampUnix seconds when the event was signed
X-SignatureHMAC-SHA256(secret, "{X-Timestamp}.{raw_body}"), hex-encoded

Verifying the signature

Compute the HMAC over "{timestamp}.{raw_request_body}" using your endpoint's signing secret, and compare it to X-Signature in constant time. Always verify against the raw body bytes, before any JSON parsing.

import crypto from 'crypto';

function verify(req, secret) {
const ts = req.header('X-Timestamp');
const sig = req.header('X-Signature');
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${req.rawBody}`) // rawBody = exact bytes received
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Respond 2xx once you've stored the event. Non-2xx responses are retried with backoff for up to 24 hours.

note

Reject deliveries whose X-Timestamp is older than a few minutes to prevent replay, and dedupe on X-Event-ID.

Configuring an endpoint

Register your receiver URL and retrieve its signing secret from the dashboard. Sandbox and production endpoints have separate signing secrets, so your test and live streams stay isolated.