# Agentic Banking Hackathon: Starter Kits

This document maps the exact API endpoints, MCP tools, simulation paths and doc links for all eight build concepts. It's a technical routing table, not the inspiration guide. For concept details, build requirements and controls, see the Builder Inspiration Guide.

Concepts 1 through 4 act for one company on its own money. Concepts 5 through 8 act for many customers through connected accounts, and they share a [common setup section](#connected-accounts-shared-setup-for-concepts-5-to-8).

Every error string, status name, field name and error code in this document came from a real call against the sandbox, not from reading the API reference.

**Scope.** Everything below holds for the **US and GB payment corridors**: every beneficiary payload, every routing rule, every error code. Other corridors use different required fields and different bank validation, so call `POST /api/v1/beneficiaries/schema` for your target country and currency before assuming a payload will work. Sandbox behavior can also change without an announcement, so if something here does not match what you see, ask in the hackathon Slack support channel rather than building a workaround.

## Sandbox environment

**Base URL:** `https://api.sandbox.airwallex.com`

Do not use `api-demo.airwallex.com`. It's deprecated.

**Authentication:** `POST /api/v1/authentication/login` with `x-client-id` and `x-api-key` headers. Returns a bearer token valid for 30 minutes.

```bash
curl -X POST https://api.sandbox.airwallex.com/api/v1/authentication/login \
  -H "Content-Type: application/json" \
  -H "x-client-id: YOUR_CLIENT_ID" \
  -H "x-api-key: YOUR_API_KEY"
```

Response:
```json
{
  "token": "eyJhbGciOiJIUzI1NiJ9...",
  "expires_at": "2026-08-11T12:30:00+0000"
}
```

Include the token as `Authorization: Bearer <token>` on all subsequent calls.

**Amounts are in major units, not cents.** `100` means one hundred dollars, not one dollar. If you have built on a payments API that uses minor units, this is the one thing most likely to cost you a demo: send `5000` intending fifty dollars and you will move five thousand. Decimals are fine, so `12.85` is twelve dollars eighty-five. This holds across REST and MCP, for transfers, charges, conversions, card limits and simulated card transactions.

**Do you need MCP?** No. Every action in this document can be done over REST, and a few can only be done over REST. The MCP server is a convenience layer that saves you some payload assembly, most usefully for cardholders and for validating a beneficiary before you commit it. It authenticates as your platform account and cannot act on behalf of a connected account, so Concepts 5 through 8 are mostly REST work. Pick whichever you like for Concepts 1 through 4 and check the table below before assuming a tool exists.

**Connecting the Airwallex Developer MCP.** This is a hosted Airwallex server, not something you build. Point your coding agent at `https://mcp.sandbox.airwallex.com/developer` and complete the OAuth flow with your sandbox account. It is sandbox only and cannot touch production.

```shell
# Claude Code
claude mcp add-json airwallex-dev '{ "type": "http", "url": "https://mcp.sandbox.airwallex.com/developer" }'
```

Cursor, Codex, Gemini CLI, Lovable and Repl.it use the same URL through their own MCP settings. Two things to know if you are building in a browser-based agent: **V0 does not support this endpoint**, and Lovable needs a paid plan for MCP. If your agent cannot authenticate, fall back to the documentation-only server at `https://mcp.sandbox.airwallex.com/docs`, which needs no auth, and do your API calls over REST.

Every MCP tool named in this document comes from that server. To confirm your connection works, ask your agent to check sandbox balances and see whether it invokes an Airwallex tool.

**Getting credentials.** Airwallex provisions a sandbox account for every accepted team, with Scale and connected accounts already switched on. You will receive a client ID and an API key. Set them as `AIRWALLEX_CLIENT_ID` and `AIRWALLEX_API_KEY` and never commit them. The token from the login call lasts 30 minutes, so refresh it rather than caching it for the length of a long run.

**Docs:** [API reference](https://www.airwallex.com/docs/api/introduction) | [Sandbox environment](https://www.airwallex.com/docs/developer-tools/sandbox-environment) | [Developer MCP](https://www.airwallex.com/docs/developer-tools/ai/developer-connector) | [Postman collection](https://www.airwallex.com/docs/developer-tools/api/quickstart-with-postman)

---

## Fund your sandbox first

Every concept here assumes you have money to move. A fresh sandbox account may start with nothing, and there is no button to top it up. You mint your own funds by creating a Global Account and simulating a deposit into it.

```
POST /api/v1/global_accounts/create
  { "request_id": "...", "country_code": "US",
    "required_features": [{ "currency": "USD", "transfer_method": "LOCAL" }] }

POST /api/v1/simulation/deposit/create
  { "global_account_id": "<id from above>", "amount": 25000, "payer_name": "Seed funding" }
```

The balance shows up immediately even though the deposit response says PENDING.

A few things that will trip you up:

- **`country_code` is a short allowlist**, and it names the country of the account, not of the currency. Accepted: `AU, GB, VN, NL, FR, DK, EE, DE, HK, JP, US, SG, CA, NZ, PL, ID, AE, MX, KR, BR, MY, PH, SE, NO, IL, CN`. For EUR use `NL`, `FR`, `DE`, `EE` or another euro member on that list. `BE` is rejected.
- **`required_features` is an array of objects**, `[{currency, transfer_method}]`, not an array of strings.
- **A US account comes back ACTIVE at once. A GB account comes back PROCESSING**, and deposits into it are still accepted while it settles.
- **There is no practical deposit cap.** A single simulated deposit of 10,000,000 was accepted, so fund generously and stop worrying about it.
- To hold a currency you have no Global Account for, fund in USD and convert. Concept 6 does that for its EUR and GBP payroll.

---

## REST vs. MCP decision table

Every Airwallex action used across the eight concepts, with the interface you should use for each.

All 21 MCP tools below were called against the sandbox and every one worked, so "Either works" means it was tested both ways rather than assumed. Three things are worth knowing before you choose a side:

- **MCP is much the easier path for cards.** A DELEGATE cardholder needs only an email and comes back READY at once, where the REST route wants a name, date of birth, address and a consent field, then leaves you polling.
- **MCP `create_beneficiary` takes `dry_run: true`**, which validates a payload and returns `"OK"` without creating anything, or the same validation error a real create would give. Note it takes nested `bank_details` and `address` objects rather than flat fields. The REST counterpart is `POST /api/v1/beneficiaries/validate`, which also accepts `x-on-behalf-of`.
- **The two interfaces disagree on card transaction status names.** See the Concept 2 gotchas before you write any matching logic.
- **Two things have no working MCP path at all.** Booking an FX conversion and challenging a payment dispute both need REST. So does anything acting on behalf of a connected account, because MCP authenticates as the platform and cannot send `x-on-behalf-of`.

| Action | MCP tool | REST endpoint | Notes |
|--------|----------|---------------|-------|
| Check balances | `get_account_balances` | `GET /api/v1/balances/current` | Either works |
| Get FX rate | `get_fx_rate` | `GET /api/v1/fx/rates/current` | Either works. Do NOT send `x-api-version` header on this endpoint. |
| Create FX quote | `create_fx_quote` | `POST /api/v1/fx/quotes/create` | Either works |
| Book FX conversion | -- | `POST /api/v1/fx/conversions/create` | **REST only.** No MCP tool. See [REST helper](#rest-helper-for-fx-conversion) below. |
| List beneficiaries | `list_beneficiaries` | `GET /api/v1/beneficiaries` | Either works |
| Get beneficiary schema | `get_beneficiary_schema` | `POST /api/v1/beneficiaries/schema` | Either works |
| Create beneficiary | `create_beneficiary` | `POST /api/v1/beneficiaries/create` | Either works |
| Create transfer | `create_transfer` | `POST /api/v1/transfers/create` | Either works |
| List transfers | `list_transfers` | `GET /api/v1/transfers` | Either works |
| Simulate transfer status | `simulate_transfer_result` | `POST /api/v1/simulation/transfers/{id}/transition` | Sandbox only |
| Create cardholder | `create_cardholder` | `POST /api/v1/issuing/cardholders/create` | Either works |
| List cardholders | `list_cardholders` | `GET /api/v1/issuing/cardholders` | Either works |
| Create virtual card | `create_card` | `POST /api/v1/issuing/cards/create` | **Use MCP for your own account.** For a card on a customer's account (Concepts 5 to 8) you must use REST with `x-on-behalf-of`, or the card is issued on your platform account instead and draws on the wrong wallet. See constraint 6 on card creation. |
| Retrieve card | `retrieve_card` | `GET /api/v1/issuing/cards/{card_id}` | Either works |
| Retrieve card limits | `retrieve_card_limits` | `GET /api/v1/issuing/cards/{card_id}/limits` | Either works |
| Simulate card transaction | `simulate_create_issuing_transaction` | `POST /api/v1/simulation/issuing/create` | Sandbox only |
| Simulate card capture | `simulate_capture_issuing_transaction` | `POST /api/v1/simulation/issuing/{transaction_id}/capture` | Sandbox only |
| Simulate card reverse | `simulate_reverse_issuing_transaction` | `POST /api/v1/simulation/issuing/{transaction_id}/reverse` | Sandbox only |
| Simulate card refund | `simulate_refund_issuing_transaction` | `POST /api/v1/simulation/issuing/refund` | Sandbox only |
| List card transactions | `list_issuing_transactions` | `GET /api/v1/issuing/transactions` | Either works |
| List global accounts | `list_global_accounts` | `GET /api/v1/global_accounts` | Either works |
| Simulate Global Account deposit | `simulate_create_deposit` | `POST /api/v1/simulation/deposit/create` | Sandbox only |
| Create a payment dispute | `simulate_create_payment_dispute` | `POST /api/v1/simulation/pa/payment_disputes/create` | Sandbox only. Visa reason codes. |
| List payment disputes | `list_payment_disputes` | `GET /api/v1/pa/payment_disputes` | Either works |
| Accept a dispute | `accept_payment_dispute` | `POST /api/v1/pa/payment_disputes/{id}/accept` | Either works. Produces a refund. |
| Challenge a dispute | -- | `POST /api/v1/pa/payment_disputes/{id}/challenge` | **REST only.** MCP omits required fields. See constraint 19 on dispute stages and reason codes. |
| Escalate a dispute | `simulate_escalate_payment_dispute` | `POST /api/v1/simulation/pa/payment_disputes/{id}/escalate` | Sandbox only |
| Resolve a dispute | -- | `POST /api/v1/simulation/pa/payment_disputes/{id}/resolve` | Sandbox only |
| Upload an evidence file | -- | `POST /api/v1/files/upload` on `files.sandbox.airwallex.com` | **Different host.** See constraint 20 on the file upload host. |
| Create connected account | -- | `POST /api/v1/accounts/create` | **REST only** |
| Activate connected account | -- | `POST /api/v1/simulation/accounts/{id}/update_status` | Sandbox only. Needs `force: true`. |
| Fund a customer wallet | -- | `POST /api/v1/connected_account_transfers/create` | **REST only** |
| Charge a customer wallet | -- | `POST /api/v1/charges/create` | **REST only** |
| Validate a beneficiary | -- | `POST /api/v1/beneficiaries/validate` | **REST only.** Works with `x-on-behalf-of`. |
| Platform report | -- | `POST /api/v1/platform_reports/create` | **REST only.** `file_format` required. |

---

## REST helper for FX conversion

If you're building Concept 1, or Concept 6 on behalf of an employer, you'll book an FX conversion. There's no MCP tool for it, so you'll call REST directly. Here's a minimal wrapper:

```typescript
const AIRWALLEX_BASE = "https://api.sandbox.airwallex.com";

async function airwallexLogin(clientId: string, apiKey: string): Promise<string> {
  const res = await fetch(`${AIRWALLEX_BASE}/api/v1/authentication/login`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-client-id": clientId,
      "x-api-key": apiKey,
    },
  });
  const data = await res.json();
  return data.token;
}

async function bookFxConversion(
  token: string,
  params: {
    buy_currency: string;
    sell_currency: string;
    buy_amount?: string;
    sell_amount?: string;
    quote_id?: string;
    request_id: string;
  }
): Promise<any> {
  const res = await fetch(`${AIRWALLEX_BASE}/api/v1/fx/conversions/create`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${token}`,
    },
    body: JSON.stringify(params),
  });
  return res.json();
}
```

**Required fields:** `buy_currency`, `sell_currency`, one of `buy_amount` or `sell_amount`, and `request_id`. Optionally pass `quote_id` from a prior `create_fx_quote` call to lock the rate.

**Docs:** [Create a conversion](https://www.airwallex.com/docs/api/transactional_fx/conversion/create) | [FX guide](https://www.airwallex.com/docs/transactional-fx/get-started/create-a-conversion)

---

## Known sandbox constraints

These apply across concepts.

1. **Transfer auto-advance.** Transfers move from SCHEDULED to PROCESSING on their own within a few seconds. This does not put you under time pressure, because the simulation calls still work from PROCESSING. See constraint 3 on transfer transitions.
2. **A failed payment ends in CANCELLED, never in a status called FAILED.** Whichever route you take, the terminal status is `CANCELLED` and the useful detail is in `failure_type` and `failure_reason`. Do not write a state machine that waits for `FAILED`, because it never arrives. Two routes work:
   - `next_status: "CANCELLED"` from SCHEDULED or PROCESSING gives `CANCELLED` with `failure_type: PAYMENT_RECALLED` and `failure_reason: "simulation"`.
   - `next_status: "FAILED"` plus a `failure_type` works only once the transfer is SENT. It gives `CANCELLED`, and your requested type shows up in `failure_reason` (sending `INVALID_ACCOUNT_NAME_OR_NUMBER` returned `failure_reason: "90101: Invalid account name/number"` with `failure_type: INCORRECT_ROUTING`). Use this route when you want a specific, realistic failure.
   - `next_status: "FAILED"` from SCHEDULED returns `operation_failed`. Send the transfer first.
3. **Supported transfer transitions.** SCHEDULED advances to PROCESSING on its own within seconds. You can request SENT from a fresh transfer and then PAID. Cancellation is not time-sensitive: cancelling a transfer that had been sitting in PROCESSING for 10 seconds worked fine.
4. **FX rate header conflict.** `GET /api/v1/fx/rates/current` breaks if you send the `x-api-version` header. Omit it for rate checks.
5. **Card limits enforced.** Per-transaction amount controls are active: $30 clears under a $50 limit; $100 is declined with `failure_reason: LIMIT_EXCEEDED`.
6. **Card creation over REST works with a standard sandbox key.** `POST /api/v1/issuing/cards/create` succeeds, though only tested on a Scale-enabled account, so it does not prove the path is open on a fresh self-serve account. Use MCP `create_card` as the reliable path for cards on your own account, and REST when you need fields MCP does not expose or when you are issuing on behalf of a customer. MCP cannot send `x-on-behalf-of`, so a customer's card must be created over REST. Getting this wrong is quiet rather than loud: the card is created successfully, just against the wrong account. Required REST fields: `request_id`, `cardholder_id`, `created_by`, `form_factor`, `is_personalized`, `program.purpose`, `authorization_controls`. Limits go in `authorization_controls.transaction_limits.limits`, not `spend_limits`.
7. **Cardholders reach READY on their own. Do not call `pass_review`.** A new cardholder returns PENDING and a new card may return PENDING, but both promote themselves to READY and ACTIVE within seconds without any call. `POST /api/v1/simulation/issuing/cardholders/{id}/pass_review` returns HTTP 400 `passReviewWithoutKyc` on both standard and connected accounts. Poll the cardholder and card instead, and wait for ACTIVE before simulating a transaction against the card.
8. **FX conversion settles immediately.** No pending state in sandbox. Conversions return `status: "SETTLED"` with `settlement_cutoff_at` equal to `created_at`.
9. **FX rate is not an FX quote.** `get_fx_rate` returns an indicative rate with no `quote_id`. You can't pass it to the conversion endpoint. To lock a rate, call `create_fx_quote`, which returns a `quote_id` and a validity window. You can also skip the quote and convert at spot rate by omitting `quote_id` from the conversion call.
10. **FX quotes are multi-use.** `create_fx_quote` returns `usage: "MULTI_USE"`. If you make two conversion calls with the same `quote_id` but different `request_id` values, both will go through. Guard against accidental double-booking in your application logic.
11. **Global Account deposit.** `simulate_create_deposit` returns a PENDING status in the response, but you'll see the balance reflect it immediately. No polling needed.
12. **Beneficiary address required.** You must include address fields for US/LOCAL beneficiaries (`street_address`, `city`, `state`, `postcode`, `country_code`). Other corridors vary. Always call `get_beneficiary_schema` first to see exactly which fields you must provide for your target country and currency combination.
13. **`bank_account_category` is required for US/LOCAL, and it is case-sensitive.** Omitting it fails with code 001. Accepted values are exactly `"Checking"` and `"Savings"`, so `"CHECKING"` fails with code 016. Other corridors differ, so check the schema per corridor.
14. **PER_TRANSACTION limits are not cumulative.** `retrieve_card_limits` shows `remaining` equal to the full limit for PER_TRANSACTION intervals because each transaction is evaluated independently. To track cumulative spend, add an ALL_TIME or MONTHLY limit alongside the per-transaction one.
15. **The USDC-to-Airwallex bridge is always simulated.** This applies to the optional stablecoin extension under Concept 1. Testnet USDC has no monetary value, so nothing converts it to fiat and nothing wires USD into Airwallex, whichever wallet you use. The x402 payment is real on Base Sepolia; the USDC-to-USD conversion and the wire are modeled in your application; the USD arrival is simulated via `simulate_create_deposit`. Teams should say plainly which legs are live and which are modeled. Do not plan around Coinbase Business: onboarding takes weeks and would consume most of the build period.
16. **Sandbox is not throttled the way production is.** Sandbox keys carry wildcard permissions with rate limiting disabled, so no backoff or retry path gets exercised. Production enforces rate limits. Build and test your retry handling deliberately, because sandbox will not force you to.
17. **Per-transaction card limits are inclusive.** On a $100 per-transaction limit, $100.00 clears and $100.01 fails with `LIMIT_EXCEEDED`. An amount equal to the limit passes the control check and goes on to the funding check.
18. **Cards and transfers use different terminal states.** A declined card transaction ends in `FAILED` with a `failure_reason`. A failed transfer ends in `CANCELLED`, never `FAILED`. Do not share one state machine between them. Card decline reasons seen in sandbox: `INSUFFICIENT_FUNDS`, `LIMIT_EXCEEDED`, `CURRENCY_NOT_ALLOWED`.
19. **The dispute sandbox runs Visa, and pre-chargeback disputes never reach you.** Mastercard reason codes are rejected outright. A dispute created at `PRE_CHARGEBACK` comes back `ACCEPTED` in the same second with `RDR_AUTO_ACCEPTED`, so build at `RFI` or `CHARGEBACK`. Challenging is REST-only, because the MCP tool omits `request_id`, `product_type` and evidence documents.
20. **Evidence files upload to `files.sandbox.airwallex.com`, and PNG is refused.** The same path on `api.sandbox.airwallex.com` returns a deprecation message. The docs list PNG as supported but the API rejects it with `File type png is not supported`. JPG and PDF work.
21. **A bad IBAN is caught at beneficiary creation, not at payment.** A checksum-invalid IBAN fails with code `083`, and a checksum-valid but fictional one is accepted, so bad bank details can never be the reason a payment fails. Use `POST /api/v1/beneficiaries/validate` to check a payload without creating anything; it works on connected accounts too. If you want a payment that dies after sending, drive it to SENT and then `FAILED` with a `failure_type` instead.
22. **SWIFT payouts charge a flat fee; local payouts are free.** This holds on the platform account and on a connected account, so it is not a Scale quirk. EUR SWIFT transfers of 100, 1,000, 2,000 and 4,000 each cost a flat EUR 12.85, while USD and GBP LOCAL transfers cost 0. The fee is not proportional. The transfer object spells it out in `fee_amount`, `fee_currency`, `fee_paid_by` (`PAYER`), `amount_payer_pays` and `amount_beneficiary_receives`. This matters any time you size a conversion against a payout: convert exactly what the beneficiary receives and the transfer fails for insufficient funds. Budget the fee before you convert.
23. **Bank routing codes must be real. Invented ones fail with a bare code `010`.** This catches people out because everything else about a beneficiary can be synthetic. Account numbers, names and addresses are never checked, but the routing code is validated against an actual bank registry. US ABA `021000021` and `026073150` are accepted while `123456789` and `999999999` are rejected; UK sort codes `200000` (Barclays) and `040004` (Monzo) are accepted while `601613`, `123456` and `010203` are rejected. Use a real routing code with a made-up account number. The error names only the field, so if you invented the number you will get no hint that the routing code is the problem. Confirmed for US and GB; expect other corridors to validate their own formats the same way.
24. **Connected accounts have their own rules.** See the [shared setup section](#connected-accounts-shared-setup-for-concepts-5-to-8) before building Concepts 5 through 8. The EIN trap in particular will cost you an account if you miss it.

---

## Concept 1: Adaptive Treasury Controller

An agent for a founder or treasury lead whose cash cannot fund every near-term obligation. Five obligations in three currencies fall due over 72 hours. The agent decides what to fund, convert, defer or escalate, tightens its own execution limit when forecast confidence drops, and recalculates when a simulated Global Account deposit arrives.

### Primary Airwallex actions

**Book an FX conversion and pay a supplier.** The conversion is REST only: `POST /api/v1/fx/conversions/create`. See the [REST helper](#rest-helper-for-fx-conversion). The transfer is either interface.

### API endpoints

| Step | Action | MCP tool | REST endpoint | Doc |
|------|--------|----------|---------------|-----|
| 1 | Check wallet balances | `get_account_balances` | `GET /api/v1/balances/current` | [Balances](https://www.airwallex.com/docs/api/core_resources/balances/current) |
| 2 | List global accounts | `list_global_accounts` | `GET /api/v1/global_accounts` | [Global Accounts](https://www.airwallex.com/docs/api/core_resources/global_accounts/list) |
| 3 | Simulate Global Account deposit | `simulate_create_deposit` | `POST /api/v1/simulation/deposit/create` | [Deposit simulation](https://www.airwallex.com/docs/api/simulation/deposits/create) |
| 4 | Check updated balances | `get_account_balances` | `GET /api/v1/balances/current` | [Balances](https://www.airwallex.com/docs/api/core_resources/balances/current) |
| 5 | Get current FX rate | `get_fx_rate` | `GET /api/v1/fx/rates/current` | [FX rates](https://www.airwallex.com/docs/api/transactional_fx/rates/current) |
| 6 | Create FX quote (lock rate) | `create_fx_quote` | `POST /api/v1/fx/quotes/create` | [FX quotes](https://www.airwallex.com/docs/api/transactional_fx/quotes/create) |
| 7 | Book FX conversion | -- | `POST /api/v1/fx/conversions/create` | [Conversions](https://www.airwallex.com/docs/api/transactional_fx/conversion/create) |
| 8 | Get beneficiary field requirements | `get_beneficiary_schema` | `POST /api/v1/beneficiaries/schema` | [Beneficiary schema](https://www.airwallex.com/docs/api/payouts/beneficiaries/schema) |
| 9 | Create supplier as beneficiary | `create_beneficiary` | `POST /api/v1/beneficiaries/create` | [Beneficiaries](https://www.airwallex.com/docs/api/payouts/beneficiaries/create) |
| 10 | Create transfer | `create_transfer` | `POST /api/v1/transfers/create` | [Transfers](https://www.airwallex.com/docs/api/payouts/transfers/create) |
| 11 | Simulate transfer to SENT | `simulate_transfer_result` | `POST /api/v1/simulation/transfers/{id}/transition` | [Transfer simulation](https://www.airwallex.com/docs/api/simulation/transfers/transition) |
| 12 | Simulate transfer to PAID | `simulate_transfer_result` | `POST /api/v1/simulation/transfers/{id}/transition` | [Transfer simulation](https://www.airwallex.com/docs/api/simulation/transfers/transition) |
| 13 | Verify final balances and transfer state | `get_account_balances`, `list_transfers` | `GET /api/v1/balances/current`, `GET /api/v1/transfers` | [Transfers](https://www.airwallex.com/docs/api/payouts/transfers/list) |

### Simulation endpoints

| Simulation endpoint | MCP tool | Purpose |
|---------------------|----------|---------|
| `POST /api/v1/simulation/deposit/create` | `simulate_create_deposit` | Stage the expected deposit that triggers the replan |
| `POST /api/v1/simulation/transfers/{id}/transition` | `simulate_transfer_result` | Advance the priority transfer through SENT to PAID |

### Quickstart sequence

1. `get_account_balances` -- check balances across your chosen currencies
2. `list_global_accounts` -- find the Global Account ID for the deposit simulation. If the account has none, create one first with `POST /api/v1/global_accounts/create` and `required_features: [{"currency":"USD","transfer_method":"LOCAL"}]`.
3. Build the initial allocation plan in your application: rank obligations by deadline, penalty and business impact
4. Drop forecast confidence on one receipt and show your policy module tighten the execution limit
5. `simulate_create_deposit` with `global_account_id` and `amount` -- the deposit lands
6. `get_account_balances` -- confirm it posted, then recalculate and show which decisions survived
7. `get_fx_rate` with `buy_currency` and `sell_currency` -- check the rate
8. `create_fx_quote` with `validity: "MIN_15"` -- lock it
9. REST `POST /api/v1/fx/conversions/create` with `quote_id` and `request_id` -- book the conversion
10. `get_beneficiary_schema`, then `create_beneficiary` with the fields it names
11. `create_transfer` with `beneficiary_id`, `source_currency`, `transfer_currency`, `transfer_amount`, `transfer_method`, `reason`, `reference`, `request_id`
12. Wait about 3 seconds for SCHEDULED to auto-advance, then `simulate_transfer_result` with `next_status: "SENT"`, then again with `"PAID"`
13. `get_account_balances` and `list_transfers` -- verify funded obligations, deferred items and the preserved reserve

### Constraints and gotchas

- The conversion endpoint is REST-only. No MCP tool books a conversion.
- Leave `x-api-version` off the FX calls entirely. The header is not the problem, the value is: `2023-09-30` returns `incorrect_version` on both the rate and conversion endpoints, while `2024-06-30` works on conversions. Neither call needs the header, so omit it rather than track which values are current.
- **The 10-to-50-character `request_id` rule belongs to transfers, not to FX.** `transfers/create` rejects 5, 9 and 51 characters with code 018 and `params: {length_max: 50, length_min: 10}`, while `fx/conversions/create` accepted a single character and only failed at around 120. A UUID is 36 characters and is safe everywhere, so just use one. Duplicates within 7 days fail separately with `duplicate_request_id`.
- The rate and quote responses expose the two tiers differently, and mixing them up gives you `undefined`. A quote response has top-level `client_rate` (what you get) and `awx_rate` (Airwallex internal). A rate response has neither: it has top-level `rate`, which equals the client rate, plus a `rate_details` array holding one entry with `level: "CLIENT"` and one with `level: "AWX"`. Use `client_rate` off a quote and `rate` off a rate check.
- FX quotes are `MULTI_USE`. The same `quote_id` with different `request_id` values books multiple conversions. Discard the `quote_id` after one successful conversion.
- You can skip the quote entirely. Omit `quote_id` and Airwallex converts at spot. The trade-off is no rate lock during the agent's decision window.
- Conversion settles immediately in sandbox. There is no pending state to poll.
- **If you pay the supplier by SWIFT, convert more than the invoice.** A SWIFT payout costs a flat fee of EUR 12.85 on top of what the beneficiary receives, taken from the same wallet. Convert the invoice amount exactly and the transfer fails for insufficient funds. Local payouts are free. See constraint 22 on SWIFT fees.
- Always call `get_beneficiary_schema` before creating a beneficiary. Required fields vary by country, currency and transfer method.
- **Use a real bank routing code with a made-up account number.** An invented ABA or sort code is rejected with a bare code `010` and no explanation. See constraint 23 on routing codes. `021000021` works for US ABA and `200000` for a UK sort code.
- You must simulate SENT before PAID. Jumping straight to PAID from PROCESSING does not work.
- After the deposit simulation, re-check balances before recalculating. The balance reflects the deposit immediately despite the PENDING status in the response.
- Solo builders may reduce to three obligations across two currencies, per the build requirements.

### Optional stablecoin extension

Take one inbound receipt as an x402 payment instead of a bank deposit. The rest of the concept is unchanged.

- **Use Coinbase Developer Platform, not Coinbase Business.** Coinbase Business has no sandbox at all. CDP has one, and you create the wallet from server code with no browser extension and no seed phrase, which suits an agent better than a consumer wallet. Authenticate with `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET` and `CDP_WALLET_SECRET`.
- **Two faucets, and you want both.** [Circle's testnet faucet](https://faucet.circle.com/) gives 20 USDC every two hours per address per chain, needs no account and no sign-in, and covers Base Sepolia. Paste an address into the form. That is your volume source, and it is roughly twenty times what the CDP faucet gives you. Circle points teams needing more at their Discord.
- **Use the CDP faucet when you want it scripted.** Smaller, but it runs from code, which suits a repeatable test setup. Using `@coinbase/cdp-sdk`:

  ```ts
  const cdp = new CdpClient();
  const account = await cdp.evm.getOrCreateAccount({ name: "your-agent" });
  await cdp.evm.requestFaucet({ address: account.address, network: "base-sepolia", token: "usdc" });
  ```

  Each claim delivered exactly 1.00 USDC and returned a transaction hash. A second claim in the same run also succeeded, and Coinbase documents the ceiling as 10 claims per 24 hours. The same call with `token: "eth"` funds gas on Base Sepolia. You need that gas call whichever faucet you use for USDC.
- **Size the demo to the faucet.** Circle's 20 USDC every two hours is the practical ceiling, so single-digit dollars per call works and a handful of calls in a demo is comfortable. Do not price the endpoint at the tens or hundreds a real API would charge, because your own demo has to pay for itself.
- **The bridge to Airwallex is always modeled.** Testnet USDC has no monetary value, so nothing converts it to fiat. The x402 payment is live on Base Sepolia. The conversion and the wire exist only in your application logic. The USD arrival is simulated via `simulate_create_deposit`. Say which legs are live and which are modeled, in the demo and in the README.
- **Deduct fees before simulating the deposit.** A wire fee reduces what arrives. Simulate the net amount, not the gross USDC.
- **Do not convert or pay out until the arrival is confirmed.** Call `get_account_balances` after the deposit simulation and verify the balance before booking the conversion.
- x402 amounts are denominated in USDC. It pegs at par, but verify the amount at each hop rather than assuming it survives the chain unchanged.
- Teams that skip the chain entirely can simulate the USDC receipt with synthetic data and go straight to the deposit simulation. The Airwallex actions are identical either way.
- A working reference implementation is at [raildesk.danjkim.workers.dev](https://raildesk.danjkim.workers.dev): an x402 and Airwallex dual-rail demo with HTTP Message Signatures auth and spend caps.

Two things below come from Coinbase's own documentation rather than from a call against the sandbox, so treat their quickstarts as the source of truth. The x402 testnet is Base Sepolia (`eip155:84532`), and the CDP SDK selects it when you set `environment: "development"`. The seller and buyer middleware packages differ by framework, and we did not run them. Start from the [CDP x402 docs](https://docs.cdp.coinbase.com/x402/welcome) and the [faucet guide](https://docs.cdp.coinbase.com/faucets/introduction).

---

## Concept 2: Intent-Bound Purchase Agent

An agent for a founder or procurement operator evaluating a SaaS purchase. It compares annual vs. monthly pricing against cash reserves and budget policy, then creates a virtual card with per-transaction spending limits. You demonstrate both a declined over-limit authorization and an approved one.

### Primary Airwallex action

**Create a virtual card with spending controls.** Use MCP `create_card`. REST also worked with a standard key, but only tested on a Scale-enabled account, so treat MCP as the reliable path. The card enforces per-transaction spending limits through `authorization_controls`.

### API endpoints

| Step | Action | MCP tool | REST endpoint | Doc |
|------|--------|----------|---------------|-----|
| 1 | List existing cardholders | `list_cardholders` | `GET /api/v1/issuing/cardholders` | [Cardholders](https://www.airwallex.com/docs/api/issuing/cardholders/list) |
| 2 | Create cardholder | `create_cardholder` | `POST /api/v1/issuing/cardholders/create` | [Cardholders](https://www.airwallex.com/docs/api/issuing/cardholders/create) |
| 3 | Poll cardholder until READY | `list_cardholders` | `GET /api/v1/issuing/cardholders/{id}` | [Cardholders](https://www.airwallex.com/docs/api/issuing/cardholders/list) |
| 4 | Create virtual card with limits | `create_card` | `POST /api/v1/issuing/cards/create` (see constraint 6 on card creation) | [Cards](https://www.airwallex.com/docs/api/issuing/cards/create) |
| 5 | Retrieve card details | `retrieve_card` | `GET /api/v1/issuing/cards/{card_id}` | [Cards](https://www.airwallex.com/docs/api/issuing/cards/details) |
| 6 | Retrieve card limits | `retrieve_card_limits` | `GET /api/v1/issuing/cards/{card_id}/limits` | [Card limits](https://www.airwallex.com/docs/api/issuing/cards/limits) |
| 7 | Simulate declined transaction (over limit) | `simulate_create_issuing_transaction` | `POST /api/v1/simulation/issuing/create` | [Issuing simulation](https://www.airwallex.com/docs/api/simulation/issuing/create) |
| 8 | Simulate approved transaction (under limit) | `simulate_create_issuing_transaction` | `POST /api/v1/simulation/issuing/create` | [Issuing simulation](https://www.airwallex.com/docs/api/simulation/issuing/create) |
| 9 | List transactions | `list_issuing_transactions` | `GET /api/v1/issuing/transactions` | [Transactions](https://www.airwallex.com/docs/api/issuing/transactions/list) |

### Simulation endpoints

| Simulation endpoint | MCP tool | Purpose |
|---------------------|----------|---------|
| `POST /api/v1/simulation/issuing/create` | `simulate_create_issuing_transaction` | Create a card authorization (declined or approved) |
| `POST /api/v1/simulation/issuing/{transaction_id}/capture` | `simulate_capture_issuing_transaction` | Capture a pending auth (skip if using `single_phase: true`) |
| `POST /api/v1/simulation/issuing/{transaction_id}/reverse` | `simulate_reverse_issuing_transaction` | Reverse a pending auth. The transaction becomes `REVERSED`. |
| `POST /api/v1/simulation/issuing/refund` | `simulate_refund_issuing_transaction` | Refund a captured transaction. Creates a new `REFUND` transaction with a positive amount. |

### Quickstart sequence

1. `create_cardholder` with `cardholder_type: "DELEGATE"` and an `email`. Send only those two fields, and the cardholder comes back READY immediately, so there is nothing to poll. Use INDIVIDUAL only if you need a personalized card, and note it costs you a name, date of birth, address and an `express_consent_obtained: "yes"`, then returns PENDING.
2. Poll the cardholder until its status is READY. It starts at PENDING and gets there on its own within seconds. Do not call `pass_review`; it returns HTTP 400 `passReviewWithoutKyc`.
3. `create_card` with:
   - `cardholder_id` from step 1
   - `form_factor: "VIRTUAL"`
   - `is_personalized: false`
   - `program: { purpose: "COMMERCIAL" }`
   - `authorization_controls: { allowed_transaction_count: "MULTIPLE", transaction_limits: { limits: [{ amount: 50, interval: "PER_TRANSACTION" }] } }`
   - `created_by: "Your Name"`
   - `request_id`
4. `retrieve_card` -- confirm ACTIVE status
5. `simulate_create_issuing_transaction` with `card_id`, `transaction_amount: 100`, `transaction_currency: "USD"` -- the response comes back with `status: "FAILED"` and `failure_reason: "LIMIT_EXCEEDED"`
6. `simulate_create_issuing_transaction` with `card_id`, `transaction_amount: 30`, `transaction_currency: "USD"`, `single_phase: true` -- the response comes back `status: "PENDING"`, and the transaction settles to `status: "APPROVED"` with `transaction_type: "CLEARING"` a few seconds later. Do not assert on the immediate response; list the transaction and check for APPROVED.
7. `retrieve_card_limits` -- note that PER_TRANSACTION remaining always equals the full limit because each transaction is evaluated independently. Add an ALL_TIME limit if you want to track cumulative spend.

### Constraints and gotchas

- The cardholder must reach READY status before you can create a card. It starts PENDING and promotes itself within seconds, so poll for it. The `pass_review` simulation endpoint returns HTTP 400 `passReviewWithoutKyc` and does not help.
- Virtual cards activate automatically. `activate_card` is only for physical cards. A new card may read PENDING for a moment before it reaches ACTIVE; wait for ACTIVE before simulating against it.
- Using `single_phase: true` on the simulation combines authorization and clearing in one call. Without it, you get a PENDING transaction and must capture it in a separate call.
- **Card controls are enforced at the rail, not by your code.** The decline arrives as `status: "FAILED"` with a `failure_reason`:

| Control | Set via | Violating authorization declines with |
|---|---|---|
| Per-transaction amount | `transaction_limits.limits[]` interval `PER_TRANSACTION` | `LIMIT_EXCEEDED` |
| Cumulative amount | same array, interval `ALL_TIME` / `DAILY` / `WEEKLY` / `MONTHLY` | `LIMIT_EXCEEDED` |
| Currency allowlist | `allowed_currencies` | `CURRENCY_NOT_ALLOWED` |
| Merchant category allowlist | `allowed_merchant_categories` | `MERCHANT_CATEGORY_NOT_ALLOWED` |
| Time window | `active_to` (ISO datetime) | `OUT_OF_ALLOWED_TIME_RANGE` |
| Single use | `allowed_transaction_count: "SINGLE"` | `CARD_CLOSED` on the second authorization |
| Funding | the wallet behind the card | `INSUFFICIENT_FUNDS` |

- **Freeze, unfreeze and cancel work through `POST /api/v1/issuing/cards/{id}/update`** with `card_status` of `INACTIVE`, `ACTIVE` or `CLOSED`. Verified end to end: frozen declines with `CARD_INACTIVE`, unfreezing restores approvals, and `CLOSED` declines with `CARD_CLOSED` and is terminal. This is the cleanest way to demonstrate an agent revoking its own authority mid-demo.
- You must include a `limits` array in the `transaction_limits` object. `cash_withdrawal_limits` is also required in `authorization_controls` when updating card limits.
- **The cardholder consent fields will reject you twice if you guess.** Creating a cardholder over REST needs `individual.express_consent_obtained: "yes"`, and `individual.cardholder_agreement_terms_consent_obtained` must be left out entirely. Include the second one and you get "Must be null for non-CA cardholder". Omit the first and you get "Must be 'yes' to confirm consent given from cardholder". Set one, not both.
- **There are two status vocabularies for the same card transaction, and which one you see depends on how you read it.**
  - The simulation tools and the REST transaction read return `PENDING`, `APPROVED`, `FAILED` and `REVERSED`.
  - MCP `list_issuing_transactions` returns `AUTHORIZED`, `CLEARED`, `DECLINED` and `REVERSED` for those same records.
  - A $450 over-limit decline reads as `FAILED` from the simulation and `DECLINED` from the MCP list. A cleared $89 reads as `APPROVED` from REST and `CLEARED` from MCP.
  - Neither vocabulary uses the word the other does, so pick one read path per code path and match against that. `failure_reason` is stable across both and is the safer thing to branch on.

---

## Concept 3: Payment Operations Incident Commander

An agent for a finance operations lead handling a transfer that hasn't reached its expected state. The supplier says no payment received, and the deadline has arrived. The agent rejects unsafe retries, cancels the original, creates a replacement, and advances it to PAID.

### Primary Airwallex action

**Transfer lifecycle management.** MCP: `create_transfer` + `simulate_transfer_result`. The agent creates transfers and controls their status progression through simulation.

### API endpoints

| Step | Action | MCP tool | REST endpoint | Doc |
|------|--------|----------|---------------|-----|
| 1 | Get beneficiary field requirements | `get_beneficiary_schema` | `POST /api/v1/beneficiaries/schema` | [Beneficiary schema](https://www.airwallex.com/docs/api/payouts/beneficiaries/schema) |
| 2 | Create beneficiary | `create_beneficiary` | `POST /api/v1/beneficiaries/create` | [Beneficiaries](https://www.airwallex.com/docs/api/payouts/beneficiaries/create) |
| 3 | Create original transfer | `create_transfer` | `POST /api/v1/transfers/create` | [Transfers](https://www.airwallex.com/docs/api/payouts/transfers/create) |
| 4 | Simulate cancel original | `simulate_transfer_result` | `POST /api/v1/simulation/transfers/{id}/transition` | [Transfer simulation](https://www.airwallex.com/docs/api/simulation/transfers/transition) |
| 5 | Verify original is CANCELLED with a failure type | `list_transfers` | `GET /api/v1/transfers` | [Transfers](https://www.airwallex.com/docs/api/payouts/transfers/list) |
| 6 | Create replacement transfer | `create_transfer` | `POST /api/v1/transfers/create` | [Transfers](https://www.airwallex.com/docs/api/payouts/transfers/create) |
| 7 | Simulate PROCESSING to SENT | `simulate_transfer_result` | `POST /api/v1/simulation/transfers/{id}/transition` | [Transfer simulation](https://www.airwallex.com/docs/api/simulation/transfers/transition) |
| 8 | Simulate SENT to PAID | `simulate_transfer_result` | `POST /api/v1/simulation/transfers/{id}/transition` | [Transfer simulation](https://www.airwallex.com/docs/api/simulation/transfers/transition) |
| 9 | Verify both transfers | `list_transfers` | `GET /api/v1/transfers` | [Transfers](https://www.airwallex.com/docs/api/payouts/transfers/list) |

### Simulation endpoints

| Simulation endpoint | MCP tool | Purpose |
|---------------------|----------|---------|
| `POST /api/v1/simulation/transfers/{id}/transition` | `simulate_transfer_result` | Cancel original, advance replacement through SENT to PAID |

### Quickstart sequence

1. `get_beneficiary_schema` -- learn required fields
2. `create_beneficiary` -- create the supplier beneficiary (include `address` and `bank_account_category` per the schema, and use a real routing code per constraint 23 on routing codes)
3. `create_transfer` with `request_id: "original-001"` -- the transfer that will get stuck
4. Advance the original with `next_status: "SENT"`, then send `next_status: "FAILED"` with `failure_type: "INVALID_ACCOUNT_NAME_OR_NUMBER"` -- this models a bank return, which is a better story than a cancellation
5. `list_transfers` -- confirm the original shows CANCELLED with the failure detail in `failure_reason`
6. `create_transfer` with `request_id: "replacement-001"` (same beneficiary, same amount) -- the replacement
7. `simulate_transfer_result` with `next_status: "SENT"` -- advance replacement
8. `simulate_transfer_result` with `next_status: "PAID"` -- complete replacement
9. `list_transfers` -- confirm: original CANCELLED, replacement PAID

### Constraints and gotchas

- **Cancellation is not time-sensitive.** A transfer sitting in PROCESSING for 10 seconds cancelled without complaint, so you do not need to act within the first few seconds. Build the flow you want.
- **Prefer the bank-return route for the demo.** Sending the transfer and then failing it with a chosen `failure_type` produces a specific `failure_reason`, which gives your agent something real to reason about. A bare cancellation only ever yields `failure_reason: "simulation"`.
- **The status name will mislead you.** CANCELLED here does not mean a person cancelled the payment. Airwallex uses it as the terminal state for a payment that did not complete, for any reason. Name your internal states accordingly so your own logs stay readable.
- Two transfers to the same beneficiary for the same amount must have different `request_id` values. Same `request_id` in a 7-day window is treated as a duplicate.
- If the original and the replacement both go by SWIFT, each one costs a flat fee. The replacement is not free just because the first one failed, so a wallet sized for one payment may not cover two. See constraint 22 on SWIFT fees.
- PAID doesn't mean the incident is closed. Keep monitoring after the replacement reaches PAID, as the Builder Inspiration Guide describes for this concept.

---

## Concept 4: Dispute Response Agent

An agent for a finance operations lead at an online merchant working a queue of chargebacks. Each case has a deadline, a disputed amount and a fee structure that makes fighting some cases worse than losing them. The agent decides accept or challenge on the economics, assembles evidence for the ones worth fighting, escalates the rest, and replans when the issuer rejects its evidence.

Every call in this section was run against the sandbox, including the full dispute lifecycle end to end.

### Primary Airwallex action

**Accept or challenge a payment dispute.** Accepting produces a real refund against the original payment. Challenging submits evidence to the issuing bank.

### API endpoints

| Step | Action | MCP tool | REST endpoint | Doc |
|------|--------|----------|---------------|-----|
| 1 | Find payments to dispute | `list_payment_intents` | `GET /api/v1/pa/payment_intents` | [PaymentIntents](https://www.airwallex.com/docs/api/payments/payment_intents/list) |
| 2 | Create a dispute | `simulate_create_payment_dispute` | `POST /api/v1/simulation/pa/payment_disputes/create` | [Dispute simulation](https://www.airwallex.com/docs/api/simulation/payments/create_payment_disputes) |
| 3 | List and read disputes | `list_payment_disputes` | `GET /api/v1/pa/payment_disputes` | [Disputes](https://www.airwallex.com/docs/api/payments/payment_disputes/list) |
| 4 | Upload evidence | -- | `POST /api/v1/files/upload` on `files.sandbox.airwallex.com` | [File upload](https://www.airwallex.com/docs/api/supporting_services/file_service/upload_files) |
| 5 | Challenge a dispute | -- | `POST /api/v1/pa/payment_disputes/{id}/challenge` | [Challenge](https://www.airwallex.com/docs/api/payments/payment_disputes/challenge) |
| 6 | Accept a dispute | `accept_payment_dispute` | `POST /api/v1/pa/payment_disputes/{id}/accept` | [Accept](https://www.airwallex.com/docs/api/payments/payment_disputes/accept) |
| 7 | Issuer rejects the evidence | `simulate_escalate_payment_dispute` | `POST /api/v1/simulation/pa/payment_disputes/{id}/escalate` | [Escalate](https://www.airwallex.com/docs/api/simulation/payments/escalate_payment_disputes) |
| 8 | Issuer decides the case | -- | `POST /api/v1/simulation/pa/payment_disputes/{id}/resolve` | [Resolve](https://www.airwallex.com/docs/api/simulation/payments/resolve_payment_disputes) |
| 9 | Verify the refund | `list_refunds` | `GET /api/v1/pa/refunds` | [Refunds](https://www.airwallex.com/docs/api/payments/refunds/list) |

### Verified state machine

| Step | Call | Result |
|---|---|---|
| 1 | create at `RFI` | `RFI` / `REQUIRES_RESPONSE`, mode `COLLABORATION` |
| 2 | `challenge` | `RFI` / `CHALLENGED` |
| 3 | simulate `escalate` | `CHARGEBACK` / `REQUIRES_RESPONSE`, new `due_at` |
| 4 | `challenge` again | `CHARGEBACK` / `CHALLENGED` |
| 5 | simulate `resolve` with `in_favor_of: MERCHANT` | `CHARGEBACK` / `WON` |

Step 3 is the one your demo is built around. The issuer rejects the evidence, the case lands one stage higher, back in `REQUIRES_RESPONSE`, with a fresh deadline and worse economics.

### Quickstart sequence

1. `list_payment_intents` with `status: "SUCCEEDED"` -- pick three payments to dispute. If your account has none, mint your own: `POST /api/v1/pa/payment_intents/create` then `POST /api/v1/pa/payment_intents/{id}/confirm` with a test card in the `payment_method` body. that raw card details are accepted server-side in sandbox and the intent reaches `SUCCEEDED` within seconds, so this concept needs no prior payment history.
2. `simulate_create_payment_dispute` three times with `payment_intent_id`, `stage: "RFI"`, a Visa `reason_code` and `due_at`
3. `list_payment_disputes` -- read amounts, reason codes, stages and deadlines
4. Score each case in your application: disputed amount, evidence strength, chargeback fee, delegated limit
5. `accept_payment_dispute` on the low-value one with `reason: "LOW_VALUE_TRANSACTION"` and a `refund_reason` -- this returns a refund object
6. Upload evidence to `files.sandbox.airwallex.com` and keep the `file_id`
7. REST `POST /api/v1/pa/payment_disputes/{id}/challenge` on the strong case with `request_id`, `reason`, `delivery_info`, `customer_info` and `supporting_documents`
8. `simulate_escalate_payment_dispute` with a new `due_at` -- the issuer rejects your evidence and the case moves to `CHARGEBACK`
9. Recalculate. The chargeback fee is now committed and the evidence you already sent has failed once.
10. Challenge again with different evidence, this time including `product_type`
11. `POST /api/v1/simulation/pa/payment_disputes/{id}/resolve` with `in_favor_of: "MERCHANT"` -- the case reaches `WON`
12. `list_refunds` -- confirm the refund from step 5 settled

### Constraints and gotchas

- **The sandbox processor is Visa.** Mastercard reason codes are rejected. Sending `4837` returns `No matching reason found for 4837 null for VISA (via CARD_SIMULATOR)`. Working codes include `10.4` (other fraud, card absent), `13.1` (merchandise not received) and `13.6` (credit not processed).
- **Do not build on the `PRE_CHARGEBACK` stage.** A dispute created there comes back `ACCEPTED` in the same second with `accept_details.reason: "RDR_AUTO_ACCEPTED"` and `accepted_by: "AIRWALLEX"`. Visa Rapid Dispute Resolution auto-accepts it and the merchant never sees it. A later challenge fails with `invalid_status_for_operation`. Create at `RFI` or `CHARGEBACK`.
- **The MCP challenge tool cannot complete a challenge.** `challenge_payment_dispute` exposes only `dispute_id`, `reason` and `challenged_by`. A real challenge also needs `request_id`, at least one evidence document, and `product_type` when the stage is `CHARGEBACK`. Missing them returns `Valid product_type must be provided` and then `Please attach at least one recommended evidence types to proceed with response`. Use REST for challenge. MCP is fine for accept.
- **Evidence files upload to a different host.** Use `https://files.sandbox.airwallex.com/api/v1/files/upload`. The same path on `api.sandbox.airwallex.com` returns `api deprecated!use the same url in 'files.airwallex.com'`.
- **PNG is refused despite the docs listing it.** Attaching a PNG returns `validation_error | File type png is not supported`. JPG and PDF both upload, and JPG is accepted as challenge evidence.
- On a successful challenge Airwallex adds its own `Evidence_Provided_Form...pdf` to `supporting_documents.generated_files`. That is normal.
- **Escalation works from `RFI`, not from `CHARGEBACK`.** Escalating a chargeback returns `validation_error | Dispute transition is not supported`.
- Escalate and resolve simulate the issuing bank, so they are only valid after the merchant has responded. Calling either while the status is `REQUIRES_RESPONSE` fails. Accept and challenge are the only valid moves at that point.
- `product_type` accepts `PHYSICAL_GOODS`, `DIGITAL_PRODUCT_OR_SERVICE`, `OFFLINE_SERVICE`, `TRAVEL`, `RESERVE_OR_BOOKING` and `OTHERS`.
- Challenge `reason` and accept `reason` are typed enums. Use them as the agent's decision output rather than free text, because they are auditable. Accept reasons include `LOW_VALUE_TRANSACTION`, which is the economic accept case exactly.
- Accepting at `RFI` triggers a refund and records who did it. A verified example returned refund `rfd_...`, 28 USD, status `ACCEPTED`, `accepted_by` set to the agent identifier passed in.

