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.

RetryWait after the previous attempt
1st30 seconds
2nd60 seconds
3rd5 minutes
4th10 minutes
5th10 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 [email protected] 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 has the full list.

FieldUse it for
eventThe notification type. It’s payment for payment notifications.
payment_referenceMatching the notification to the payment you created.
statusThe payment status. See Payment statuses.
order_idYour own order ID, if you set one.
crypto_currency, crypto_amountWhat the customer paid. See Coins and networks.
payment_currency, payment_amountThe guaranteed amount in your preferred currency.
api_idThe 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.

Verify the signature

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

Triplea-Signature header
t=<unix-timestamp>,v1=<hex-encoded-signature>

To verify a webhook, follow these steps.

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

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.

verify-webhook.js
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.

These pages cover related topics.