Skip to main content

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​

  1. 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.
  2. Activate alipay_wap and register the return destination. Keep the assigned return_url_id on your backend. The published guide describes target behavior; this adapter still needs activation before live requests.
  3. Implement the app-owned checkout/status endpoints from Connect your app to checkout, and copy checkout-browser.mjs into your frontend source.
  4. 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.
  5. 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​

SymptomCheck and fix
Entry is unavailableConfirm 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 HTMLRead invocation.kind. Use the dedicated form handler for html_form; never navigate to the literal HTML string.
Continue button is missingCheck 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 submissionCheck 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 submissionPreserve 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 WeChatDirect 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 stuckPrefer 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 appTest 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 sessionRecover 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 cashierThis is expected while reusing one unresolved attempt. Query or close it before offering a deliberately new attempt.
Refund requested but money is not returnedRead 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.