Alipay mobile web
Use alipay_wap for checkout in a mobile website. The customer continues through Alipay's mobile checkout and your return page checks the result. For a native Android or iOS app, use Alipay App.
1. Prepare the mobile flow
Have this target entry activated for your application before using it. Register a checkout return destination with NextAppMarket and use its assigned return_url_id. Preserve the customer's authenticated business order across navigation away from your website.
The platform's target contract supports hosted browser instructions. The precise invocation kind is returned by the activated adapter. Handle the discriminator explicitly rather than treating an HTML form as a URL. Availability is documented in the release notes.
Setup checklist
- Serve your checkout over HTTPS in a mobile browser. Hosted mobile checkout does not require a native SDK. If the purchase begins in your native app, use the Alipay App tutorial for the SDK integration.
- Activate
alipay_wapand register the return destination. Keep the assignedreturn_url_idon your backend. The published guide describes target behavior; this adapter still needs activation before live requests. - Implement the app-owned checkout/status endpoints from Connect your app to checkout, and copy checkout-browser.mjs into your frontend source.
- Prepare both a redirect handler and the form handler below if your activated adapter returns
html_form. Your checkout backend validates the provider destination. The app never constructs provider signing parameters. - Prepare an authenticated return page and a Check payment action. If return opens a browser without the original session, sign in again and load the owned order from your backend; do not grant access from return parameters.
Checkpoint: the Pay button can call your backend, the response identifies the selected invocation kind, and your return page can display an unresolved order without initiating another payment.
2. Create the order on your backend
Authenticate the customer, calculate the price, and save the business order and payment attempt as described in Build your payment backend. Save the following body as payment.json, replacing the application and external order IDs:
{
"app_id": "app_demo",
"payment_entry": "alipay_wap",
"amount_minor": 1000,
"currency": "CNY",
"external_order_id": "shop_order_1042",
"description": "100 app credits",
"scene": {
"return_url_id": "return_checkout"
}
}
curl --fail-with-body --request POST \
'https://pay.appconnect.cn/payment/v1/payment-sessions' \
--header "Authorization: Bearer $PAYMENT_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: create-shop-order-1042-v1' \
--data-binary @payment.json
Use the key and exact body persisted for this attempt. Save data.order.id before returning the business order ID and data.invocation to the client. The Payment API key stays on your server.
If the invocation is null, keep the same order and retrieve its payment session. Do not create another order while the first result is unresolved.
3. Open mobile checkout
Check expires_at before presenting the invocation. For redirect_url, navigate to the exact returned HTTPS URL from a top-level browser page:
function openAlipayMobile(invocation) {
const expires = Date.parse(invocation.expires_at);
if (!Number.isFinite(expires) || expires <= Date.now()) throw new Error('Payment instructions expired');
if (invocation.kind !== 'redirect_url') throw new Error('Expected redirect instructions');
const url = new URL(invocation.redirect_url);
if (url.protocol !== 'https:') throw new Error('Invalid cashier URL');
window.location.assign(invocation.redirect_url);
}
Your backend validates the provider destination before forwarding it. Keep signed parameters unchanged. Avoid embedding this checkout in an arbitrary app WebView; use the browser behavior validated for your activated integration.
If the adapter returns html_form
Render a form only from an authenticated Payment API invocation, through your own authenticated checkout endpoint. The following browser handler parses the returned document without inserting its markup, accepts a single POST form to the approved production gateway, and rebuilds only hidden fields and your own Continue button. It does not execute scripts from the response.
function mountAlipayForm(invocation, container) {
if (invocation.kind !== 'html_form') throw new Error('Expected a payment form');
const expires = Date.parse(invocation.expires_at);
if (!Number.isFinite(expires) || expires <= Date.now()) throw new Error('Payment form expired');
const parsed = new DOMParser().parseFromString(invocation.html_form, 'text/html');
const forms = parsed.querySelectorAll('form');
if (forms.length !== 1) throw new Error('Unexpected payment form structure');
const source = forms[0];
const action = new URL(source.getAttribute('action'));
if (action.username || action.password || action.origin !== 'https://openapi.alipay.com' || action.pathname !== '/gateway.do' ||
(source.getAttribute('method') || '').toLowerCase() !== 'post') {
throw new Error('Unexpected payment form destination');
}
if (source.querySelector('select, textarea, button, iframe, object')) throw new Error('Unexpected form controls');
const form = document.createElement('form');
form.method = 'POST';
form.action = source.getAttribute('action');
form.acceptCharset = 'UTF-8';
const names = new Set();
for (const input of source.querySelectorAll('input')) {
if (input.type !== 'hidden' || !input.name || names.has(input.name)) throw new Error('Unexpected payment field');
names.add(input.name);
const field = document.createElement('input');
field.type = 'hidden'; field.name = input.name; field.value = input.value;
form.appendChild(field);
}
if (!names.size) throw new Error('Empty payment form');
const button = document.createElement('button');
button.type = 'submit'; button.textContent = 'Continue to Alipay';
form.appendChild(button);
form.addEventListener('submit', event => {
if (Date.now() >= expires) {
event.preventDefault();
container.textContent = 'Payment form expired. Check the existing order.';
}
});
container.replaceChildren(form);
}
Put this handler on a dedicated checkout page. Set its CSP form-action to the approved provider gateway and allow only your own application scripts. Treat the allowed origin/path as activation configuration: if the platform introduces another destination, review and update it deliberately. Never broaden it to any user-supplied URL.
The form's original hidden values must reach the provider unchanged. Do not calculate new signatures, rename parameters, trim values, or append client-controlled fields. A structure the handler does not understand should fail visibly and retain the order for support; do not fall back to raw innerHTML rendering.
Wire the Pay button to this handler
Use these elements on the checkout page. The backend renders the CSRF token using your normal session protection. The cart ID below is illustrative; use the cart owned by the signed-in customer.
<meta name="csrf-token" content="SERVER_RENDERED_TOKEN">
<button id="pay" type="button">Pay</button>
<button id="check-payment" type="button">Check payment</button>
<p id="payment-status" role="status"></p>
<div id="provider-form"></div>
Copy this into your frontend module alongside the handler above:
import { checkoutRequest } from './checkout-browser.mjs';
const payButton = document.getElementById('pay');
const status = document.getElementById('payment-status');
payButton.addEventListener('click', async () => {
payButton.disabled = true;
try {
const checkout = await checkoutRequest('/api/checkout', {
method: 'POST',
csrfToken: document.querySelector('meta[name="csrf-token"]').content,
body: { cartId: 'cart_1042', paymentEntry: 'alipay_wap' },
});
sessionStorage.setItem('pendingBusinessOrderId', checkout.businessOrderId);
if (checkout.paymentStatus === 'paid') {
window.location.assign('/orders/' + encodeURIComponent(checkout.businessOrderId));
return;
}
if (!['pending', 'processing'].includes(checkout.paymentStatus) || !checkout.invocation) {
status.textContent = 'Payment is not ready. Check the existing order.';
return;
}
if (checkout.invocation.kind === 'html_form') {
mountAlipayForm(checkout.invocation, document.getElementById('provider-form'));
} else {
openAlipayMobile(checkout.invocation);
}
} catch {
status.textContent = 'Unable to open payment. Your order can be checked before retrying.';
} finally { payButton.disabled = false; }
});
The /orders/… path is your application's order page. POST /api/checkout must reuse an unresolved attempt, so another tap cannot silently create a duplicate charge. Your backend supplies any entry-specific scene fields from trusted configuration.
Connect the Check payment button in the same module:
document.getElementById('check-payment').addEventListener('click', () => {
const id = sessionStorage.getItem('pendingBusinessOrderId');
if (id) window.location.assign('/orders/' + encodeURIComponent(id));
else status.textContent = 'Sign in and open this purchase from your order history.';
});
Build the return page
Use the complete return/status page example with the saved business order. The Check payment action and order history must work even if the provider does not navigate back. The return URL transports the customer; your backend transports the financial truth.
4. Confirm payment and deliver the purchase
When the customer returns, ask your backend for the business order's status. Your backend checks ownership and retrieves the saved platform order:
curl --fail-with-body \
'https://pay.appconnect.cn/payment/v1/orders/ord_demo_001' \
--header "Authorization: Bearer $PAYMENT_API_KEY"
Validate the returned identity, currency, and amount. Fulfill only after the authoritative status is paid, using a unique fulfillment record. A callback or a return to your page is a reason to check this order, not a reason to grant the purchase.
Implement webhooks and order reconciliation so confirmation works even when the customer closes the app or browser. Show a pending state for unresolved results and offer Check payment instead of immediately starting another charge.
5. Refund or close the order
For a paid order, follow Refund a payment to submit a full or partial refund and track it to completion. Use the saved platform order ID; your client does not refund through the provider SDK.
For an abandoned unpaid order, use the close-order flow. Removing the payment UI does not close the order. Run the integration checklist before release.
6. Test mobile checkout
Test each mobile browser you support with Alipay installed and absent. Check redirects or form submission, app switching, browser Back, cancellation, and a return to a different tab. Record the invocation kind delivered by your activated adapter and test that branch with its actual response.
7. Common problems and how to fix them
| Symptom | Check and fix |
|---|---|
| Entry is unavailable | Confirm alipay_wap has been implemented and activated for your application. A target tutorial does not enable a channel. Continue local frontend tests with fixtures while activation is pending. |
| Expected a URL but received HTML | Read invocation.kind. Use the dedicated form handler for html_form; never navigate to the literal HTML string. |
| Continue button is missing | Check form-parser validation, the expected POST method/action, and hidden input names. Keep the order and report an unsupported adapter form; do not insert unfiltered HTML as a fallback. |
| Browser blocks form submission | Check CSP form-action, the HTTPS gateway allowlist, and whether your page is sandboxed without form permissions. Keep only the approved destination. |
| Signature fails after form submission | Preserve the decoded hidden field values and normal form encoding. Do not trim, rename, remove, or add signed parameters. Do not copy a form between orders. |
| Alipay will not open inside WeChat | Direct the user to a supported external browser. Do not treat an embedded browser's blocked app switch as a failed financial payment. |
| Native app WebView gets stuck | Prefer the Alipay App SDK for native checkout. This tutorial targets a mobile browser; a custom WebView needs its own tested scheme/navigation integration. |
| Customer has no Alipay app | Test the browser cashier behavior enabled by your provider configuration; keep the return and status path working. Do not invent a successful payment from an unsuccessful app launch. |
| Return loses the login session | Recover from your authenticated server-side checkout or order history after sign-in. Never trust a return parameter as proof of ownership or payment. |
| Repeated Pay taps reopen the same cashier | This is expected while reusing one unresolved attempt. Query or close it before offering a deliberately new attempt. |
| Refund requested but money is not returned | Read both refund review and execution status. Accepted or approved is not succeeded; track the original refund instead of requesting another. |
These browser checks are adapted from Alipay's mobile web FAQ. For API and lifecycle issues, use shared troubleshooting.