### A complete challenge call that works

Upload a JPG first, keep the `file_id`, then send this to `POST /api/v1/pa/payment_disputes/{id}/challenge`. This works at both the RFI and chargeback stages. `product_type` is required at `CHARGEBACK` and ignored at `RFI`.

```json
{
  "request_id": "chg-1f0c9a7e-4b21-4d3e-9c88-7a1e5d2b0c44",
  "challenged_by": "dispute-agent-01",
  "reason": "PURCHASE_HISTORY",
  "product_type": "PHYSICAL_GOODS",
  "product_description": "Wireless keyboard, qty 1",
  "customer_info": {
    "name": "Jordan Avery",
    "email": "jordan.avery@example.com",
    "ip": "203.0.113.44",
    "device_id": "59ec5db9-399c-4043-9a6c-fcd1a48d99aa",
    "billing_address": "1460 Mission St, San Francisco, CA 94103, US"
  },
  "delivery_info": {
    "shipped_at": "2026-08-05T10:00:00.000Z",
    "delivered_at": "2026-08-08T14:22:00.000Z",
    "shipping_company": "USPS",
    "tracking_number": "9400111899223197428490"
  },
  "supporting_documents": {
    "documents": [
      {
        "type": "PRIMARY",
        "description": "Signed delivery confirmation",
        "file_ids": ["<file_id from the upload call>"]
      }
    ]
  }
}
```

