Guide to Airwallex Webhooks: Features and Best Practices
Airwallex webhooks notify your systems about payments activity: a payment intent succeeds, a payment attempt is authorized, a refund settles, a dispute opens. If you're building payments on Airwallex, webhooks are how you react to money movement without polling.
This guide covers how Airwallex webhooks work, the events you'll handle, how to verify the x-signature, replay protection, and the best practices for production.
What are Airwallex webhooks?
Airwallex webhooks are HTTP POSTs delivered to endpoints you configure, each signed. Every delivery carries two headers: x-timestamp (Unix milliseconds) and x-signature (a hex HMAC-SHA256 digest). Each webhook URL has its own unique secret.
Airwallex webhook features
| Feature | Details |
|---|---|
| Configuration | Web app > Settings > Developer > Webhooks (per-URL secret) |
| Signature headers | x-timestamp (Unix ms), x-signature (hex HMAC-SHA256) |
| Signature scheme | HMAC-SHA256 over x-timestamp + raw_body, keyed with the per-endpoint secret |
| Replay check | Compare x-timestamp to now (no fixed window documented) |
| Transport | HTTPS; Airwallex publishes source IPs to allowlist |
| Retries | Any non-200/timeout is retried; events can be re-triggered from the dashboard |
| SDK | @airwallex/node-sdk (beta); no pip |
Common events
Airwallex event types are dot-namespaced:
| Event | Fires when |
|---|---|
payment_intent.succeeded | A payment intent succeeds |
payment_intent.requires_capture | A payment intent is authorized and awaits capture |
payment_attempt.authorized | A payment attempt is authorized |
refund.received | A refund is received |
refund.settled | A refund settles |
payment_dispute.opened | A dispute is opened |
Note the exact families: refunds are refund.received / accepted / settled / failed (there is no refund.processed), and disputes are payment_dispute.* (not dispute.*). Also payment_consent.*. Consult Airwallex's event reference for the full list.
Setting up Airwallex webhooks
Configure webhook URLs under Web app > Settings > Developer > Webhooks. Each URL gets its own unique secret, that's the HMAC key. Endpoints must be HTTPS; optionally allowlist Airwallex's published source IPs.
Securing Airwallex webhooks
Each delivery carries x-timestamp and x-signature. To verify, concatenate x-timestamp + raw_request_body (in that order), compute an HMAC-SHA256 with the endpoint's secret, hex-encode it, and compare against x-signature. Verify against the original, unmodified raw body, before any JSON parsing.
const crypto = require("crypto");
const SECRET = process.env.AIRWALLEX_WEBHOOK_SECRET; // per-endpoint
function verify(timestamp, rawBody, signature) {
const valueToDigest = timestamp + rawBody.toString();
const expected = crypto
.createHmac("sha256", SECRET)
.update(valueToDigest)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.headers["x-timestamp"];
if (!verify(timestamp, req.body, req.headers["x-signature"])) {
return res.sendStatus(401);
}
// Optional replay check: reject implausibly old timestamps
if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) {
return res.sendStatus(401);
}
res.sendStatus(200); // acknowledge fast
processQueue.add(JSON.parse(req.body)); // process asynchronously
});
The same check in Python:
import hashlib
import hmac
import os
SECRET = os.environ["AIRWALLEX_WEBHOOK_SECRET"].encode()
def verify(timestamp: str, raw_body: bytes, signature: str) -> bool:
value = timestamp.encode() + raw_body
expected = hmac.new(SECRET, value, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
Airwallex documents a replay check against x-timestamp but doesn't publish a fixed tolerance, so pick a window (a few minutes) that fits your latency.
Airwallex webhook limitations and pain points
The signed value is timestamp + body
The Problem: The HMAC is over x-timestamp concatenated with the raw body, not the body alone. Signing the body by itself fails, and re-serializing the body before verifying also fails.
Why It Happens: Airwallex binds the timestamp into the signed value for replay protection.
Workarounds:
- Concatenate
x-timestamp + raw_bodyand verify before parsing.
How Hookdeck Can Help: Hookdeck verifies the x-signature at the edge, so your app receives pre-verified events without the concatenation and raw-body handling.
No documented replay window
The Problem: Airwallex describes a timestamp check but doesn't publish a tolerance, so you have to choose one. Too loose invites replays; too tight drops delayed deliveries.
Why It Happens: The window isn't specified.
Workarounds:
- Pick a sensible window (a few minutes) and make processing idempotent so a replay has no additional effect.
How Hookdeck Can Help: Hookdeck verifies freshness and deduplicates at the edge, so replays are filtered before they reach your app. See our guide to webhook idempotency.
Per-endpoint secrets
The Problem: Each webhook URL has its own secret, so verifying with the wrong secret (or sharing one across endpoints) fails.
Why It Happens: Airwallex scopes secrets per endpoint.
Workarounds:
- Store and select the secret per endpoint.
How Hookdeck Can Help: Hookdeck verifies each source with its own secret at the edge, so the key lives in one place.
Event-name families are specific
The Problem: It's easy to assume refund.processed or dispute.*, but Airwallex uses refund.received/accepted/settled/failed and payment_dispute.*. Handlers keyed to the wrong names silently miss events.
Why It Happens: Airwallex's naming differs from common assumptions.
Workarounds:
- Branch on the exact dot-namespaced event types and handle unknown ones defensively.
How Hookdeck Can Help: Hookdeck's filters route on the event type you actually receive, so naming is handled in one place.
Best practices
Verify over timestamp + raw body
Concatenate x-timestamp + raw_body, HMAC-SHA256 with the per-endpoint secret, hex-encode, and compare in constant time. Verify before parsing.
Add a replay window and dedupe
Reject implausibly old x-timestamp values and dedupe on an event id since re-triggers reuse it.
Acknowledge fast, process asynchronously
Return 200 immediately and defer work to a queue. See why to process webhooks asynchronously.
Confirm before fulfilling
For payment events, confirm state via the API before releasing value.
Make Airwallex webhooks production-ready
Hookdeck verifies x-signature, deduplicates, and durably queues every payment event
Conclusion
Airwallex webhooks are verified with an x-signature HMAC over x-timestamp + raw body, keyed per endpoint, with a replay check you tune yourself. Verify against the raw body, use the exact refund.* / payment_dispute.* event names, add a freshness window, and dedupe.
Hookdeck verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique payment events.
Get started with Hookdeck for free and handle Airwallex webhooks reliably in minutes.