# Webhooks

Triple-A sends an HTTP `POST` with `Content-Type: application/json` to your `notify_url` each time a payment status changes. Respond with any 2xx status to confirm that you received it.

## Retries

If you return anything other than a 2xx status, or your URL is unreachable, Triple-A retries up to five times after the first attempt. The table below shows how long Triple-A waits before each retry.

| Retry | Wait after the previous attempt |
|---|---|
| 1st | 30 seconds |
| 2nd | 60 seconds |
| 3rd | 5 minutes |
| 4th | 10 minutes |
| 5th | 10 minutes |

Because of retries, the same notification can arrive more than once, so make your handler safe to run twice for the same payment. If all retries fail, contact support@triple-a.io to have the webhook triggered again.

## Fields

The table below lists the fields you need to handle a payment notification. The `payment` webhook callback on [Make a payment request](/api/stablecoin-payments/payments/post-payment/) has the full list.

| Field | Use it for |
|---|---|
| `event` | The notification type. It's `payment` for payment notifications. |
| `payment_reference` | Matching the notification to the payment you created. |
| `status` | The payment status. See [Payment statuses](/docs/reference/payment-statuses/). |
| `order_id` | Your own order ID, if you set one. |
| `crypto_currency`, `crypto_amount` | What the customer paid. See [Coins and networks](/docs/reference/coins-and-networks/). |
| `payment_currency`, `payment_amount` | The guaranteed amount in your preferred currency. |
| `api_id` | The account that was used. A sandbox API ID means a test payment. |

Triple-A also sends `transaction_fee_amount`, `vat_amount`, and `cart`. The `txs` field is for Triple-A's debugging, so you can ignore it. To choose which amount to reconcile on, see [Your first payment](/docs/stablecoin-payments/your-first-payment/#which-currency-is-which).

## Verify the signature

Every webhook carries a `Triplea-Signature` header. Its value has a timestamp and a signature in the following format.

```text title="Triplea-Signature header"
t=<unix-timestamp>,v1=<hex-encoded-signature>
```

To verify a webhook, follow these steps.

<Steps>
  <Step>Read `t` and `v1` from the header.</Step>
  <Step>Build the string `<t>.<raw request body>`.</Step>
  <Step>Compute an HMAC-SHA256 of that string with your `notify_secret` as the key, and hex-encode it.</Step>
  <Step>Compare the result with `v1`.</Step>
  <Step>Check that `t` is within 300 seconds of the current time.</Step>
</Steps>

<Callout type="warning" title="Use the raw body">
  Verify against the exact bytes you received, before any JSON parsing. Parsing and serializing again can change the formatting and break the signature.
</Callout>

The following Node.js example verifies a webhook with a constant-time comparison. The highlighted lines read the raw body, sign the raw bytes, and compare the signatures in constant time.

```js title="verify-webhook.js" {7,13,18}
const crypto = require('crypto');
const express = require('express');
const app = express();

const SECRET = process.env.TRIPLEA_NOTIFY_SECRET;

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.headers['triplea-signature'] || '';
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));

  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${parts.t}.${req.body}`) // req.body is a Buffer of the raw bytes
    .digest('hex');

  const a = Buffer.from(parts.v1 || '', 'hex');
  const b = Buffer.from(expected, 'hex');
  const valid = a.length === b.length && crypto.timingSafeEqual(a, b);
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;

  if (!valid || !fresh) return res.status(400).end();

  const payment = JSON.parse(req.body);
  // Look up the order by payment.payment_reference or payment.order_id,
  // then act on payment.status.
  res.status(200).end();
});

app.listen(3000);
```

The example is adapted from the Node.js sample on developers.triple-a.io, which also provides a sample body, secret, and header for testing your verification code. Comment out the timestamp check when you use them, because the sample is older than 5 minutes.

## Related content

These pages cover related topics.

- [Stablecoin checkout](/docs/stablecoin-payments/tutorials/stablecoin-checkout/) shows where webhooks fit in a full checkout
- [Error codes](/docs/reference/error-codes/) lists the errors a payment request can return