Skip to main content
A charge or payout does not settle synchronously. The webhook is where you learn the real outcome. AptaPay delivers events to your registered callback_url — or callback_url_test for test-mode traffic — as a signed POST.

Event shape

Headers on every delivery:

Verify before you trust

Verify the signature before doing anything with the body. A request that fails verification should get a non-2xx response — do not process it and do not acknowledge it. Use your outbound signing secret here. It is a different value from the secret you sign requests with.
verify-webhook.ts
Verify over the raw bytes. If your framework parses JSON before you get to the body, the re-serialized object will not hash to the same value. In Express, mount express.raw({ type: "application/json" }) on the webhook route specifically.
The outbound signature always signs the literal string "outbound" in the key_id position, not a generation identifier. During a rotation of your outbound secret you cannot tell from the signed bytes which generation signed a given callback, so be prepared to verify against either the previous or the current secret for a short window around a rotation.

Correlate on metadata, never on our reference

reference is AptaPay’s own identifier, formatted APT-{prefix}-{nanoid}. It is globally unique and unguessable, and it deliberately carries none of your structure. You cannot recover an order id from it, and parsing it as though it were yours will fail on every callback. Send what you need at charge time, and read it straight back:
correlate.ts
metadata is stored verbatim on the transaction and echoed on every delivery. It must serialize to at most 4096 bytes — it rides every attempt of every event, so an oversized object is rejected with a 422 at charge time rather than producing undeliverable webhooks. It is null when the charge carried none.

Delivery is at-least-once and unordered

1

Deduplicate on event_id

AptaPay guarantees a stable event_id per logical event. Track processed ids — even a short-TTL cache is enough — so a legitimate retry of an already-processed event is a safe no-op rather than a double-application.
2

Detect out-of-order arrivals with sequence

sequence increases monotonically per transaction. Combined with previous_status, it lets you ignore a stale delivery rather than assuming arrival order.
3

Check the amount, not just the status

Always compare observed_amount_minor and observed_currency against what you expected. A webhook reporting successful for the wrong amount is the underpayment case. AptaPay already guards this at the gateway — a mismatch is frozen for review and never fanned out — but your settlement logic should verify independently rather than trust status alone.

Retry schedule

If your endpoint does not return 2xx, AptaPay retries at 0s, 30s, 2m, 10m, 1h, 6h. After the final attempt the event lands in a dead-letter queue with an operator replay path. Return 2xx as soon as you have durably recorded the event. Do the slow work afterwards — a handler that does settlement inline and times out will be retried, and you will process it twice.

Responding

handler.ts