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.
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
Theevent 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:
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. Theevent 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.The envelope
Every webhook body, on both streams, has the same three top-level fields:Escrow payload
Thedata 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
Thedata 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 anx-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
Delivery & retries
Best practices
Always verify the signature
Always verify the signature
Treat any request with a missing or mismatched
x-payluk-signature as
untrusted and reject it with 401. Use a constant-time comparison.Be idempotent
Be idempotent
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.Ignore events you don't handle
Ignore events you don't handle
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.Don't assume order
Don't assume order
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.Never treat a webhook as the balance
Never treat a webhook as the balance
A transaction event tells you money moved, not what the wallet now holds. If
you display a balance, read it from
Get customer wallet.
Separate test and live endpoints
Separate test and live endpoints
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.