PIK pushes a webhook to your endpoint whenever a deposit (incoming fund) on one of your virtual accounts changes state. Use these events to keep your own ledger in sync with PIK and to notify your end users when funds arrive.This document covers everything needed to integrate the Deposit 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 |
Every request carries the following HTTP headers:| Header | Description |
|---|
Content-Type | Always application/json. |
X-Webhook-Event | Event group. For this document the value is always DEPOSIT. |
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. Long-running work should be done asynchronously.
Any non-2xx response, network error, or timeout will be retried.
Responses are read but truncated to the first 1000 chars in our log — keep ack bodies small (e.g. {"received":true}).
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 |
A delivery is considered a success on the first 2xx and is not retried further.Idempotency#
Webhooks may be delivered more than once (retries, network issues, 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 deposit, use data.deposit_id (stable across pending, completed, compliance.rejected).
2. Envelope#
Every Deposit webhook shares the same outer envelope:{
"version": "V1.6.0",
"event_name": "DEPOSIT",
"event_type": "deposit.pending | deposit.completed | deposit.compliance.rejected",
"event_id": "<uuid, unique per event delivery>",
"source_id": "<deposit_id this event refers to>",
"data": { ... }
}
| Field | Type | Description |
|---|
version | string | Webhook payload schema version. |
event_name | string | Event group. Always DEPOSIT. |
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 deposits this is deposit_id. |
data | object | Event-specific payload. See §4. |
3. Event types#
event_type | Meaning | Terminal? |
|---|
deposit.pending | Funds have been received and are awaiting compliance review. | No |
deposit.completed | Funds have cleared and have been credited to the account balance. | Yes |
deposit.compliance.rejected | The deposit was rejected by compliance and will not be credited. | Yes |
4. State machine#
┌──────────────────────────────┐
│ deposit.pending │
│ (status = "Pending") │
└──────────────┬───────────────┘
│
┌─────────────────┴──────────────────┐
▼ ▼
┌──────────────────────────┐ ┌────────────────────────────────┐
│ deposit.completed │ │ deposit.compliance.rejected │
│ (status = "Completed") │ │ (status = "Rejected") │
│ Balance credited │ │ Funds not credited; │
│ │ │ no balance change │
└──────────────────────────┘ └────────────────────────────────┘
(terminal) (terminal)
deposit.pending may be skipped for some deposits — you should handle deposit.completed arriving without a prior pending.
deposit.completed is the only event on which we credit the recipient's balance. Treat it as the source of truth for "money has arrived".
The amount and fee on deposit.completed are the final values. Values on deposit.pending are indicative and may change on completion.
5. data field reference#
| Field | Type | Always present? | Description |
|---|
deposit_id | string | yes | Unique deposit identifier. Stable across all events for this deposit. Use to dedupe by deposit. |
account_id | string | yes | The virtual account that received the funds. |
account_name | string | yes | The legal account holder name on the virtual account. |
direct_id | string | yes | Parent direct account identifier. "0" if not applicable. |
short_reference_id | string | yes | Short human-readable reference shown to senders. |
deposit_currency | string | yes | ISO 4217 currency code of the deposit (e.g. USD). |
deposit_amount | string | yes | Gross deposit amount as a decimal string. Use a decimal/BigDecimal type to parse. |
deposit_fee | string | yes | Fee deducted from deposit_amount (decimal string). |
deposit_status | string | yes | Deposit status: Pending | Completed | Rejected. |
deposit_reference | string | optional | Free-text reference supplied by the sender. |
sender_name | string | optional | Name of the remitting party, if available. |
create_time | string | yes | ISO-8601 timestamp when the deposit was first observed. |
complete_time | string | optional | ISO-8601 timestamp when the deposit completed. null for rejected deposits. |
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 deposit.pending#
{
"version": "V1.6.0",
"event_name": "DEPOSIT",
"event_type": "deposit.pending",
"event_id": "319318dc-934e-4d96-a994-601383e0d8a6",
"source_id": "881147e4-89de-4e0e-afbc-7d19f6c4f14b",
"data": {
"direct_id": "0",
"account_id": "ac1e31ab-f0fd-4432-91fb-b06ec1b3d7b9",
"account_name": "ACME PTE LTD.",
"deposit_id": "881147e4-89de-4e0e-afbc-7d19f6c4f14b",
"short_reference_id": "230907-GwisGvJK",
"deposit_currency": "USD",
"deposit_amount": "100.00",
"deposit_fee": "0",
"deposit_status": "Pending",
"deposit_reference": "",
"sender_name": "John Doe",
"create_time": "2026-05-25T14:19:52+08:00",
"complete_time": "2026-05-25T14:19:52+08:00",
"update_time": "2026-05-25T14:19:52+08:00"
}
}
6.2 deposit.completed#
{
"version": "V1.6.0",
"event_name": "DEPOSIT",
"event_type": "deposit.completed",
"event_id": "f531776b-df59-4d11-84f0-11e7ae3755f0",
"source_id": "881147e4-89de-4e0e-afbc-7d19f6c4f14b",
"data": {
"direct_id": "0",
"account_id": "ac1e31ab-f0fd-4432-91fb-b06ec1b3d7b9",
"account_name": "ACME PTE LTD.",
"deposit_id": "881147e4-89de-4e0e-afbc-7d19f6c4f14b",
"short_reference_id": "230907-GwisGvJK",
"deposit_currency": "USD",
"deposit_amount": "100.00",
"deposit_fee": "5.00",
"deposit_status": "Completed",
"deposit_reference": "",
"sender_name": "John Doe",
"create_time": "2026-05-25T14:19:52+08:00",
"complete_time": "2026-05-25T14:21:10+08:00",
"update_time": "2026-05-25T14:21:10+08:00"
}
}
6.3 deposit.compliance.rejected#
{
"version": "V1.6.0",
"event_name": "DEPOSIT",
"event_type": "deposit.compliance.rejected",
"event_id": "40f6b64c-e77f-43fb-af12-2cb4b35a8b71",
"source_id": "881147e4-89de-4e0e-afbc-7d19f6c4f14b",
"data": {
"direct_id": "0",
"account_id": "ac1e31ab-f0fd-4432-91fb-b06ec1b3d7b9",
"account_name": "ACME PTE LTD.",
"deposit_id": "881147e4-89de-4e0e-afbc-7d19f6c4f14b",
"short_reference_id": "230907-GwisGvJK",
"deposit_currency": "USD",
"deposit_amount": "100.00",
"deposit_fee": "0",
"deposit_status": "Rejected",
"deposit_reference": "",
"create_time": "2026-05-25T14:19:52+08:00",
"complete_time": null,
"update_time": "2026-05-25T14:25:00+08:00"
}
}
Modified at 2026-05-25 08:14:42