Gareth Wilson Gareth Wilson

Guide to Airwallex Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationWeb app > Settings > Developer > Webhooks (per-URL secret)
Signature headersx-timestamp (Unix ms), x-signature (hex HMAC-SHA256)
Signature schemeHMAC-SHA256 over x-timestamp + raw_body, keyed with the per-endpoint secret
Replay checkCompare x-timestamp to now (no fixed window documented)
TransportHTTPS; Airwallex publishes source IPs to allowlist
RetriesAny 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:

EventFires when
payment_intent.succeededA payment intent succeeds
payment_intent.requires_captureA payment intent is authorized and awaits capture
payment_attempt.authorizedA payment attempt is authorized
refund.receivedA refund is received
refund.settledA refund settles
payment_dispute.openedA 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_body and 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.