The `customer_info` fields are not decoration. Visa's compelling-evidence rules want two prior undisputed transactions sharing at least two identifiers with the disputed one, and `ip` or `device_id` must be one of them. Meet those rules and `PURCHASE_HISTORY` becomes a defensible challenge reason rather than a guess.

---

## Connected accounts: shared setup for Concepts 5 to 8

Concepts 5 through 8 all act on behalf of customers who hold their own connected accounts. The setup below is the same for all four, so build it once.

### Creating and activating a connected account

| Step | Action | REST endpoint | Notes |
|------|--------|---------------|-------|
| 1 | Create the account | `POST /api/v1/accounts/create` | `account_details` is **required**, though `{}` satisfies it. Put as much as you can here, including the business person. Returns an `acct_` ID with status CREATED. |
| 2 | Fill any gaps | `POST /api/v1/accounts/{id}/update` | See the required list below. Fields become immutable after submit, and the person does not persist through this endpoint. |
| 3 | Submit for activation | `POST /api/v1/accounts/{id}/submit` | Returns 200. |
| 4 | Activate | `POST /api/v1/simulation/accounts/{id}/update_status` | Body `{"next_status": "ACTIVE", "force": true}`. Wait about 2 seconds after submit. |
| 5 | Confirm | `GET /api/v1/accounts/{id}` | Status ACTIVE. |

