Airwallex Billing Hosted Checkoutの統合をサポートするスキルです。定期課金、1回限りの支払い(PAYMENT)、カード情報の保存(SETUP)といった様々なシーン(利用場面)に対応しています。 認証、商品・料金設定、無料トライアル期間、割引コード、完了画面、ウェブフック(サーバー間の自動連携)、トラブル解決、そして環境設定ファイル(.env)を通じたAPI バージョン管理など、幅広い内容をカバーしています。 **次のような場合に使用:** - 請求チェックアウト、定期課金の統合、継続的な課金、無料トライアル、割引コード、プロモーションコード、billing_checkouts API、ホスト型請求ページについて - または「订阅」「周期扣款」「优惠券」などの中国語表記に関するご質問 **注意:** HPP(ホスト型支払いページ)、Hosted Payment Page、PaymentIntent に関するご質問には対応していません。これらはオンライン決済機能に属しており、請求機能ではないためです。
Guide integration of Airwallex Billing Hosted Checkout for subscription, one-off payment (PAYMENT), and card-saving (SETUP) scenarios. Covers authentication, product/price creation, free trial, coupons, success page, webhooks, troubleshooting, and API version via .env. Use when user mentions billing checkout, subscription integration, recurring billing, free trial, coupon, promo code, billing_checkouts API, hosted billing page, 订阅, 周期扣款, 优惠券. Do NOT use for HPP / Hosted Payment Page / PaymentIntent; those belong to Online Payments, not Billing.
This skill guides end-to-end integration of Airwallex Billing Hosted Checkout.
Canonical source: the API steps below distill Hosted Billing Checkout and the related Billing docs. Full mapping in references/api-reference.md. Those pages are authoritative. Prefer them if anything here drifts.
How it differs from HPP (Hosted Payment Page):
| Dimension | HPP (Online Payments) | Billing Hosted Checkout |
|---|---|---|
| Core API | PaymentIntent + redirectToCheckout() |
billing_checkouts/create → returns url |
| Typical use case | One-off payments | Subscriptions / recurring billing / one-off invoices / card-only setup |
| Auto-created resources | None | Customer, Payment Source, Subscription |
Checkout Modes: SUBSCRIPTION · PAYMENT (one-off) · SETUP (card-saving only, requires x-api-version >= 2025-08-29)
billing_checkouts, "hosted billing checkout", "billing checkout"subscription.active, subscription.cancelled, or other Billing WebhooksLanguage: Detect the user's language. Code, API fields, and enum values stay in English; explanations and comments localized.
Interaction first: When intent is unclear, ask questions per §4 before outputting content; never dump all sections at once
Output strategy: Only output sections relevant to the user's needs; use "conclusion → steps" structure
Security: Use <PLACEHOLDER> for sensitive values in code examples
Environments: Demo = api-demo.airwallex.com, Production = api.airwallex.com
API versioning: Minimum x-api-version per capability and the current latest are in api-reference.md §2. Set AIRWALLEX_X_API_VERSION in .env (template api-reference.md §1.1); examples below read that value and do not hardcode a date, so nobody assumes one fixed version is mandatory. If you rely on the account default and omit the header, confirm the default meets the §2 minimums for what you use.
If the user's first message is already specific (e.g. "SUBSCRIPTION + 14-day trial + 20% off coupon"), skip known questions and jump straight to the relevant path.
Q1: What's your business scenario?
- (A) Subscription / recurring billing →
SUBSCRIPTION, continue to Round 2- (B) One-off invoice payment →
PAYMENT, skip to Round 3- (C) Save card only, no immediate charge →
SETUP, skip to Round 3- (D) Not sure → Follow-up: "Will customers repeatedly purchase the same product/service on a schedule? Yes → A; No → B"
- (E) Already integrated, need to troubleshoot → Jump to §8 Troubleshooting
Q2: Free trial? Yes → Ask for number of days; No → Skip
Q3: Coupon / discount? Yes → Ask for type (percentage
PERCENTAGE/ fixed amountFLAT) and duration (ONCE/CUSTOM/INDEFINITELY); No → SkipQ4: Pricing model? Fixed
FLAT/ Per-unitPER_UNIT/ TieredVOLUME(unit price by total-quantity tier) orGRADUATED(unit price changes as quantity grows) / Not sure → SuggestFLATUsage-based billing is not a
pricing_model. Setmetered: trueplus ameter_idon the Price, then report usage via the Ingest Usage Events API.
Q5: Tech stack? Frontend + backend language
Q6: Currency and region?
Q7: Do you have an Airwallex account and Product/Price?
- Have both → Skip Step 1
- Have account but no Product/Price → Start from Step 1
- Have neither → Prompt to sign up and get API key
- Have an existing HPP integration, want to migrate → See api-reference.md §14
| Signal | Output section |
|---|---|
SUBSCRIPTION |
§5 (follow Step order) |
PAYMENT |
§6 |
SETUP |
§6.5 |
| Free trial = Yes | §5 Free trial notes |
| Coupon = Yes | §5 Coupon notes |
| Already has Product/Price | Skip Step 1 |
| Troubleshooting | §8 |
| All paths | End with Webhook + Testing + Production checklist |
| User signal | Recommended alternative |
|---|---|
| One-off online payment, no subscription concept | HPP (Hosted Payment Page) |
| Embed payment components in own page | Embedded Elements |
| Fully custom UI, PCI ROC certified | Native API |
Call POST /api/v1/authentication/login (Headers: x-client-id + x-api-key) to obtain an ACCESS_TOKEN (valid 30 minutes; use the expires_at field as the source of truth). See api-reference.md §1. Recommended: manage credentials via .env (template in api-reference.md §1.1).
These
curlsnippets useAIRWALLEX_X_API_VERSION(same as.env). Export it before running (export AIRWALLEX_X_API_VERSION=...) or load.envwith your tooling; the value must satisfy api-reference.md §2 for the features you use.
curl -X POST https://api-demo.airwallex.com/api/v1/products/create \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Content-Type: application/json' \
-H "x-api-version: ${AIRWALLEX_X_API_VERSION}" \
-d '{ "request_id": "<UUID>", "name": "Basic Plan" }'
curl -X POST https://api-demo.airwallex.com/api/v1/prices/create \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Content-Type: application/json' \
-H "x-api-version: ${AIRWALLEX_X_API_VERSION}" \
-d '{
"request_id": "<UUID>",
"product_id": "<PRODUCT_ID>",
"currency": "USD",
"pricing_model": "FLAT",
"flat_amount": 10,
"recurring": { "period": 1, "period_unit": "MONTH" }
}'
You can also create these via the Web App → Products & Prices UI.
Call POST /api/v1/coupons/create, setting discount_model (PERCENTAGE/FLAT) and duration_type (ONCE/CUSTOM/INDEFINITELY).
Field exclusivity:
FLATusesamount_off+currency;PERCENTAGEusespercentage_offonly. Sendingamount_off/currencyon aPERCENTAGEcoupon returns400 validation_error; don't copycurrencyfrom the price onto the coupon.
curl -X POST https://api-demo.airwallex.com/api/v1/coupons/create \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H 'Content-Type: application/json' \
-H "x-api-version: ${AIRWALLEX_X_API_VERSION}" \
-d '{
"request_id": "<UUID>",
"name": "WELCOME20",
"discount_model": "PERCENTAGE",
"percentage_off": 20,
"duration_type": "ONCE",
"active": true
}'
Note the returned coup_xxx ID for use in Step 3. Full examples (incl. FLAT + custom duration) and the end-to-end coupon → checkout block in api-reference.md §10.
Frontend responsibilities: display plans → collect email → bind unique request_id → POST to backend → redirect to Checkout URL.
async function handleSubscribe(email, priceId) {
const res = await fetch('/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, priceId, request_id: crypto.randomUUID() })
});
const { url, checkout_id } = await res.json();
sessionStorage.setItem('checkout_id', checkout_id);
window.location.href = url;
}
Pre-flight check (ask the user before generating code):
- How many Legal entities are under your Airwallex account? → If more than one,
legal_entity_idis required- How many Payment accounts are linked to your account? → If more than one,
linked_payment_account_idis requiredIf the user is unsure, ask them to check in Web App → Account settings → Payment accounts and Legal entities. A single-entity, single-account setup can omit both fields; otherwise missing them will return
Need to specify the linked_payment_account_id ...or similar 400 errors.
app.post('/create-checkout', async (req, res) => {
const { email, priceId, request_id } = req.body;
const accessToken = await getAccessToken();
const apiVersion = process.env.AIRWALLEX_X_API_VERSION;
if (!apiVersion) {
return res.status(500).json({ error: 'Missing AIRWALLEX_X_API_VERSION in .env — see api-reference.md §1.1' });
}
const resp = await fetch(
'https://api-demo.airwallex.com/api/v1/billing_checkouts/create',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
'x-api-version': apiVersion
},
body: JSON.stringify({
mode: 'SUBSCRIPTION',
// legal_entity_id: '<LEGAL_ENTITY_ID>', // Required for multi-entity accounts
// linked_payment_account_id: '<PAYMENT_ACCOUNT_ID>', // Required for multi-account accounts
customer_data: { email },
line_items: [{ price_id: priceId, quantity: 1 }],
subscription_data: {
// trial_ends_at: new Date(Date.now() + 14*86400e3).toISOString().replace(/\.\d{3}Z$/, '+0000'),
// duration: { period: 12, period_unit: 'MONTH' },
// days_until_due: 5
},
// discounts: [{ type: 'COUPON', coupon: { id: '<COUPON_ID>' } }],
// locale: 'ZH',
request_id,
success_url: 'https://yoursite.com/success',
back_url: 'https://yoursite.com/pricing'
})
}
);
const data = await resp.json();
if (!resp.ok) return res.status(resp.status).json({ error: data.message });
res.json({ url: data.url, checkout_id: data.id });
});
Key fields:
| Field | Description |
|---|---|
mode |
SUBSCRIPTION / PAYMENT / SETUP |
subscription_data |
Required for SUBSCRIPTION (pass {} even if empty, otherwise 400) |
line_items |
[{ price_id, quantity }] |
request_id |
Idempotency key (UUID) |
success_url |
Redirect after successful payment |
back_url |
URL for the back button on the checkout page. Not to be confused with cancel_url, which no Airwallex API has |
discounts |
Checkout-level Coupon |
line_items[].discounts |
Line-item-level Coupon, stackable with Checkout-level |
locale |
Hosted page language (EN/ZH/JA etc.) |
legal_entity_id |
Required when multiple legal entities exist (can omit for single entity) |
linked_payment_account_id |
Required when multiple payment accounts exist (can omit for single account) |
trial_ends_at |
ISO8601 with timezone offset (e.g. +0000) |
For Python / Java / Go backends, the logic is the same, POST JSON to the same endpoint. Python example in api-reference.md §6.1.
After Checkout completes, the customer is redirected to success_url. Use the checkout_id stored in sessionStorage to query GET /api/v1/billing_checkouts/{id} and retrieve the subscription_id. Always treat Webhooks as the authoritative source; polling Checkout status is only for immediate frontend feedback. Detailed implementation in api-reference.md §6.2.
Always rely on Webhooks, not client-side redirects.
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-signature'];
const ts = req.headers['x-timestamp'];
const expected = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(`${ts}${req.body.toString()}`).digest('hex');
// Constant-time compare; guard length first (timingSafeEqual throws on a length mismatch).
const sigBuf = Buffer.from(sig || '', 'hex');
const expectedBuf = Buffer.from(expected, 'hex');
if (sigBuf.length !== expectedBuf.length || !crypto.timingSafeEqual(sigBuf, expectedBuf)) {
return res.sendStatus(401);
}
// Replay protection: reject deliveries whose timestamp is more than 5 minutes old.
// `x-timestamp` is in milliseconds, so compare against Date.now() directly.
if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) return res.sendStatus(401);
const event = JSON.parse(req.body.toString());
switch (event.type) {
case 'subscription.active': /* Grant access */ break;
case 'subscription.in_trial': /* Provide trial features */ break;
case 'subscription.cancelled': /* Revoke access */ break;
case 'subscription.unpaid': /* Notify to update payment */ break;
}
res.sendStatus(200);
});
Note:
express.raw()must be registered beforeexpress.json(), otherwise signature verification will fail. The sample compares signatures withcrypto.timingSafeEqual()(guarding buffer length first) so signature validity is not leaked through response timing; a plainsig !== expectedis timing-attack vulnerable. It also rejects deliveries older than 5 minutes (replay protection) and deduplicating byevent.idis recommended, since the same event can be delivered more than once. Full details in api-reference.md §8.
4035 5010 0000 0008 (any expiry/CVC); do not use 4242…. Full card list in the canonical Test card numbers doc (see api-reference.md §12 for the billing-specific notes)Set via subscription_data.trial_ends_at (ISO8601 with timezone). Trial period is independent of billing cycles. Omit the field to skip trial. Webhook flow: subscription.in_trial → subscription.active.
Create Coupon in Step 1.5 → reference in Step 3 via discounts (Checkout-level) or line_items[].discounts (line-level). Line-level discounts are applied before Checkout-level; Checkout-level discounts are pro-rata distributed across line items. Hosted Checkout discounts are pre-filled via the API; users cannot enter promo codes on the page. If user input is needed, collect the code on your site and pass it in the create call. See api-reference.md §10.
One-off invoice payment. No subscription_data; use invoice_data instead. Creates an Invoice (not a Subscription) on completion. No Free trial. Coupons are supported.
curl -X POST https://api-demo.airwallex.com/api/v1/billing_checkouts/create \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
-H "x-api-version: ${AIRWALLEX_X_API_VERSION}" \
-d '{
"request_id": "<UUID>",
"mode": "PAYMENT",
"line_items": [{ "price_id": "<PRICE_ID>", "quantity": 10 }],
"invoice_data": { "default_tax_percent": 10 },
"success_url": "https://yoursite.com/success",
"back_url": "https://yoursite.com/back"
}'
Verifies and saves a Payment Source without charging. Requires x-api-version >= 2025-08-29. No line_items/subscription_data/invoice_data needed. Returns billing_customer_id + payment_source_id on completion, which can be used with the Subscription API or Invoice API later. See api-reference.md §6.3.
POST /api/v1/subscriptions/{id}/update, replace price_id, set proration_behavior (PRORATED/ALL/NONE)POST /api/v1/subscriptions/{id}/cancel; for end-of-period cancellation, set cancel_at_period_end: true| Symptom | Investigation |
|---|---|
| URL shows error | Checkout expires after 1 hour; recreate |
| No payment methods shown | Verify Billing permissions are active and linked_payment_account_id is correct |
| 400 error | Check source field in response; verify legal_entity_id and environment match |
| Wrong page language | Set locale parameter |
| Symptom | Investigation |
|---|---|
subscription_data must be provided |
SUBSCRIPTION mode requires subscription_data: {} |
resource_not_found on price_id |
Verify via GET /api/v1/prices/list |
request_id conflict |
Idempotent design; use a new UUID |
authentication_error |
Token expires after 30 min (use expires_at), re-login |
cancel_url ignored |
No Airwallex API accepts cancel_url. Billing Checkout uses back_url; HPP uses failUrl and successUrl |
Need to specify the linked_payment_account_id ... |
Account has multiple Payment Accounts. The request must explicitly include linked_payment_account_id; retrieve it from Web App → Account settings → Payment accounts (similarly legal_entity_id corresponds to "Need to specify the legal_entity_id ...") |
| Symptom | Investigation |
|---|---|
| Events not received | Verify URL is configured correctly and publicly accessible |
| Signature failure | Check Secret; confirm express.raw() is used for raw body |
Status stuck at PENDING |
Customer hasn't completed payment; possible risk block |
| Symptom | Investigation |
|---|---|
| Not activated | Check collection_method and Payment Source validity |
| No charge after trial | Verify trial_ends_at format (timezone offset +0000); check auto-charge config |
| Still charging after cancellation | Check cancel_at_period_end; confirm cancellation request succeeded |
| Symptom | Investigation |
|---|---|
| Redemption 400 | Verify coup_xxx exists and active: true; for FLAT type, currency must match |
validation_error: amount_off and currency should not be provided when discount_model is not FLAT |
PERCENTAGE coupons take percentage_off only; remove amount_off/currency (those are FLAT-only). Don't copy currency from the price/product onto the coupon. |
| Amount mismatch | Check applied_discounts in response; verify duration_type and stacking order |
For detailed API schemas, complete code examples, and the production checklist, see api-reference.md.
原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。