PIK pushes a webhook to your endpoint whenever a payout (outgoing transfer) you initiated changes state. Use these events to track the lifecycle of each payout — from upstream pickup to settlement or failure — and to keep your own ledger and notifications in sync.This document covers everything needed to integrate the Payout webhook end-to-end: delivery format, signature verification, retry behaviour, the full set of event types, the state machine, and a JSON sample for every event.
1. How delivery works#
| Item | Value |
|---|
| HTTP method | POST |
| Content type | application/json; charset=utf-8 |
| Request timeout | 10 seconds |
| Success rule | HTTP 2xx is treated as ack; anything else is a failure |
| Retry policy | Up to 5 retries, 5 minutes between attempts |
| Header | Description |
|---|
Content-Type | Always application/json. |
X-Webhook-Event | Event group. For this document the value is always PAYOUT. |
X-Webhook-Event-Type | Concrete event type (see §3 Event types). |
X-Webhook-Signature | Hex-encoded HMAC-SHA256 of the raw request body using your App Secret. Present only when an App Secret is configured for your account. |
Signature verification#
The signature is computed as:X-Webhook-Signature = HEX( HMAC_SHA256( app_secret, raw_request_body ) )
Sign the raw request body bytes (UTF-8), not a re-serialized JSON, and verify in constant time.
Client response requirements#
Reply with HTTP 200 (or any 2xx) as soon as you have persisted the event.
Any non-2xx response, network error, or timeout will be retried.
Keep ack bodies small (e.g. {"received":true}) — we only log the first 1000 chars of the response.
Retry behaviour#
| Attempt | Trigger |
|---|
| 1 | Immediately when PIK processes the source event |
| 2–6 | 5 minutes after the previous failure |
| Final | After 5 retries the task is marked EXHAUSTED and is not sent again |
Idempotency#
Webhooks may be delivered more than once (retries, network blips, replays). Use event_id as the idempotency key in your handler — if you have already processed an event_id, return 2xx and skip the side-effect.For business identity across events for the same payout, use data.payout_id (stable across ready.send, completed, failed, compliance.rejected).
2. Envelope#
Every Payout webhook shares the same outer envelope:{
"version": "V1.6.0",
"event_name": "PAYOUT",
"event_type": "payout.ready.send | payout.completed | payout.failed | payout.compliance.rejected",
"event_id": "<uuid, unique per event delivery>",
"source_id": "<payout_id this event refers to>",
"data": { ... }
}
| Field | Type | Description |
|---|
version | string | Webhook payload schema version. |
event_name | string | Event group. Always PAYOUT. |
event_type | string | Concrete event. See §3. |
event_id | string | Unique ID of this event delivery. Use as idempotency key. |
source_id | string | The business object the event is about. For payouts this is payout_id. |
data | object | Event-specific payload. See §4. |
3. Event types#
event_type | Meaning | Terminal? |
|---|
payout.ready.send | The payout has been accepted and is being dispatched. Funds are committed but not yet settled. | No |
payout.completed | Payout has settled. The beneficiary has received funds and the fee is final. | Yes |
payout.failed | Payout failed (e.g. beneficiary bank rejection). No funds were debited. | Yes |
payout.compliance.rejected | Payout was rejected by compliance. No funds were debited. | Yes |
4. State machine#
Payout created via API
(local status = INIT, funds reserved)
│
▼
┌────────────────────────────┐
│ payout.ready.send │ payout has been picked up for dispatch
│ (local status = PROCESSING) │
└──────────────┬─────────────┘
│
┌──────────┴──────────────────────────────┐
▼ ▼
┌─────────────────────┐ ┌──────────────────────────────────────────┐
│ payout.completed │ │ payout.failed │
│ (status = SUCCESS) │ │ or payout.compliance.rejected │
│ Funds debited. │ │ (status = FAILED) │
│ Fee finalized. │ │ Reserved funds released. No debit. │
└─────────────────────┘ └──────────────────────────────────────────┘
(terminal) (terminal)
How funds and fees are settled#
The payout amount you submitted (e.g. 100) is the gross amount. The fee is deducted from this amount, not on top of it.
Example: gross = 100, fee = 8 → beneficiary receives 92, your account is debited exactly 100.
On payout.ready.send, funds are still reserved (no balance write).
On payout.completed, the reservation is released and the gross amount is debited from your balance in a single move. A separate fee transaction is recorded for visibility but does not debit additional funds.
On payout.failed / payout.compliance.rejected, the reservation is released; no debit, no fee transaction.
5. data field reference#
| Field | Type | Always present? | Description |
|---|
payout_id | string | yes | Unique payout identifier. Stable across all events for this payout. Use to dedupe by payout. |
account_id | string | yes | The sub-account from which the payout is funded. |
status | string | yes | Payout status string, e.g. Pending, Completed, Failed, Rejected. |
currency | string | yes | ISO 4217 currency code of the payout amount. |
amount | string | yes | Gross payout amount as a decimal string. |
fee_amount | string | yes | Final fee deducted from amount, as a decimal string. Final value is on payout.completed. |
fee_currency | string | yes | Currency of fee_amount. |
beneficiary_id | string | optional | Beneficiary the payout is sent to. |
reference | string | optional | Reference string supplied at payout creation. |
fail_reason | string | optional | Present on payout.failed / payout.compliance.rejected. Human-readable failure message. |
create_time | string | yes | ISO-8601 timestamp when the payout was created. |
complete_time | string | optional | ISO-8601 timestamp when the payout settled. null until terminal Completed. |
update_time | string | yes | ISO-8601 timestamp of the latest status update. |
All monetary fields are sent as strings to avoid floating-point precision loss. Parse them with a decimal type (BigDecimal, decimal.Decimal, etc.).
6. Sample payloads#
6.1 payout.ready.send#
{
"version": "V1.6.0",
"event_name": "PAYOUT",
"event_type": "payout.ready.send",
"event_id": "b2cf6e21-2a90-4d68-a4d7-6c9a44210cd1",
"source_id": "7c1d9f1b-9b6e-4a3b-bbf5-3a2f4f4d9e21",
"data": {
"payout_id": "7c1d9f1b-9b6e-4a3b-bbf5-3a2f4f4d9e21",
"account_id": "ac1e31ab-f0fd-4432-91fb-b06ec1b3d7b9",
"beneficiary_id": "be8a17d0-7e8d-4f4a-9b81-1bd5e7b9aa11",
"status": "Pending",
"currency": "USD",
"amount": "100.00",
"fee_currency": "USD",
"fee_amount": "0",
"reference": "INV-20260525-001",
"create_time": "2026-05-25T15:00:00+08:00",
"update_time": "2026-05-25T15:01:30+08:00",
"complete_time": null
}
}
6.2 payout.completed#
{
"version": "V1.6.0",
"event_name": "PAYOUT",
"event_type": "payout.completed",
"event_id": "8e3f9bc4-2dcb-4ef9-9d33-a7d04b7c2cf8",
"source_id": "7c1d9f1b-9b6e-4a3b-bbf5-3a2f4f4d9e21",
"data": {
"payout_id": "7c1d9f1b-9b6e-4a3b-bbf5-3a2f4f4d9e21",
"account_id": "ac1e31ab-f0fd-4432-91fb-b06ec1b3d7b9",
"beneficiary_id": "be8a17d0-7e8d-4f4a-9b81-1bd5e7b9aa11",
"status": "Completed",
"currency": "USD",
"amount": "100.00",
"fee_currency": "USD",
"fee_amount": "5.00",
"reference": "INV-20260525-001",
"create_time": "2026-05-25T15:00:00+08:00",
"complete_time": "2026-05-25T15:12:44+08:00",
"update_time": "2026-05-25T15:12:44+08:00"
}
}
6.3 payout.failed#
{
"version": "V1.6.0",
"event_name": "PAYOUT",
"event_type": "payout.failed",
"event_id": "fc1c8d4e-d3bd-44a6-a4e0-5a98c3bf21d7",
"source_id": "7c1d9f1b-9b6e-4a3b-bbf5-3a2f4f4d9e21",
"data": {
"payout_id": "7c1d9f1b-9b6e-4a3b-bbf5-3a2f4f4d9e21",
"account_id": "ac1e31ab-f0fd-4432-91fb-b06ec1b3d7b9",
"beneficiary_id": "be8a17d0-7e8d-4f4a-9b81-1bd5e7b9aa11",
"status": "Failed",
"currency": "USD",
"amount": "100.00",
"fee_currency": "USD",
"fee_amount": "0",
"reference": "INV-20260525-001",
"fail_reason": "Beneficiary bank rejected the transfer",
"create_time": "2026-05-25T15:00:00+08:00",
"complete_time": null,
"update_time": "2026-05-25T15:18:02+08:00"
}
}
6.4 payout.compliance.rejected#
{
"version": "V1.6.0",
"event_name": "PAYOUT",
"event_type": "payout.compliance.rejected",
"event_id": "27b5b1c4-1ed1-4f37-a3ed-8d23ec8b9f01",
"source_id": "7c1d9f1b-9b6e-4a3b-bbf5-3a2f4f4d9e21",
"data": {
"payout_id": "7c1d9f1b-9b6e-4a3b-bbf5-3a2f4f4d9e21",
"account_id": "ac1e31ab-f0fd-4432-91fb-b06ec1b3d7b9",
"beneficiary_id": "be8a17d0-7e8d-4f4a-9b81-1bd5e7b9aa11",
"status": "Rejected",
"currency": "USD",
"amount": "100.00",
"fee_currency": "USD",
"fee_amount": "0",
"reference": "INV-20260525-001",
"fail_reason": "Compliance rejected",
"create_time": "2026-05-25T15:00:00+08:00",
"complete_time": null,
"update_time": "2026-05-25T15:05:11+08:00"
}
}
Modified at 2026-05-25 08:15:10