This page covers payin callbacks. Payout callbacks use the same URL, the same
headers and the same signature scheme — so your verification code is shared — but they carry a
different body and, unlike payins, they are not retried.
When we send a callback
We send one when a payment reaches a settled state — meaning the funds arrived and your balance has already moved:
We do not send callbacks for in-progress states (
created, awaiting_payment) or for expired
and failed. Nothing moved, so there is nothing to fulfil.
expired is not final. If a customer pays a lapsed link late, the payment still settles and you
still get this callback — see expiry is not final.Registering a URL
Set your Callback URL in Developer Tools. This is the same field payout callbacks use — one URL receives both products. Tell them apart by theevent field, which is present on payin callbacks and absent on payout ones.
Requirements:
https://only- Must resolve to a public address. Private and internal addresses are rejected
- Redirects are not followed. A
3xxis a failed delivery, not a hop
Your callback host is validated when you save it and again at delivery time, so a URL that
stops meeting these requirements later simply stops receiving callbacks.
The request we send
All amounts are decimal strings, never numbers. Parse them with a decimal type, not a float.
The payload carries only what you need to fulfil the order. Our fee reaches you as a single total.
Verify the signature
Recompute the HMAC over${timestamp}.${rawBody} and compare in constant time. Reject anything
stale or mismatched.
The scheme is byte-for-byte identical to payout callbacks, so if you already handle those,
reuse that code unchanged — the full snippets in Node and Python are on
Payout callbacks → Verify the signature.
Node
Retries and delivery failures
Unlike payouts, payin callbacks are retried:
Any non-2xx response, a timeout, a redirect, or a transport failure is a failed delivery and is
retried. Respond 2xx as soon as you have durably recorded the event — do your fulfilment work
afterwards, not while we are waiting on the socket.
We stop early, without using the remaining attempts, when retrying cannot change the answer:
A missed callback is not a lost payment. If we cannot reach you, the payment is still settled and
still credited — read it back with the status endpoint, which
is exactly what it is there for.
Idempotency
Because deliveries are retried, your handler will eventually see the same event twice. Assume at least once, never exactly once.- Key on
merchant_reference(your own order id) orgateway_payment_id - Every callback also carries a unique
x-pontis-event-idyou can store to dedupe on - On a duplicate, do nothing and still return 2xx — a non-2xx schedules another retry
Node
Handle all three settled statuses
All three moved your balance, but they do not mean the same thing and must not share a branch.
We credit an underpayment rather than refusing it because the money genuinely arrived and is yours;
refusing the ledger entry would hide a real balance, not prevent one. What to do about the shortfall
is yours to decide — part-ship, hold, ask for the remainder, or refund.
Node
Testing your handler
You can still build and verify most of the handler before going live:- The signature check is pure — feed it a body and a header you construct yourself using your HMAC secret, and assert it accepts a good one and rejects a tampered one. No network needed.
- Idempotency is testable the same way: deliver the same event twice to your own endpoint and assert the second is a no-op.
- The settled statuses —
paid,underpaid,overpaid— can be exercised through sandbox trigger codes ongetPayinStatus, which is enough to prove your branching before a real payment reaches it.
localhost or
private addresses.