Webhooks
Apa POSTs a signed JSON event to your endpoint whenever a payment changes state, so you can reconcile orders without polling. Verify the signature, return any 2xx fast, and let retries handle the rest.
Registering endpoints
Register the URLs that should receive events — from the dashboard, or over the API with POST /v1/webhook-endpoints (and GET / DELETE to list and remove them; see the API reference). Each endpoint returns a signing secret — store it to verify deliveries.
POST /v1/webhook-endpoints
{
"url": "https://store.example/api/apa/webhook",
"events": ["payment.paid", "payment.failed", "payment.refund_required"]
}
201 Created
{
"data": { "id": "we_123", "secret": "whsec_9aF2k3Lm…", "status": "active" },
"request_id": "req_2bN7vK9m"
}Event types
Every event carries a type from the list below. Subscribe an endpoint to the events you care about from the API or the dashboard; most integrations only need payment.paid.
Event payload
Each delivery is a JSON envelope: a unique event id, the type, a created timestamp, and a data object holding the full payment. The order_id and any metadata you set on the session are echoed back so you can match the event to your order, alongside the payout_wallet_id, settled amount and on-chain tx_hash.
{
"id": "evt_3aF2k9",
"type": "payment.paid",
"created": "2026-06-26T14:21:08Z",
"data": {
"id": "pay_8fK2mQ",
"order_id": "ord_1042",
"reference": "apa_3aF2k9Lm7Qp1",
"payout_wallet_id": "pp_8dK3p91A",
"payment_link_id": null,
"session": "cs_123",
"status": "paid",
"route": "routed",
"amount": "240.00",
"currency": "USD",
"pay_asset": "ETH",
"pay_network": "ethereum",
"receive_asset": "USDC",
"receive_network": "solana",
"receive_address": "8dK3…p91A",
"expected_output": "236.40 USDC",
"net_settlement": "236.40",
"actual_output": "236.40 USDC",
"apa_fee": "3.60",
"tx_hash": "0x9a2f…f70",
"failure_reason": null,
"metadata": { "customer_id": "cus_789" }
}
}Verifying signatures
Every request includes an Apa-Signature header of the form t=<timestamp>,v1=<hex>, where v1 is an HMAC-SHA256 over <timestamp>.<raw body>keyed with your endpoint's signing secret (whsec_…) and encoded as hex. Recompute it, compare in constant time, and (optionally) reject stale timestamps before trusting an event.
import crypto from "node:crypto";
// secret is the endpoint's signing secret, e.g. whsec_9aF2k3Lm…
// Apa-Signature: t=<timestamp>,v1=<hex>
// v1 = HMAC_SHA256(secret, `${t}.${rawBody}`)
export function verifyApaSignature(header, rawBody, secret) {
const parts = Object.fromEntries(
(header ?? "").split(",").map((p) => p.split("="))
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`) // "<timestamp>.<raw body>", before JSON.parse
.digest("hex");
// Constant-time compare avoids leaking the signature
const valid =
Boolean(parts.v1) &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
// Optional replay protection: reject if the timestamp is too old.
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
return valid && fresh;
}
// Express — express.raw() keeps req.body as the untouched bytes
app.post("/api/apa/webhook", express.raw({ type: "*/*" }), (req, res) => {
const ok = verifyApaSignature(
req.headers["apa-signature"],
req.body,
process.env.APA_WEBHOOK_SECRET
);
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body);
if (event.type === "payment.paid") markOrderPaid(event.data.order_id);
res.sendStatus(200);
});Retries
A delivery succeeds when your endpoint returns a 2xx status. Any other response, a timeout, or a connection error is retried with exponential backoff over roughly nine hours. Events can arrive out of order and more than once, so make your handler idempotent and key off the payment id.
Testing delivery
Fire a sample event at one of your endpoints to confirm the URL, signature check and your handler all work — no real payment required. The response reports the HTTP status your server returned.
curl https://apa.app/v1/webhooks/test \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{ "endpoint_id": "we_123", "event": "payment.paid" }'
200 OK
{ "data": { "delivered": true, "response_code": 200 }, "request_id": "req_7qP1zM5x" }