Everything below hangs off `account_details`.

Inside `account_details.business_details`:

- `business_name`. At the top level of the request it is ignored.
- `operating_country`, an array such as `["US"]`
- `industry_category_code`, in `ICCV3_XXXXXX` format. These are **not** MCC codes. Fetch valid values from `GET /api/v1/reference/industry_categories`, which returns categories grouped as `{name, values:[{value: "ICCV3_0806XX"}]}`.
- `account_usage.product_reference`, an array such as `["RECEIVE_TRANSFERS", "MAKE_TRANSFERS", "CONVERT_FUNDS"]`
- `account_usage.estimated_monthly_revenue` as `{"currency": "USD", "amount": 50000}`
- `description_of_goods_or_services`, 1 to 500 characters
- `registration_address` and `business_address`
- `business_identifiers` with an EIN
- `business_structure`, `business_start_date`, `state_of_incorporation`, `contact_number`

Sibling of `business_details`, at `account_details.business_person_details`:

- An array of people, each with `first_name`, `last_name`, `roles`, `date_of_birth`, `nationality`, `residential_address` and `identifications`

```json
"identifications": { "primary": {
  "identification_type": "PASSPORT",
  "issuing_country_code": "US",
  "passport": { "number": "X1234567", "effective_at": "2020-01-01", "expire_at": "2030-01-01" }
}}
```

