# Apa Webhooks

Canonical: https://apa.app/developers/webhooks

Apa sends signed JSON webhook events whenever a payment changes state. The `Apa-Signature` header has the form `t=<timestamp>,v1=<hex>`, where `v1` is an HMAC-SHA256 over `<timestamp>.<raw body>` keyed with your endpoint signing secret (`whsec_`). Recompute and compare it in constant time before trusting the event.

## Events

- payment.created
- payment.pending
- payment.routing
- payment.settling
- payment.paid
- payment.failed
- payment.expired
- payment.refund_required
- payment.refunded

## Verification

```js
import crypto from "node:crypto";

// 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");

  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;
}
```

## Payment paid payload

```json
{
  "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_123",
    "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_hashes": ["0x9a2f...f70"],
    "failure_reason": null,
    "metadata": { "customer_id": "cus_789" }
  }
}
```

## Integration guidance

- Return any 2xx quickly.
- Handle duplicate events idempotently.
- Events can arrive out of order.
- Fulfill ecommerce orders only after a terminal success state such as `payment.paid`.
- Do not rely only on the customer success redirect.
