Skip to main content
Instead of polling List escrow transactions or Get customer wallet, let Payluk push updates to you. Payluk sends a signed POST request to your registered callback URL on two separate streams: Both streams share the same envelope, the same signature scheme, and the same callback URL. You choose which ones to act on by reading the event field.

Prerequisites

You must set a callback URL for the environment (test or live) in your dashboard business settings. Without one, nothing is sent. Beyond that, each stream has its own condition:
Escrow events: the escrow carries your merchantId (it was created through the API under your merchant account), and its seller is one of your merchant customers (merchant_user).
Transaction events: the transaction belongs to one of your merchant customers. Transactions on your own merchant account do not fire webhooks.
If a condition is not met, no webhook fires and the underlying operation still succeeds as normal.
Payluk picks the callback URL and signing key by the environment the record belongs to: anything created with a sk_live_ key posts to your live callback URL signed with your live secret; sk_test_ records post to your test callback URL signed with your test secret. Transactions that no API key initiated (money arriving in a customer’s reserved account, for example) take the environment of the deployment they happened on.

Escrow events

The event field is escrow.<status> (lower-cased), plus escrow.created when a new escrow is generated. Every status transition emits one event: An escrow.split payload carries a split object alongside the usual fields, so you can reconcile both legs without a second call:
See Escrow lifecycle for how these map to state and status.
Multi-quantity links emit events for each escrow independently; the original link and every cloned escrow. Match on data.id, not the paymentToken. See Multi-quantity escrows.

Transaction events

Every transaction a merchant customer makes emits an event when it settles, whichever flow produced it, along with the two payouts Payluk makes into your own merchant wallet. Nothing is sent while a transaction is still pending: a deposit that has not cleared is a question, not an answer. The event field is payment.<transactionType>.<outcome>: Each of these has a .failed counterpart that fires when the transaction reaches a terminal failure, for example payment.withdrawal.failed when a bank declines a payout and the customer’s wallet is made whole again. A .reversed counterpart exists for the rarer case of a settled transaction being unwound.
Funding an escrow settles a transaction and moves the escrow, so you receive two webhooks: payment.escrow.success and escrow.ongoing. They describe the same money from different angles. Pick the stream that matches what you are building (escrow.* to track deals, payment.* to keep a ledger) rather than acting on both.
Read creditType to tell direction without parsing the event name: credit means the customer’s wallet went up, debit means it went down. A wallet-to-wallet send emits one event for each side.

The envelope

Every webhook body, on both streams, has the same three top-level fields:

Escrow payload

The data object mirrors the escrow you get from the API, including status, state, settlementType, milestones, totalQuantity, environment and merchantId. The shape is the same across all escrow.* events.

Transaction payload

The data object describes the settled transaction. The shape is the same across all payment.* events; only the detail object matching the transaction type is populated, and the rest are null.
The payload does not name the bank or processor that moved the money. Which rail Payluk settles through changes without notice and is not part of this contract, so nothing in your handler should branch on it. Read transactionType for what happened, and the detail objects for how: withdrawalDetails marks a bank payout, blockchainDetails a crypto one.

Verify the signature

Each request carries an x-payluk-signature header: an HMAC-SHA512 of the raw JSON body, keyed with your environment’s secret key. Always verify it before trusting a payload. This is identical on both streams.
Headers
Sign against the exact raw bytes of the request body. If you re-serialize the parsed JSON, key ordering or spacing may differ and the signature won’t match.

Delivery & retries

Acknowledge fast with a 2xx, then do heavy work asynchronously. If you need state that may have arrived out of order, re-fetch: escrows with Verify payment token using the paymentToken, and balances with Get customer wallet using customerId.

Best practices

Treat any request with a missing or mismatched x-payluk-signature as untrusted and reject it with 401. Use a constant-time comparison.
The same event may be delivered more than once (a retry after a slow 2xx). De-duplicate on data.reference for transaction events, and on data.id + event for escrow events, so reprocessing is a no-op.
New event names are added over time, and both streams arrive at the same endpoint. Switch on the event field and return 2xx for anything you do not recognise rather than failing the request.
Events are sent as state changes happen and may arrive out of order under retries. Trust data.status / data.state as the source of truth rather than the sequence of events.
A transaction event tells you money moved, not what the wallet now holds. If you display a balance, read it from Get customer wallet.
Configure distinct callback URLs and verify each with its matching secret (sk_test_ vs sk_live_). The data.environment field tells you which one a payload belongs to.