Skip to main content
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.
A payin callback is the moment your customer’s money became your balance. Everything else in the flow is a hint: the browser redirect can be lost, the status endpoint is a snapshot you have to ask for. The callback is the fact, and it is pushed to you.
Never fulfil an order on the browser redirect to success_url. A customer can close the tab, lose signal, or pay and never come back. Fulfil here.

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 the event 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 3xx is 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

Reconcile against credited_amount and credited_currency together. requested_currency is what you invoiced in; credited_currency is what you were actually paid in, and the two differ whenever your customer picks an asset other than the one you priced in. credited_amount alone is ambiguous the moment you accept more than one asset — see the three currencies.
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
Sign over the raw bytes you received, not a re-serialized JSON object. Re-serializing changes whitespace and key ordering, and the signature will not match.

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) or gateway_payment_id
  • Every callback also carries a unique x-pontis-event-id you 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.
Never auto-fulfil on underpaid. There is no minimum: a customer who sends 10 USDT against a 100 USDT order produces an underpaid callback and moves your balance by roughly 10 USDT. If that shares a branch with paid, you ship the order for a tenth of the price. Compare credited_amount against requested_amount before you act.
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

Callbacks are not sent in sandbox — for payins or payouts. Sandbox simulates the create and status endpoints without storing anything, so there is no settlement event to notify you of.
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 statusespaid, underpaid, overpaid — can be exercised through sandbox trigger codes on getPayinStatus, which is enough to prove your branching before a real payment reaches it.
While developing, point your callback URL at any publicly reachable HTTPS endpoint — a request-bin service or an HTTPS tunnel in front of your local server both work. We do not accept localhost or private addresses.