Skip to content

Webhooks

Aura sends a webhook to your endpoint when a purchase reaches a final state, so you do not have to poll. Your technical contact sets up your endpoint URL and gives you its signing secret.

Event Sent when
purchase.successful The network confirmed the bundle was credited. You were charged.
purchase.failed The purchase ended without a credit. Nothing was charged.
purchase.reversed The network reversed a credit it had confirmed. You were refunded.

There is no event for pending: the response to your purchase request already tells you that.

POST /webhooks/aura HTTP/1.1
Content-Type: application/json
Aura-Event-Id: evt_01JA80C4M2T7R9F3H6K8P1D5QS
Aura-Event-Type: purchase.successful
Aura-Timestamp: 1760260476
Aura-Signature: v1=5f2b0c7d9e8a41b3c6d0e9f2a7b4c1d8e3f6a9b2c5d8e1f4a7b0c3d6e9f2a5b8
{
"id": "evt_01JA80C4M2T7R9F3H6K8P1D5QS",
"object": "event",
"type": "purchase.successful",
"created_at": "2026-10-12T09:14:31Z",
"data": {
"object": {
"id": "pur_01JA7ZK3Q9W2D8M5X4N6B1C0VE",
"object": "purchase",
"reference": "order-10293",
"status": "successful",
…
}
}
}

data.object is the whole purchase, exactly as Get a purchase would return it at that moment. The full schemas are in the API reference.

Every webhook is signed. Check the signature before you trust anything in it:

  1. Take Aura-Timestamp and the raw request body, before any JSON parsing.
  2. Compute the HMAC-SHA256 of {timestamp}.{raw body} with your webhook secret, as lowercase hex.
  3. Compare it, in constant time, with each v1= value in Aura-Signature. During a secret rotation there are two, separated by a comma; accept the webhook if either matches.
  4. Reject the webhook if the timestamp is more than five minutes from your clock. That stops an old webhook being replayed.
import crypto from 'node:crypto';
import express from 'express';
const SECRET = process.env.AURA_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;
export function isValidAuraWebhook(rawBody, timestamp, signatureHeader) {
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || !Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;
const expected = crypto.createHmac('sha256', SECRET).update(`${timestamp}.`).update(rawBody).digest('hex');
return (signatureHeader ?? '').split(',').some((part) => {
const [version, signature] = part.trim().split('=');
return (
version === 'v1' &&
signature?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
});
}
const app = express();
app.post('/webhooks/aura', express.raw({ type: 'application/json' }), async (req, res) => {
if (!isValidAuraWebhook(req.body, req.get('Aura-Timestamp'), req.get('Aura-Signature'))) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
await saveEventOnce(event); // Store by event.id; ignore IDs you have already stored.
res.sendStatus(200);
});

Return any 2xx status within 10 seconds. Store the event first, then do slow work, such as messaging the customer, afterwards. Anything else, including a timeout or a redirect, counts as a failed delivery.

If a delivery fails, Aura tries again: 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours after the first attempt. Each attempt is signed again with a new timestamp and carries the same Aura-Event-Id.

If every attempt fails, the event is not sent again. Nothing is lost: the purchase’s final state is always there to look up. If your endpoint was down, look up the purchases you are still holding as pending once it is back.

  • Delivered at least once. The same event can arrive more than once. Record each Aura-Event-Id you process and ignore repeats.
  • Not guaranteed in order. A purchase.reversed could, rarely, arrive before the purchase.successful it follows. A purchase only ever moves forward, from pending to successful or failed, and from successful to reversed. Never move a purchase backwards in your records because of a late event.
  • It must use HTTPS with a valid certificate.
  • It must accept a POST with a JSON body.
  • Your technical contact can send test events to it, and the sandbox sends real webhooks for test purchases.