Guide: checkout handoff (payment link)
Goal: your service does all the work — search, build the cart, create the checkout — then hands the buyer a hosted payment link to finish paying in their browser. You never touch card data and never need the buyer to OAuth into your app.
Who this is for: concierge bots, “prepare my order” assistants, and any flow where the buyer is not present in an authenticated session with you but will complete payment themselves.
| You need | You do not need |
|---|---|
| A platform API key | A buyer OAuth bearer |
| A hosted agent profile + AP2 signing key | A CIMD document |
| The buyer signed into your app |
This pattern is card-only. The crypto rail is buyer-bound (it needs the buyer’s OAuth bearer and an Agents spend cap), so it cannot be handed off this way — see Shopping agent (SDK) for crypto.
How the handoff works
You complete the checkout on the card rail with no payment token. Because
there is no instrument to charge, the server returns the checkout with
status: requires_escalation and a continue_url — the hosted (Stripe) page
where the buyer enters their card. You forward that URL; the buyer finishes.
Understand which continue_url you mean
continue_url appears on more than one response, so be precise:
| Where it appears | What it is | Use it to |
|---|---|---|
| On a cart / an incomplete checkout body | A buyer-facing web link to view / continue that cart or checkout | Let the buyer pick up where the agent left off |
On a checkout with status: requires_escalation | The hosted payment page (3-DS / card entry) | Hand off payment — this is the link in this guide |
| On a completed / cancelled checkout | (omitted) — the session is terminal | — |
So the payment link you forward is specifically the continue_url returned with
a requires_escalation status after you complete on card without a token. A
continue_url on a still-incomplete checkout is just a “continue in browser”
link, not proof of payment.
With the SDK
confirmPurchase surfaces the handoff as the escalation_required outcome:
const outcome = await shop.confirmPurchase({
storeSlug: 'YOUR_STORE_SLUG',
checkoutId: checkout.id as string,
paymentMethod: 'artos.card', // card rail, no token supplied -> hosted handoff
});
if (outcome.status === 'escalation_required') {
// Send this to the buyer (email / chat / SMS). They pay on the hosted page.
await sendPaymentLinkToBuyer(outcome.continueUrl);
}The SDK still mints and sends the AP2 checkout_mandate (your signing key) and
verifies the store’s merchant_authorization before producing the link, so the
handoff is bound to the priced terms.
With raw UCP
Complete on card with no credential.token; read continue_url off the
requires_escalation response:
curl -X POST https://api.artos.sh/s/YOUR_STORE_SLUG/checkout-sessions/CHECKOUT_ID/complete \
-H "Content-Type: application/json" \
-H "Request-Id: $(uuidgen)" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'UCP-Agent: profile="https://your-app.example/.well-known/ucp"' \
-H "X-API-Key: $UCP_PLATFORM_API_KEY" \
-d '{ "payment": { "instruments": [{ "handler_id": "artos.card", "selected": true }] },
"ap2": { "checkout_mandate": "<header>..<signature>" } }'{
"ucp": { "status": "ok" },
"checkout": {
"id": "CHECKOUT_ID",
"status": "requires_escalation",
"continue_url": "https://<merchant-hosted-pay-page>/checkout-sessions/CHECKOUT_ID"
}
}Replace
YOUR_STORE_SLUG,CHECKOUT_ID, and the mandate with live values. See Direct UCP for the full header rules and Checkout for the card/crypto details.
After the buyer pays
The hosted page completes the charge and places the order. Track it with the
store-scoped get_order (your platform key needs the orders:read permission —
see Authentication → scopes), or subscribe to the order.placed
webhook.
Next steps
- Checkout — rails, AP2, coupons,
$0orders - Platform API keys — the credential this guide needs
- Shopping agent (SDK) — complete in-session instead of handing off