Airwallex
Setup guide for the airwallex payments provider. The shared API (initiate_payment,
verify_payment, refund_payment, webhooks, hooks, saved methods) is documented in
Payments.
The airwallex provider supports one-time payments through Airwallex Payment
Links (initiate_payment, verify_payment; cards and the local payment
methods your account enables), refunds (refund_payment), reading disputes
and webhooks, in the currencies Airwallex acquires (AUD, HKD, SGD, CNY, GBP,
EUR, USD, …). GET /payments/providers reports dispute_read.
| Feature | Status |
|---|---|
| One-time payments (Payment Links), verify, refunds | Supported |
get_dispute, dispute webhooks |
Supported |
submit_dispute_evidence |
Not supported (NOT_SUPPORTED, 501): respond in the Airwallex web app |
| Subscriptions, billing portal | Not supported (NOT_SUPPORTED, 501) |
Saved payment methods, charge_off_session |
Not supported yet (NOT_SUPPORTED, 501) |
It needs no extra install: it calls Airwallex’s REST API directly over HTTP
(https://api-demo.airwallex.com in sandbox, https://api.airwallex.com
in production), following Airwallex’s API reference as of September 2026.
Why plain HTTP (no Airwallex SDK). Airwallex publishes no Python SDK; its official server SDK is for Node.js.
Access token. Every call carries a bearer token from
POST /api/v1/authentication/login (x-client-id, x-api-key), cached per
instance until two minutes before its expires_at (about 30 minutes) and
fetched again once when a call answers 401.
1. Get credentials. In the Airwallex web app, Developer -> API keys, copy the Client ID and create an API key (a sandbox account has its own).
2. Set the webhook. In Developer -> Webhooks, add a notification URL
https://your-domain.com/payments/webhook/airwallex
(/payments/webhook/<instance> for another instance name), subscribe to
payment_link.paid, refund.accepted, refund.settled and the
payment_dispute.* events, and copy the URL’s secret.
3. Configure the instance and secrets. In kirak.json:
"airwallex": { "type": "airwallex", "environment": "sandbox" }environment is sandbox (the default, the demo host) or production. In
the environment:
KIRAK_PAYMENT_AIRWALLEX_CLIENT_ID=...KIRAK_PAYMENT_AIRWALLEX_API_KEY=...KIRAK_PAYMENT_AIRWALLEX_WEBHOOK_SECRET=<the notification URL's secret>For an instance with another name the variables are
KIRAK_PAYMENT_<INSTANCE>_CLIENT_ID, _API_KEY and _WEBHOOK_SECRET. All
three are required.
4. Use it:
result = await kirak.payments.initiate_payment({ "provider": "airwallex", "user_id": "42", "amount": 16515, "currency": "EUR", # cents: EUR 165.15 "name": "Pro Plan", "type": "one_time",})reference = result["data"]["reference"] # the link's reference, kirak-<32 hex>
await kirak.payments.verify_payment({"provider": "airwallex", "payment_id": reference})await kirak.payments.refund_payment({"provider": "airwallex", "payment_id": reference, "amount": 6515})Payments. initiate_payment stores a random reference (kirak-<32 hex>)
on the PENDING row before creating a single-use payment link (title =
name, amount x quantity, the reference, metadata naming the transaction
and instance), then keeps the link id on the row and returns the link’s
url. A 4xx answer marks the row FAILED; a timeout, 5xx or 429 leaves it
PENDING.
payment_link.paid is never trusted alone: the link is re-read (it must carry
the row’s reference and stored link id, be PAID and have metadata that,
where present, names this transaction and instance), then its
latest_successful_payment_intent_id is read; the row completes once
(payment_completed, with amount and currency) only for a SUCCEEDED
intent in the row’s currency for at least amount x quantity. Otherwise
nothing is written and the webhook fails (500), so Airwallex retries. A row
whose create call timed out (so no link id was stored) is completed from the
event’s link, re-read the same way. verify_payment sets PROCESSING for a
confirmed payment.
Refunds. refund_payment (payment_id = the reference; optional
amount in minor units and reason) calls POST /api/v1/pa/refunds/create
for the completing payment intent, with metadata naming the reference and
instance and a request_id derived from the caller’s idempotency_key and
the transaction’s random reference (random without one), so a retry with the
same key never refunds twice. A refund Airwallex answers ACCEPTED or
SETTLED is recorded at once; one RECEIVED is recorded by the
refund.accepted/refund.settled webhook, which re-reads the refund and
finds the row through that metadata (keyed by refund id with a running total;
REFUNDED never moves back; refund_completed). Refunds made in the
Airwallex web app carry no such metadata and are not recorded in Kirak.
Disputes (read-only). get_dispute reads
GET /api/v1/pa/payment_disputes/{id}. A payment_dispute.* webhook re-reads
the dispute and its payment intent and reports it (dispute_created for
payment_dispute.requires_response, else dispute_updated) for the row that
intent completed, found through the intent’s metadata or
merchant_order_id naming the reference; the row is not changed.
Webhook security. x-signature must be the hex HMAC-SHA256, with the
notification URL’s secret, of x-timestamp (milliseconds) followed by the raw
body; a missing header is MISSING_WEBHOOK_SIGNATURE, a wrong one
INVALID_WEBHOOK_SIGNATURE (400). No time window is set: Airwallex does not
document whether a retry is signed again. Deliveries are deduplicated on the
event id.
Currencies. Airwallex amounts are major units, sent as JSON numbers built exactly from the decimal, with ISO 4217 decimals.
Known limits.
- Subscriptions, saved payment methods and off-session charges are not supported yet.
- Dispute webhooks are reported only when Airwallex copies the link’s
reference to the payment intent (
metadataormerchant_order_id); this is not documented and is on the sandbox checklist. - Refunds made in the Airwallex web app are not recorded.
Webhook events
Section titled “Webhook events”after_webhook receives these provider-neutral event names (see
Webhook Hooks):
Airwallex reports payment_completed (payment_link.paid, once the re-read link and payment intent confirm it), refund_completed (refund.accepted/refund.settled of a Kirak refund, event_type refund.recorded), dispute_created (payment_dispute.requires_response) and dispute_updated (other payment_dispute.*, event_type payment_dispute.updated).