`identification_type` accepts `PASSPORT`, `DRIVERS_LICENCE`, `PERSONAL_ID` or `TAX_ID`, and the nested object is named after the type you chose.

### A complete create call that works

This exact body created three accounts that submitted, activated and paid. Copy it, change the names, and you skip every trap above. Note `business_person_details` sitting beside `business_details` rather than inside it, and the addresses using `address_line1` and `suburb`.

```json
{
  "primary_contact": { "email": "ops@employer-a.example.com" },
  "customer_agreements": {
    "agreed_to_data_usage": true,
    "agreed_to_terms_and_conditions": true
  },
  "account_details": {
    "legal_entity_type": "BUSINESS",
    "business_details": {
      "business_name": "Employer A GmbH",
      "business_structure": "CORPORATION",
      "industry_category_code": "ICCV3_0806XX",
      "operating_country": ["US"],
      "business_start_date": "2021-03-15",
      "state_of_incorporation": "CA",
      "contact_number": "+14155550142",
      "description_of_goods_or_services": "Software and professional services",
      "url": "https://employer-a.example.com",
      "business_identifiers": [
        { "type": "EIN", "number": "98-1234567", "country_code": "US" }
      ],
      "account_usage": {
        "product_reference": ["RECEIVE_TRANSFERS", "MAKE_TRANSFERS", "CONVERT_FUNDS"],
        "estimated_monthly_revenue": { "currency": "USD", "amount": 50000 }
      },
      "registration_address": {
        "country_code": "US", "state": "CA", "suburb": "San Francisco",
        "address_line1": "500 Howard St", "postcode": "94105"
      },
      "business_address": {
        "country_code": "US", "state": "CA", "suburb": "San Francisco",
        "address_line1": "500 Howard St", "postcode": "94105"
      }
    },
    "business_person_details": [
      {
        "first_name": "Robin", "last_name": "Hale",
        "roles": ["AUTHORISED_PERSON", "BENEFICIAL_OWNER", "DIRECTOR"],
        "date_of_birth": "1985-04-12",
        "nationality": "US",
        "email": "robin.hale@employer-a.example.com",
        "residential_address": {
          "country_code": "US", "state": "CA", "suburb": "San Francisco",
          "address_line1": "500 Howard St", "postcode": "94105"
        },
        "identifications": {
          "primary": {
            "identification_type": "PASSPORT",
            "issuing_country_code": "US",
            "passport": {
              "number": "X1234567",
              "effective_at": "2020-01-01",
              "expire_at": "2030-01-01"
            }
          }
        }
      }
    ]
  }
}
```

