Alipay desktop web
Use alipay_page for a desktop web checkout. Your backend creates the order and receives a signed cashier URL. The browser opens Alipay's checkout; your application confirms the result through the platform order.
To present the cashier as a code for another device, follow Alipay QR code. For mobile web checkout, use Alipay mobile web.
1. Prepare checkout and return pages
Have alipay_page enabled. If you need an automatic return, register your return destination with NextAppMarket and keep its assigned return_url_id on your backend. Your return page must recover an authenticated business order and display a pending state while confirmation is in progress.
No native SDK or provider signing key is required in your website. The platform creates the signed alipay.trade.page.pay request used by the hosted checkout.
Setup checklist
- Create an HTTPS checkout page with a Pay with Alipay button and an order-status page. No native SDK or Alipay JavaScript SDK is required for hosted page checkout.
- Activate
alipay_pagefor your platform application. If you use automatic return, supply the destination to NextAppMarket and save its approvedreturn_url_idon your backend. - Implement the application-owned checkout and status endpoints in Connect your app to checkout. Your backend creates the platform session and returns its invocation only after persisting the mapping.
- Copy checkout-browser.mjs into your frontend source. Render your session's CSRF token and the Pay/status elements shown in the browser setup.
- Preserve the business order in your authenticated server-side checkout session and order history. Session storage is useful on the current tab, but cannot be the only recovery record.
Checkpoint: a signed-in customer can create and query their own business order before any browser navigation. Requests for another customer's order are rejected.
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_page",
"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.
The return configuration is optional for this entry. Omit scene if you do not use an approved return destination; customers can still view their order status in your application.
3. Send the browser to the cashier
Expect redirect_url from this entry. On your checkout page:
function openAlipayPage(invocation) {
if (invocation.kind !== 'redirect_url') throw new Error('Unexpected payment instructions');
const expires = Date.parse(invocation.expires_at);
if (!Number.isFinite(expires) || expires <= Date.now()) throw new Error('Payment instructions expired');
const url = new URL(invocation.redirect_url);
if (url.protocol !== 'https:') throw new Error('Invalid cashier URL');
window.location.assign(invocation.redirect_url);
}
Use the response from your authenticated backend, which validates the expected provider destination. Do not rebuild the query string or append a return URL in the browser. Use top-level navigation so the checkout is not dependent on popup permissions.
On the return page, load your saved business order and ask your backend for its status. Treat URL parameters as untrusted navigation data. They do not authorize fulfillment or let a customer read another account's order.
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>
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_page' },
});
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;
}
openAlipayPage(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 desktop checkout
Complete an approved payment through the cashier and verify the returned order status and delivery. Test browser Back, a closed cashier tab, expired instructions, delayed confirmation, and a return page opened without the original session. The backend must finish processing even when no return occurs.
7. Common problems and how to fix them
| Symptom | Check and fix |
|---|---|
| Browser does not navigate | Verify the Pay handler receives redirect_url, its expiry is valid, and the exception handler did not stop the flow. Use top-level navigation instead of a popup opened after an asynchronous request. |
| Cashier refuses to load in an iframe | Use the top-level redirect shown above. Do not depend on provider iframe permission or embedded third-party cookies. |
| Invalid signature or malformed request | Compare the exact URL across API response and browser handoff. Remove extra encoding, tracking parameters, and string reconstruction. |
| No automatic return | Verify the approved return_url_id was supplied by the backend. A customer may close the cashier or return manually; provide order history and a status button. |
| Return page says unpaid although the cashier showed success | Query the original order again. Do not treat return query parameters or a reporting projection as the authoritative result. |
| Customer sees another order on return | Check session-to-business-order mapping, ownership validation, and reuse of global/static order variables. Never accept a URL order ID without an ownership check. |
| Session is missing after return | Recover through sign-in and owned order history. Review your cookie scope and return flow; do not put an API key or customer token in the return URL. |
| Desktop customer wants to scan from a phone | Use the dedicated alipay_qr cashier-link tutorial and keep the original page's status polling. |
| Can I change the amount after session creation? | Create the correct server-priced business attempt before payment. Do not edit signed checkout parameters. Resolve/close the old attempt before a deliberate replacement. |
| Double-click creates two payments | Reuse the unresolved attempt and saved idempotency command on your backend. A disabled button alone is not a server concurrency control. |
For payment/refund states, timeouts, and failed webhooks, follow shared troubleshooting.