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.