Then `POST /api/v1/accounts/{id}/submit` with an empty body, wait about two seconds, and `POST /api/v1/simulation/accounts/{id}/update_status` with `{"next_status": "ACTIVE", "force": true}`. Give it another minute or two before your first FX call.

### Acting on behalf of a customer

Send `x-on-behalf-of: {acct_id}` on an ordinary banking call and it runs as that customer.

| Action | Works on behalf of? | Notes |
|---|---|---|
| Read balances | Yes | `GET /api/v1/balances/current` |
| Create beneficiary | Yes | `bank_account_category` is case-sensitive |
| Validate beneficiary | Yes | `POST /api/v1/beneficiaries/validate`, no object created |
| Create transfer | Yes | Needs an EIN on the account |
| FX quote and conversion | Yes, once ACTIVE | `forbidden` while the account is CREATED. `unconfigured_client_fee` for the first minutes after activation. See constraint below. |
| Create Global Account | Yes | `required_features` is an array of objects |
| Simulate a deposit | Yes | The header is mandatory here |
| Issue a card | Yes | Card spend draws on that customer's wallet |
| Simulate a transfer status | Yes, with the header | Without it you get `bad_request: Can't find payment ... associated with`. |

### Platform-level money movement

| Action | REST endpoint | Notes |
|---|---|---|
| Platform to customer | `POST /api/v1/connected_account_transfers/create` | Settles instantly. Body takes `destination`, `amount`, `currency`, `reason`, `reference`, `request_id`. |
| Customer to platform | `POST /api/v1/charges/create` | Settles instantly. Body takes `source`, `amount`, `currency`, `reason`, `reference`, `request_id`. |
| Portfolio report | `POST /api/v1/platform_reports/create` | `file_format: "CSV"` is required and appears in almost no doc example. Returns a `download_url`. |

