Skip to Content
GuidesCheckout handoff

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 needYou do not need
A platform API keyA buyer OAuth bearer
A hosted agent profile + AP2 signing keyA 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 appearsWhat it isUse it to
On a cart / an incomplete checkout bodyA buyer-facing web link to view / continue that cart or checkoutLet the buyer pick up where the agent left off
On a checkout with status: requires_escalationThe 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

Last updated on