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
| Event | When |
|---|---|
transfer.created | A transfer was created from a quote |
transfer.funded | The sender's funds were secured |
transfer.processing | The payout is in flight to the provider |
transfer.completed | The recipient has been paid |
transfer.failed | The transfer failed |
transfer.refunded | Funds 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_idequals theX-Event-IDheader — use it as your idempotency / dedupe key.recipient_idis a pseudonymised identifier; recipient PII is masked, never sent in full.livemodeisfalsefor sandbox/test traffic so you can keep it out of your production analytics.
Headers
| Header | Description |
|---|---|
X-Webhook-Event | The event type, e.g. transfer.completed |
X-Event-ID | Unique delivery id (matches event_id in the body) |
X-Timestamp | Unix seconds when the event was signed |
X-Signature | HMAC-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.
- Node.js
- Python
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));
}
import hmac, hashlib
def verify(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
Respond 2xx once you've stored the event. Non-2xx responses are retried with backoff for up to 24 hours.
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.