Checkout
Checkout is per-store. Build a checkout at one store, pick a payment rail, and complete it. For multiple shops, run one checkout per store sequentially (there is no universal cart).
This page covers both tracks. On Connect the bridge collapses cart → checkout →
mandate → complete into confirm_purchase. On the Build track you drive each
UCP step yourself.
The store slug
energy-sportand ids likecheckout_456in the examples are illustrative — substitute live values from your own search results (metadata.artos_seller.slug) and checkout responses.
Lifecycle
Payment rails
A checkout response advertises every enabled rail under ucp.payment_handlers,
keyed by handler type (sh.artos.card / sh.artos.crypto), each entry carrying
an inner id and inline config. You select a rail by that inner id —
artos.card or artos.crypto — not by the registry key.
| Rail | Registry key | Select with (handler_id / payment_method) | Auth on completion |
|---|---|---|---|
| Card | sh.artos.card | artos.card | Platform key |
| Crypto | sh.artos.crypto | artos.crypto | Buyer bearer only |
Card is tokenized (Stripe pm_…) or a hosted pay-URL handoff; crypto settles
on-chain to the merchant in USDsui and is where coupons apply.
Crypto completion is buyer-bound: send the buyer OAuth bearer only, never the platform
X-API-Key(see Authentication).
Selecting a rail
UCP has no “select payment” operation. The agent picks a rail by completing with
the chosen instrument. If a store offers more than one rail and you complete with
none selected, the server returns the checkout body as a recoverable UCP error
payment_selection_required (HTTP 200, ucp.status: error, checkout
status: incomplete). No mandate is verified or consumed. Read
ucp.payment_handlers, ask the buyer, and retry with the rail selected.
Connect — confirm_purchase
The bridge does the heavy lifting: fetches a fresh quote, verifies the store’s
merchant_authorization against its published signing keys, mints the AP2
mandate, and calls complete_checkout with the chosen rail.
{
"name": "confirm_purchase",
"arguments": {
"checkouts": [{ "store_slug": "energy-sport", "checkout_id": "checkout_456" }],
"payment_method": "artos.crypto",
"payment_mandate_id": "pm_optional_preauth"
}
}checkouts— the checkout to pay, or one per shop (up to 5) to pay them all in one crypto transaction (below).payment_method— the handler id to route to (artos.card/artos.crypto). Omit it only if you want thepayment_selection_requiredprompt.payment_mandate_id— optional; enforces a pre-authorized spending allowance.quote_id— optional, crypto only; binds the purchase to a price locked withquote_purchase(below).- Report totals exactly as returned — do not pre-compute discounts. When a
hosted rail is selected the response is
requires_escalationwith acontinue_urlthe buyer must open; always present that link.
Lock the crypto price first — quote_purchase
A crypto total is paid from the coins in the buyer’s wallet. Only the coins the
store lists in its sh.artos.crypto handler’s accepted_coins count; Artos
accepts only coins with deep liquidity on DeepBook. It picks them in this
order: USDC first (sent to the merchant as is, no conversion), then other
stablecoins such as USDsui, then SUI and the other accepted tokens. It combines
up to three coins in one transaction. Any coin other than USDC is converted to USDC
through DeepBook at a rate that holds for about 60 seconds. A payment made
entirely in USDC has no rate, so its quote holds as long as the checkout and
never changes. When the buyer names a coin (“pay with SUI”), pass pay_with
with its symbol or coin type, and Artos pays with that coin only. The buyer can
switch coins at any point before paying: quote again with the new pay_with.
To show the buyer
exactly what they will pay before they approve, call quote_purchase with the
same arguments you will pass to confirm_purchase, pay_with included. It
prepares the payment and moves no money:
{
"name": "quote_purchase",
"arguments": {
"checkouts": [{ "store_slug": "energy-sport", "checkout_id": "checkout_456" }],
"payment_method": "artos.crypto"
}
}The result is the checkout plus a payment_quote:
{
"payment_quote": {
"quote_id": "3f9c…",
"coin_symbol": "USDC",
"coin_amount": 2.27,
"total": 427,
"currency": "USD",
"expires_at": "2026-09-25T12:01:00.000Z",
"legs": [
{ "kind": "direct", "coin_symbol": "USDC", "coin_amount": 2.27, "stable": true },
{ "kind": "swap", "coin_symbol": "USDsui", "coin_amount": 2.002, "stable": true }
]
}
}total is in minor units. coin_symbol / coin_amount name the coin paying
the most, and legs lists every coin; show the buyer all of them. When a
single non-stable coin pays (stable: false), its rate is
total / coin_amount. If the wallet cannot cover the total (keeping a little
SUI for network fees), quote_purchase returns insufficient_funds with what
the wallet holds, and nothing is locked. If the buyer named a coin whose
DeepBook market is too thin for this amount right now, it returns
insufficient_liquidity: offer another coin or a smaller order. Pass quote_id
to confirm_purchase before expires_at. The purchase pays that exact amount or
nothing: if the price no longer holds, confirm_purchase charges nothing and
returns a new payment_quote to show the buyer before asking again. A card or
$0 checkout has nothing to lock; quote_purchase says so, and you confirm it
directly.
Pay several shops in one transaction
A buyer shopping at several Artos stores can pay them all at once. Pass one
checkout per shop (2 to 5) in checkouts to quote_purchase, then the same
checkouts and the returned quote_id to confirm_purchase. One Sui
transaction pays every merchant, all or nothing, and each shop places its own
order:
{
"name": "quote_purchase",
"arguments": {
"checkouts": [
{ "store_slug": "energy-sport", "checkout_id": "checkout_456" },
{ "store_slug": "goodys", "checkout_id": "checkout_789" }
]
}
}The quote carries the whole payment in payment_quote and each shop’s checkout
with its share under checkouts[].payment_quote. The purchase returns
orders, one per shop. The buyer’s spend caps apply to the combined total.
Paying together is crypto only. A checkout that redeems an Artos coupon, shops
in different currencies, or shops that share a payout wallet return
combined_unsupported with nothing charged: pay those one by one. In the rare
case a shop fails after the payment went through, the result reports which
orders were placed along with the transaction_digest. Call
confirm_purchase with the failed checkouts and that transaction_digest to
place them without paying again.
Build — the SDK’s confirmPurchase
With @artos-commerce/ucp-client, a single
confirmPurchase is the equivalent of the manual cart → checkout → mandate →
complete sequence: it re-prices, verifies the store’s merchant_authorization,
mints the AP2 mandate, routes the $0 / card / crypto rail, and (for crypto)
signs and submits the Sui PTB.
const outcome = await shop.confirmPurchase({
checkouts: [{ storeSlug: 'energy-sport', checkoutId: 'checkout_456' }],
paymentMethod: 'artos.crypto', // or 'artos.card'; omit to be prompted
});
// outcome.status: 'completed' | 'escalation_required'
// | 'payment_selection_required' | 'quote_changed' | 'error'To lock a crypto price before the buyer approves, call preparePurchase with
the same input and pass its quote.quoteId as quoteId:
const prep = await shop.preparePurchase({
checkouts: [{ storeSlug: 'energy-sport', checkoutId: 'checkout_456' }],
paymentMethod: 'artos.crypto',
});
if (prep.status === 'quoted') {
// Show every prep.quote.legs coin and prep.quote.expiresAt, then on approval:
await shop.confirmPurchase({
checkouts: [{ storeSlug: 'energy-sport', checkoutId: 'checkout_456' }],
paymentMethod: 'artos.crypto',
quoteId: prep.quote.quoteId,
}); // 'quote_changed' carries the new quote; nothing was signed
}Pass one checkout per shop to pay several at once: preparePurchase returns
quoted_together, and confirmPurchase signs once and returns
completed_together with an order per shop (or partially_completed with the
transactionDigest to retry the rest with).
The raw UCP completion the SDK performs is documented below for non-JS clients.
Raw UCP complete (MCP + REST)
Build the checkout, then complete it. Every state-changing call needs both
meta["ucp-agent"].profile and meta["idempotency-key"].
Create the checkout:
{
"name": "create_checkout",
"arguments": {
"checkout": {
"cart_id": "cart_123",
"shipping_address": {
"line1": "1 Market St", "city": "San Francisco",
"region": "CA", "postal_code": "94105", "country": "US"
}
},
"meta": {
"ucp-agent": { "profile": "https://your-app.example/.well-known/ucp" },
"idempotency-key": "co-create-2f9c…"
}
}
}In raw UCP the checkout takes either
cart_idor aline_itemsarray, where each line item is{ "item": { "id": "variant_123" }, "quantity": 1 }— not the bridge’s flat{ id, quantity }.
Card completion. Supply a tokenized instrument and the AP2 checkout_mandate:
{
"name": "complete_checkout",
"arguments": {
"id": "checkout_456",
"checkout": {
"payment": {
"instruments": [{
"handler_id": "artos.card",
"selected": true,
"credential": { "type": "token", "token": "pm_1Pxxxx" }
}]
},
"ap2": { "checkout_mandate": "<base64url-header>..<base64url-signature>" }
},
"meta": {
"ucp-agent": { "profile": "https://your-app.example/.well-known/ucp" },
"idempotency-key": "co-complete-7b1a…"
}
}
}The REST equivalent is POST /s/:slug/checkout-sessions/:id/complete with the
{ payment, ap2 } body (no MCP wrapping), plus Request-Id, UCP-Agent, and
Idempotency-Key headers. See Direct UCP for the full
header rules.
Crypto completion.
- Prepare the payment intent (buyer bearer only) to get an unsigned Sui PTB:
curl -X POST https://api.artos.sh/s/energy-sport/checkout-sessions/checkout_456/payment-intent \
-H "Authorization: Bearer BUYER_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Request-Id: $(uuidgen)" \
-H 'UCP-Agent: profile="https://your-app.example/.well-known/ucp"' \
-d '{ "buyer_address": "0xBUYER" }'input_coin_type defaults to auto (the wallet order above). Pass a coin
type such as 0x2::sui::SUI, or a symbol the store accepts such as SUI
(any case), to pay with that coin only. Calling again with another coin
replaces the prepared payment. A coin missing from the store’s
accepted_coins returns an unsupported_coin error message, and a named
coin without enough DeepBook liquidity for the amount returns
insufficient_liquidity.
- Sign and submit the returned PTB with the buyer’s wallet (the bridge does this with the shared agent key on the buyer’s alias).
- Complete with the tx digest as the instrument token plus the AP2 mandate,
again with the buyer bearer only. Pass
ap2.intent_mandate(the user-signed allowance) when settling crypto directly.
Payment handoff (continue_url)
When a checkout completes on the card rail without a payment token (or hits a
3-DS step), the server returns status: requires_escalation and a
continue_url — the hosted page where the buyer enters their card. Forward
that link and the buyer finishes payment themselves. This is the basis of the
Checkout handoff guide.
Be precise about which continue_url you mean:
| Response | continue_url is… |
|---|---|
| Cart / incomplete checkout | a “continue in browser” link to that resource |
Checkout with requires_escalation | the hosted payment page (hand this off) |
| Completed / cancelled checkout | omitted (the session is terminal) |
A continue_url on a still-incomplete checkout is not a payment link and
not proof of payment.
AP2 mandates
The AP2 mandate is the agent authorization over the priced terms — not a
payment option the buyer picks. Artos uses a compact ES256 JWS of the form
header..signature (empty middle segment), not full SD-JWT+kb. The server:
- resolves the agent key by
kidfrom the platform profilekeysand verifies the ES256 signature; - requires
exp(missing/past →mandate_expired) and ajti(replays rejected); - requires the
merchantbinding to equal the store slug; - confirms the embedded terms (
id/total/currency) equal the live quote; - re-verifies the embedded
merchant_authorization(proof the agent signed over terms Artos authored).
See Profiles & trust for the full conformance notes and deliberate deviations.
Coupons (crypto only)
Any eligible Artos coupon the buyer owns is applied when paying with crypto with
apply_coupon: true (buyer opt-in via confirm_purchase / the SDK’s
applyCoupon flag):
- applies up to the full order total (merchandise, tax, and shipping);
- is reusable — a smaller order spends only that amount, the rest stays;
- one coupon per order (best eligible, chosen automatically); no stacking;
- account total ≠ one-order discount — e.g. two $1 coupons give at most $1 off a single checkout, not $2;
- does not apply on the card rail.
Never pre-compute or promise a discounted total — report the totals exactly as returned after the order is placed.
If one coupon does not fully cover the order, the remaining amount is paid on the chosen rail. On crypto, USDC in the wallet pays any remainder directly. A remainder paid in another coin goes through DeepBook, which enforces a minimum trade size: small leftovers (e.g. $0.30 after a $1 coupon on a $1.30 order) can fail when the wallet has no USDC, even when coupons are valid.
$0 orders
Some orders total $0 when a single coupon (or discount) fully covers the
total. These complete normally — report the returned total and never treat a $0
total as an error. Partial coupon coverage is not $0: the remainder still must
clear on crypto (in USDC, or above DeepBook’s minimum in another coin) or card.