# 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.

```javascript
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:

```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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Confirm before fulfilling

For payment events, confirm state via the API before releasing value.

## 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Airwallex webhooks reliably in minutes.