### Constraints and gotchas

- **Set `business_identifiers` (EIN) before submitting.** Without it, on-behalf-of transfers fail in an unrecoverable loop: no `payer` returns error 001, and supplying `payer` returns error 048. The payer auto-populates from the business details once an EIN exists. Accounts cannot be edited after submission, so a missed EIN means a dead account. The EIN gates payments, not the business person. An account with no business persons still submits, activates and pays.
- **FX on behalf of a customer is gated on account status, not on pricing configuration.** A `CREATED` account returns `forbidden`. For roughly one to four minutes after activation it returns `unconfigured_client_fee`. After that it works. Every `ACTIVE` account passes and every `CREATED` account fails, regardless of how old it is. Activate the account, do your other setup while it settles, then make your first FX call. A few minutes is normal. Do not treat `unconfigured_client_fee` as a missing capability. If it is still failing well after activation, ask in the hackathon Slack support channel, because at that point it is worth a look.
- **Two different address shapes exist in this API, and the wrong one fails silently.** Account addresses (`registration_address`, `business_address`, `residential_address`) use `address_line1` and `suburb`. Beneficiary addresses use `street_address` and `city`. Send the beneficiary shape to an account address and the fields are dropped with no error: a full address came back stored as country, state and postcode only.
- **Put the business person in the `accounts/create` call.** Supplied there it persists and returns a `person_id`. Supplied through `accounts/{id}/update` it silently stored nothing on three attempts, including with the fully correct schema.
- **`simulation/issuing/create` needs `x-on-behalf-of` for a customer's card, and lies when you omit it.** Without the header it returns HTTP 400 `bad_request: "Transaction could not be created"` **and creates the transaction anyway**: the transaction count rises even on the call that reports failure. An agent that retries on that error double-charges the customer. With the header you get a normal `PENDING` or `FAILED` response carrying `failure_reason`.
- **Transfer simulation works on a customer's payment, but only with `x-on-behalf-of`.** Without the header you get `bad_request: "Can't find payment ... associated with"`, because the platform is looking in its own transfer list. With it, SENT, PAID and FAILED all behave exactly as they do on the platform account.
- **Customer payments do not advance on their own.** They stay in PROCESSING indefinitely, for days. You must drive them with the simulation endpoint. Do not write an executor that polls and waits, because nothing will happen.
- **You cannot make a customer payment fail on bad bank details.** Airwallex validates the IBAN when you create the beneficiary, not when you send the money. A checksum-invalid IBAN is rejected at creation with code `083` on `beneficiary.bank_details.iban`. A checksum-valid but fictional IBAN is accepted and resolves a bank name, so it will not fail either. Combined with the line above, there is no route to a bad-details payment failure. Validate the payload and hold the payment instead.
- **`POST /api/v1/beneficiaries/validate` is the clean way to check.** It accepts `x-on-behalf-of` and returns the same `083` error without creating anything. This is the REST counterpart to the MCP `dry_run` flag, which cannot be used here because MCP tools authenticate as the platform and cannot send the header.
- **An overdrawn wallet is your reliable exception, and there are three different spellings of it.** On the same account:

  | Call | Code on insufficient balance |
  |---|---|
  | `transfers/create` | `balance_insufficient` |
  | `charges/create` | `insufficient_fund` (singular) |
  | `fx/conversions/create` | `insufficient_funds` (plural) |

  All three are deterministic and easy to stage. Match on the code the endpoint actually returns rather than assuming one name covers all of them.
- **SWIFT payouts charge a flat fee; local payouts are free.** See constraint 22. It bites hardest on batch payouts, where converting the headline total leaves the last payment short.
- **`required_features` for Global Accounts is an array of objects**, `[{currency, transfer_method}]`, not an array of strings. Strings return "not of the expected type."
- **Deposit simulation into a customer's Global Account needs `x-on-behalf-of`.** Without it the endpoint cannot find the account.
- **`bank_account_category` is case-sensitive.** `"Checking"` works, `"CHECKING"` fails with code 016.
- **Routing codes must be real.** See constraint 23. It applies to customer beneficiaries exactly as it does to your own.
- **GB/GBP/LOCAL wants `bank_name`, and the routing type is lowercase.** `bank_name` is required for GB but not for US. `account_routing_type1` must be `sort_code`; `SORT_CODE` fails with code 011. The IBAN plus SWIFT route works for GBP with no routing fields at all.
- **Transfer field names.** `POST /api/v1/transfers/create` wants `transfer_currency` and `transfer_amount`. Sending `payment_currency` or `payment_amount` returns code 001 on `transfer_currency`. Beneficiary creation wants `transfer_methods`, not `payment_methods`.
- **MCP tools cannot act on behalf of a customer.** They authenticate as the platform and cannot send the header, so customer balances and transactions never appear through them. Call REST directly for anything customer-scoped.
- **Reading a customer's ledger.** Use `GET /api/v1/transfers`, `GET /api/v1/balances/current` and `GET /api/v1/balances/history` with the header. The history window caps at 7 days.
- Platform Liquidity Programs are out of scope. No concept needs one, every PLP route is admin-key-only, and none is provisioned. Do not design around one.

---

## Concept 5: Platform Spend Controller

A corporate card platform. The platform funds each customer's wallet and issues cards against it, then decides who to fund when capital is scarce.

### Primary Airwallex action

**Fund a customer wallet and issue a card on its behalf.** Card spend draws on that customer's wallet, so the funding decision has real consequences.

### Sequence

1. Create, populate, submit and activate three connected accounts. See the shared setup above.
2. `POST /api/v1/connected_account_transfers/create` -- fund each wallet from the platform.
3. Create a cardholder and a card for each business with `x-on-behalf-of`. MCP is the easier path for cards on the platform's own account, but customer-scoped issuing needs REST plus the header.
4. `POST /api/v1/simulation/issuing/create` -- build month-to-date spend.
5. Run the allocator: wallet balances, card limits, pending authorizations, platform balance, reserve floor. Return fund, defer or deny per customer.
6. Fund one customer, defer the other, and record the release condition.
7. Run three authorizations against the new card to produce three different decline reasons.
8. `POST /api/v1/charges/create` -- collect the monthly fee from each customer, and skip the one whose wallet cannot cover it.

### Constraints and gotchas

- **Per-transaction limits are inclusive.** On the platform account with a $100 limit, $100.00 cleared and $100.01 failed with `LIMIT_EXCEEDED`. On a card issued on behalf of a connected account with a $5,000 limit and a $3,000 wallet, a $5,000 charge failed with `INSUFFICIENT_FUNDS` rather than `LIMIT_EXCEEDED`, which is exactly the distinction this concept exists to make. Card spend drew the customer wallet from $3,000 to $1,000, confirming the wallet is the funding source.
- The three decline reasons you want are `INSUFFICIENT_FUNDS`, `LIMIT_EXCEEDED` and `CURRENCY_NOT_ALLOWED`. All three come back with the same status and differ only in `failure_reason`. Read the reason, not the status.
- Set `allowed_currencies` to a single currency to produce `CURRENCY_NOT_ALLOWED`. Verified with a EUR charge against a USD-only card.
- **`PER_TRANSACTION` limits are not cumulative.** `retrieve_card_limits` reports `remaining` equal to the full limit, because each transaction is evaluated on its own. Add an `ALL_TIME` or `MONTHLY` limit alongside it to track cumulative spend.
- Limits go in `authorization_controls.transaction_limits.limits`, not `spend_limits`.
- **Cardholders and cards promote themselves.** A new cardholder returns PENDING and a new card may return PENDING, but both reach READY and ACTIVE within seconds with no call. Do not call `pass_review`; it returns HTTP 400 `passReviewWithoutKyc` on standard and connected accounts alike. Poll instead, and wait for ACTIVE before simulating a transaction.
- A declined card transaction ends in a status called `FAILED` with a `failure_reason`. A failed transfer ends in `CANCELLED`. Do not share a state machine between them.

---

## Concept 6: Multi-Employer Payroll Executor

A contractor payment platform that batches payroll across several employers, converting currency for each and surviving partial failure.

### Primary Airwallex action

**Convert currency and send payments on behalf of each employer**, then collect a fee from each.

### Sequence

1. Create and activate three connected accounts. Fund two of them.
2. `GET /api/v1/balances/current` with the header for each employer -- check funding against payroll totals.
3. `POST /api/v1/beneficiaries/validate` with the header for every contractor -- validate before committing anything.
4. `POST /api/v1/beneficiaries/create` with the header for the ones that pass.
5. FX quote and conversion with the header for employers paying in a currency they do not hold.
6. `POST /api/v1/transfers/create` with the header -- send each payment.
7. `POST /api/v1/simulation/deposit/create` with the header -- the late employer's deposit arrives mid-batch.
8. Replan, run the newly funded employer, and poll every payment to terminal state.
9. `POST /api/v1/charges/create` -- collect the fee from each employer.

### Constraints and gotchas

- **Price the payroll before you convert it.** Each SWIFT payout adds a flat fee of EUR 12.85 that comes out of the same wallet. Converting the headline payroll total leaves you short on the last contractor. In the guide's worked example, Employer A converts EUR 7,525.70 to pay EUR 7,500 across two SWIFT contractors. Your own numbers will differ; the fee behaviour will not. Local payouts are free, so an employer paying only USD or GBP locally needs no fee headroom.
- **One employer's balance is never available to another.** Step 5 turns on this. A $5,000 deposit into Employer C's wallet leaves Employer B's balance untouched at $6,075. If your agent sums balances across tenants it will report a funded batch that cannot be paid.
- **Validate before you send.** A bad IBAN is rejected at beneficiary creation, not at payment, so validation is the only way to catch bad contractor details. See the shared constraints.
- **Drive your payments to terminal state; they will not get there alone.** Send `x-on-behalf-of` on the simulation call. A customer payment left alone stays in PROCESSING forever.
- **You can also stage a realistic mid-batch failure.** Take a payment to SENT, then send `next_status: "FAILED"` with a `failure_type`, and it lands in CANCELLED with a real `failure_reason`. That gives you a second, richer exception alongside the validation catch if you want one.
- FX on behalf of an employer needs that employer's account to be ACTIVE, plus a few minutes of settling time before conversion comes online. See the shared constraints. Activate all three employers at the start of your run and the problem disappears. If conversion is still refused well after activation, ask in the hackathon Slack support channel.
- For a second exception, let one employer's wallet run dry partway through its batch and catch `balance_insufficient`.
- Fees are ordinary charges. Check the wallet balance before charging, rather than firing and catching.

---

## Concept 7: Portfolio Lending Agent

A revenue-based financing platform. It disburses advances into borrower wallets, collects a share of weekly revenue, and sizes new advances against a reserve floor.

### Primary Airwallex action

**Collect repayments from borrower wallets and disburse a new advance**, then report on the portfolio.

### Sequence

1. Create and activate three connected accounts.
2. `POST /api/v1/simulation/deposit/create` with the header -- stage each borrower's weekly revenue into their Global Account.
3. `GET /api/v1/balances/current` with the header -- read actual revenue rather than expected revenue.
4. `POST /api/v1/charges/create` -- collect each borrower's contracted share.
5. Apply the reserve floor to find the disbursement ceiling, then size the new advance underneath it.
6. `POST /api/v1/connected_account_transfers/create` -- disburse to the new borrower.
7. `POST /api/v1/platform_reports/create` with `file_format: "CSV"` -- snapshot the portfolio.

### Constraints and gotchas

- Each borrower needs a Global Account before you can simulate revenue into it. `required_features` is an array of objects.
- Collect from actual balance, not from the expected figure. The gap between them is the whole decision.
- `platform_reports` requires `file_format`. Omit it and the call fails with no useful hint. The report goes PENDING then COMPLETED quickly and returns a `download_url`.
- The reserve floor should bind. If the full advance still fits above the floor, the agent is not making a decision, it is expressing a preference. Set the floor so the approved amount cannot go out in full.
- Collecting more than a borrower's wallet holds fails with insufficient funds. Check first and carry a receivable.

---

## Concept 8: Marketplace Settlement Agent

A marketplace that collects buyer money centrally and settles it out to seller connected accounts, holding a reserve against refund risk in between.

### Primary Airwallex action

**Settle to seller wallets with a per-seller reserve, then recover from a wallet when refunds exceed what was held.** This is the only concept where charges do the product work rather than collecting a fee.

### Sequence

1. Create and activate three seller connected accounts.
2. `POST /api/v1/simulation/deposit/create` -- stage the settlement pool into the platform's own Global Account. No header here; the pool is the platform's money.
3. Calculate a reserve per seller from its trailing refund rate.
4. `POST /api/v1/connected_account_transfers/create` -- pay each seller its net.
5. New risk evidence arrives on one seller. Recalculate that seller's reserve and leave the others alone.
6. `GET /api/v1/balances/current` with the header -- confirm each seller wallet.
7. Refunds settle. Release the unused reserve with a second transfer, or recover a shortfall with `POST /api/v1/charges/create`.
8. `POST /api/v1/platform_reports/create` -- produce a settlement report that reconciles per seller.

### Constraints and gotchas

- Keep the pool and the reserves in your own ledger. Airwallex has no held-balance or escrow primitive, so a reserve is money the platform simply has not sent yet.
- The reconciliation is the deliverable. Each seller's payouts plus refunds plus released reserve must equal what they were owed, and the platform's remaining balance must equal the sum of unreleased reserves.
- Recovering from a seller wallet that cannot cover the amount fails with `insufficient_fund`. Check the balance and carry a receivable rather than firing the call and catching the error.
- Two transfers to the same seller in one cycle need different `request_id` values. Same id inside 7 days is treated as a duplicate.
- Do not reopen reserves the new evidence did not touch. That restraint is the thing judges will look for.

---

## Doc links index

All documentation URLs referenced in this document, alphabetized.

- [API reference introduction](https://www.airwallex.com/docs/api/introduction)
- [Balances: current](https://www.airwallex.com/docs/api/core_resources/balances/current)
- [Beneficiaries: create](https://www.airwallex.com/docs/api/payouts/beneficiaries/create)
- [Beneficiaries: schema](https://www.airwallex.com/docs/api/payouts/beneficiaries/schema)
- [Cards: create](https://www.airwallex.com/docs/api/issuing/cards/create)
- [Cards: details](https://www.airwallex.com/docs/api/issuing/cards/details)
- [Cards: limits](https://www.airwallex.com/docs/api/issuing/cards/limits)
- [Cardholders: create](https://www.airwallex.com/docs/api/issuing/cardholders/create)
- [Cardholders: list](https://www.airwallex.com/docs/api/issuing/cardholders/list)
- [Coinbase x402 MCP payments](https://docs.cdp.coinbase.com/x402/buyer/mcp-payments)
- [Conversions: create](https://www.airwallex.com/docs/api/transactional_fx/conversion/create)
- [Developer MCP](https://www.airwallex.com/docs/developer-tools/ai/developer-connector)
- [Disputes: accept](https://www.airwallex.com/docs/api/payments/payment_disputes/accept)
- [Disputes: challenge](https://www.airwallex.com/docs/api/payments/payment_disputes/challenge)
- [Disputes: list](https://www.airwallex.com/docs/api/payments/payment_disputes/list)
- [Dispute simulation: create](https://www.airwallex.com/docs/api/simulation/payments/create_payment_disputes)
- [Dispute simulation: escalate](https://www.airwallex.com/docs/api/simulation/payments/escalate_payment_disputes)
- [Dispute simulation: resolve](https://www.airwallex.com/docs/api/simulation/payments/resolve_payment_disputes)
- [File upload](https://www.airwallex.com/docs/api/supporting_services/file_service/upload_files)
- [Industry categories reference](https://www.airwallex.com/docs/api/scale/reference/industry_categories)
- [PaymentIntents: list](https://www.airwallex.com/docs/api/payments/payment_intents/list)
- [Refunds: list](https://www.airwallex.com/docs/api/payments/refunds/list)
- [FX guide: create a conversion](https://www.airwallex.com/docs/transactional-fx/get-started/create-a-conversion)
- [FX quotes: create](https://www.airwallex.com/docs/api/transactional_fx/quotes/create)
- [FX rates: current](https://www.airwallex.com/docs/api/transactional_fx/rates/current)
- [Global Accounts: list](https://www.airwallex.com/docs/api/core_resources/global_accounts/list)
- [Issuing simulation: capture](https://www.airwallex.com/docs/api/simulation/issuing/capture)
- [Issuing simulation: create](https://www.airwallex.com/docs/api/simulation/issuing/create)
- [Issuing simulation: pass review](https://www.airwallex.com/docs/api/simulation/issuing/pass_review_cardholders) (documented, but returns HTTP 400 in sandbox; see constraint 7 on cardholder status)
- [Issuing simulation: refund](https://www.airwallex.com/docs/api/simulation/issuing/refund)
- [Issuing simulation: reverse](https://www.airwallex.com/docs/api/simulation/issuing/reverse)
- [Issuing transactions: list](https://www.airwallex.com/docs/api/issuing/transactions/list)
- [Sandbox environment](https://www.airwallex.com/docs/developer-tools/sandbox-environment)
- [Simulate deposits to Global Account](https://www.airwallex.com/docs/accounts/receive-funds/receive-bank-transfers-to-global-accounts/simulate-deposits-to-your-global-account)
- [Simulate card transactions](https://www.airwallex.com/docs/issuing/transactions/simulate-transactions-on-issued-cards)
- [Simulate transfer status](https://www.airwallex.com/docs/payouts/transfers/create-a-transfer/simulate-transfer-status-transition)
- [Transfer simulation: transition](https://www.airwallex.com/docs/api/simulation/transfers/transition)
- [Transfers: create](https://www.airwallex.com/docs/api/payouts/transfers/create)
- [Transfers: list](https://www.airwallex.com/docs/api/payouts/transfers/list)
- [x402 protocol specification](https://www.x402.org)
