Complete checkout
After buyers select products from the Catalog, they need to complete checkout to finalize their purchase. Checkout MCP Tools implement UCP's checkout capability, enabling your AI agent to create and manage checkout sessions, transitioning buyers through the purchase flow until the order is complete.
This tutorial shows you how to create a checkout session, update the checkout with buyer information, transition checkout based on status, complete checkout and if required, handle escalations with Embedded Checkout Protocol.
Anchor to What you'll learnWhat you'll learn
In this tutorial, you'll learn how to do the following tasks:
- Create a checkout session with a selected product variant
- Monitor checkout status and handle
incompleteorrequires_escalationstates - Update checkout with missing information
- Complete checkout when status reaches
ready_for_complete
Anchor to RequirementsRequirements
- Complete the Search the Catalog tutorial
- A product variant ID and shop domain.
- The shop domain from the selected product (for example,
ecowear-example.myshopify.com)
Anchor to Step 1: Create a checkout sessionStep 1: Create a checkout session
Using the variant ID from the Catalog lookup, create a checkout session with the create_checkout tool:
-
Create a
checkout.jsfile that creates a checkout session:import 'dotenv/config';import { randomUUID } from 'crypto';const bearerToken = process.env.BEARER_TOKEN;// From previous tutorial.const shopDomain = 'ecowear-example.myshopify.com';const variantId = 'gid://shopify/ProductVariant/11111111111';fetch(`https://${shopDomain}/api/ucp/mcp`, {method: 'POST',headers: {'Content-Type': 'application/json','Authorization': `Bearer ${bearerToken}`},body: JSON.stringify({jsonrpc: '2.0',method: 'tools/call',id: 1,params: {name: 'create_checkout',arguments: {_meta: {ucp: {profile: 'https://agent.example/profiles/shopping-agent.json'}},idempotency_key: randomUUID(),currency: 'USD',line_items: [{quantity: 1,item: {id: variantId}}],buyer: {email: 'buyer@example.com'}}}})}).then(res => res.json()).then(data => {if (data.result?.content?.[0]?.text) {const checkout = data.result.content[0].text;console.log('Checkout ID:', checkout.id);console.log('Status:', checkout.status);if (checkout.messages?.length > 0) {console.log('Messages:', checkout.messages);}if (checkout.continue_url) {console.log('Continue URL:', checkout.continue_url);}}}).catch(err => console.error('Request failed:', err));{"jsonrpc": "2.0","method": "tools/call","id": 1,"params": {"name": "create_checkout","arguments": {"_meta": {"ucp": {"profile": "https://agent.example/profiles/shopping-agent.json"}},"idempotency_key": "<UUID>","currency": "USD","line_items": [{"quantity": 1,"item": {"id": "<VARIANT_ID>"}}],"buyer": {"email": "buyer@example.com"}}}}{"jsonrpc": "2.0","id": 1,"result": {"content": [{"type": "text","text": {"ucp": {"version": "2026-01-11","capabilities": [{ "name": "dev.ucp.shopping.checkout", "version": "2026-01-11" }]},"id": "gid://shopify/Checkout/abc123?key=xyz789","status": "incomplete","currency": "USD","line_items": [{"id": "li_1","quantity": 1,"item": {"id": "gid://shopify/ProductVariant/11111111111","title": "Organic Cotton Crewneck Sweater","price": 8900},"base_amount": 8900,"subtotal": 8900,"total": 8900}],"totals": [{ "type": "subtotal", "amount": 8900 }],"messages": [{"type": "error","code": "missing_shipping_address","severity": "recoverable","content": "Shipping address is required"}]}}],"isError": false}}checkout.js
import 'dotenv/config'; import { randomUUID } from 'crypto'; const bearerToken = process.env.BEARER_TOKEN; // From previous tutorial. const shopDomain = 'ecowear-example.myshopify.com'; const variantId = 'gid://shopify/ProductVariant/11111111111'; fetch(`https://${shopDomain}/api/ucp/mcp`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${bearerToken}` }, body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/call', id: 1, params: { name: 'create_checkout', arguments: { _meta: { ucp: { profile: 'https://agent.example/profiles/shopping-agent.json' } }, idempotency_key: randomUUID(), currency: 'USD', line_items: [ { quantity: 1, item: { id: variantId } } ], buyer: { email: 'buyer@example.com' } } } }) }) .then(res => res.json()) .then(data => { if (data.result?.content?.[0]?.text) { const checkout = data.result.content[0].text; console.log('Checkout ID:', checkout.id); console.log('Status:', checkout.status); if (checkout.messages?.length > 0) { console.log('Messages:', checkout.messages); } if (checkout.continue_url) { console.log('Continue URL:', checkout.continue_url); } } }) .catch(err => console.error('Request failed:', err));{} MCP input reference
{ "jsonrpc": "2.0", "method": "tools/call", "id": 1, "params": { "name": "create_checkout", "arguments": { "_meta": { "ucp": { "profile": "https://agent.example/profiles/shopping-agent.json" } }, "idempotency_key": "<UUID>", "currency": "USD", "line_items": [ { "quantity": 1, "item": { "id": "<VARIANT_ID>" } } ], "buyer": { "email": "buyer@example.com" } } } }{} Response
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": { "ucp": { "version": "2026-01-11", "capabilities": [ { "name": "dev.ucp.shopping.checkout", "version": "2026-01-11" } ] }, "id": "gid://shopify/Checkout/abc123?key=xyz789", "status": "incomplete", "currency": "USD", "line_items": [ { "id": "li_1", "quantity": 1, "item": { "id": "gid://shopify/ProductVariant/11111111111", "title": "Organic Cotton Crewneck Sweater", "price": 8900 }, "base_amount": 8900, "subtotal": 8900, "total": 8900 } ], "totals": [ { "type": "subtotal", "amount": 8900 } ], "messages": [ { "type": "error", "code": "missing_shipping_address", "severity": "recoverable", "content": "Shipping address is required" } ] } } ], "isError": false } } -
Execute the script from your terminal:
Terminal
node checkout.jsThe response includes a
statusfield that tells you what to do next:incomplete: Missing required information. Checkmessagesfor what's needed. In this case, the shipping address is missing.requires_escalation: Buyer input needed. Redirect tocontinue_url.ready_for_complete: Ready to finalize the order.
In this case, the status is
incompletebecause checkout requires more information from the buyer (their shipping address in this case).
Anchor to Step 2: Update checkout detailsStep 2: Update checkout details
When status is incomplete, use update_checkout to provide missing information identified in the messages array.
We'll update the checkout with the missing shipping address:
checkout.js
import 'dotenv/config';
import { randomUUID } from 'crypto';
const bearerToken = process.env.BEARER_TOKEN;
const shopDomain = 'ecowear-example.myshopify.com';
const variantId = 'gid://shopify/ProductVariant/11111111111';
// Step 1: Create checkout
fetch(`https://${shopDomain}/api/ucp/mcp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${bearerToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/call',
id: 1,
params: {
name: 'create_checkout',
arguments: {
_meta: {
ucp: {
profile: 'https://agent.example/profiles/shopping-agent.json'
}
},
idempotency_key: randomUUID(),
currency: 'USD',
line_items: [
{
quantity: 1,
item: { id: variantId }
}
],
buyer: { email: 'buyer@example.com' }
}
}
})
})
.then(res => res.json())
.then(data => {
const checkout = data.result?.content?.[0]?.text;
// Check if status is incomplete with missing_shipping_address
if (checkout?.status === 'incomplete') {
const missingAddress = checkout.messages?.find(
msg => msg.code === 'missing_shipping_address'
);
if (missingAddress) {
// Update checkout with shipping address
return fetch(`https://${shopDomain}/api/ucp/mcp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${bearerToken}`
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/call',
id: 2,
params: {
name: 'update_checkout',
arguments: {
id: checkout.id,
fulfillment: {
methods: [
{
type: 'shipping',
destinations: [
{
first_name: 'Jane',
last_name: 'Smith',
street_address: '123 Main Street',
address_locality: 'Brooklyn',
address_region: 'NY',
postal_code: '11201',
address_country: 'US'
}
]
}
]
},
payment: {
instruments: [
{
id: 'pm_1234567890abc',
handler_id: 'gpay',
type: 'card'
}
],
selected_instrument_id: 'pm_1234567890abc'
}
}
}
})
});
}
}
return null;
})
.then(res => res ? res.json() : null)
.then(data => {
if (data) {
console.log(JSON.stringify(data, null, 2));
}
})
.catch(err => console.error('Request failed:', err));{} MCP input reference
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 2,
"params": {
"name": "update_checkout",
"arguments": {
"id": "<CHECKOUT_ID>",
"fulfillment": {
"methods": [
{
"type": "shipping",
"destinations": [
{
"first_name": "Jane",
"last_name": "Smith",
"street_address": "123 Main Street",
"address_locality": "Brooklyn",
"address_region": "NY",
"postal_code": "11201",
"address_country": "US"
}
]
}
]
},
"payment": {
"instruments": [
{
"id": "pm_1234567890abc",
"handler_id": "gpay",
"type": "card"
}
],
"selected_instrument_id": "pm_1234567890abc"
}
}
}
}{} Response
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": {
"id": "gid://shopify/Checkout/abc123?key=xyz789",
"status": "requires_escalation",
"currency": "USD",
"totals": [
{ "type": "subtotal", "amount": 8900 },
{ "type": "fulfillment", "amount": 500, "display_text": "Shipping" },
{ "type": "total", "amount": 9400 }
],
"continue_url": "https://ecowear-example.myshopify.com/cart/c/abc123?key=xyz789"
}
}
],
"isError": false
}
}Anchor to Step 3: Complete checkoutStep 3: Complete checkout
Depending on the status of the previous call of update_checkout, you may have everything you need.
-
When status reaches
ready_for_complete, callcomplete_checkoutto finalize the order:{"jsonrpc": "2.0","method": "tools/call","id": 3,"params": {"name": "complete_checkout","arguments": {"id": "<CHECKOUT_ID>","idempotency_key": "<UUID>"}}}{"jsonrpc": "2.0","id": 3,"result": {"content": [{"type": "text","text": {"id": "gid://shopify/Checkout/abc123?key=xyz789","status": "completed","currency": "USD","totals": [{ "type": "subtotal", "amount": 8900 },{ "type": "fulfillment", "amount": 500, "display_text": "Shipping" },{ "type": "tax", "amount": 712, "display_text": "Tax" },{ "type": "total", "amount": 10012 }]}}],"isError": false}}{} MCP input reference
{ "jsonrpc": "2.0", "method": "tools/call", "id": 3, "params": { "name": "complete_checkout", "arguments": { "id": "<CHECKOUT_ID>", "idempotency_key": "<UUID>" } } }{} Response
{ "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": { "id": "gid://shopify/Checkout/abc123?key=xyz789", "status": "completed", "currency": "USD", "totals": [ { "type": "subtotal", "amount": 8900 }, { "type": "fulfillment", "amount": 500, "display_text": "Shipping" }, { "type": "tax", "amount": 712, "display_text": "Tax" }, { "type": "total", "amount": 10012 } ] } } ], "isError": false } }
Anchor to Optional: handle escalations with Embedded Checkout ProtocolOptional: handle escalations with Embedded Checkout Protocol
When you updated checkout details in the previous step, notice that the value of status was requires_escalation, not ready_for_complete.
With this status the checkout requires buyer input that can't be collected via the API such as a business critical Checkout UI extension implemented by the merchant for age verification.
Use the continue_url to render an embedded checkout sheet where buyers can complete these steps securely.
-
Construct the embedded checkout URL
Add the following query parameters to the
continue_urlto enable the Embedded Checkout Protocol:Parameter Required Description ec_versionYes The version of the embedded checkout protocol. Must be 2026-01-11.ec_authYes The JWT token retrieved from your server-side authentication. ec_delegateYes A comma-separated list of delegations: fulfillment.address_change,payment.instruments_change,payment.credential.The resulting URL will look like:
Construct the embedded checkout URL
https://ecowear-example.myshopify.com/cart/c/abc123?ec_version=2026-01-11&ec_auth=eyJ...&ec_delegate=fulfillment.address_change,payment.instruments_change,payment.credentialNote that the
ec_-prefixed parameters contain characters that must be URL-escaped. -
Render in a web view
Load the embedded checkout URL in a web view. Checkout coordinates with your application through two JavaScript global variables:
EmbeddedCheckoutProtocolConsumer: Created by your app. Checkout callspostMessage()on this object to send events to your application.EmbeddedCheckoutProtocol: Created by Shopify Checkout. Your app callspostMessage()on this object to respond to Checkout events.
iOS
Use
WKWebViewto render the embedded checkout:import WebKitclass CheckoutViewController: UIViewController, WKScriptMessageHandler {var webView: WKWebView!func loadEmbeddedCheckout(url: URL) {let config = WKWebViewConfiguration()// Register message handler for Checkout eventsconfig.userContentController.add(self, name: "EmbeddedCheckoutProtocolConsumer")webView = WKWebView(frame: view.bounds, configuration: config)view.addSubview(webView)webView.load(URLRequest(url: url))}// Handle messages from Checkoutfunc userContentController(_ controller: WKUserContentController,didReceive message: WKScriptMessage) {// Process ECP message from Checkoutprint("Received message: \(message.body)")// Respond using evaluateJavaScriptwebView.evaluateJavaScript("EmbeddedCheckoutProtocol.postMessage({ type: 'response', ... })")}}Android
Use
WebViewto render the embedded checkout:import android.webkit.WebViewimport android.webkit.JavascriptInterfaceclass CheckoutActivity : AppCompatActivity() {private lateinit var webView: WebViewfun loadEmbeddedCheckout(url: String) {webView = findViewById(R.id.webView)webView.settings.javaScriptEnabled = true// Register message handler for Checkout eventswebView.addJavascriptInterface(EmbeddedCheckoutConsumer(),"EmbeddedCheckoutProtocolConsumer")webView.loadUrl(url)}inner class EmbeddedCheckoutConsumer {fun postMessage(message: String) {// Process ECP message from Checkoutprintln("Received message: $message")// Respond using evaluateJavascriptwebView.evaluateJavascript("EmbeddedCheckoutProtocol.postMessage({ type: 'response', ... })",null)}}}Web
For web applications, open the embedded checkout in an iframe or new window:
if (checkout.status === 'requires_escalation' && checkout.continue_url) {const embeddedURL = await createEmbeddedCheckoutURL(checkout);window.open(embeddedURL, '_blank');}Use
WKWebViewto render the embedded checkout:import WebKitclass CheckoutViewController: UIViewController, WKScriptMessageHandler {var webView: WKWebView!func loadEmbeddedCheckout(url: URL) {let config = WKWebViewConfiguration()// Register message handler for Checkout eventsconfig.userContentController.add(self, name: "EmbeddedCheckoutProtocolConsumer")webView = WKWebView(frame: view.bounds, configuration: config)view.addSubview(webView)webView.load(URLRequest(url: url))}// Handle messages from Checkoutfunc userContentController(_ controller: WKUserContentController,didReceive message: WKScriptMessage) {// Process ECP message from Checkoutprint("Received message: \(message.body)")// Respond using evaluateJavaScriptwebView.evaluateJavaScript("EmbeddedCheckoutProtocol.postMessage({ type: 'response', ... })")}}
InfoInstead of implementing the full Embedded Checkout Protocol described below, you can open the
continue_urldirectly in an in-app browser to let buyers complete checkout.For a complete reference of all events and delegation patterns, see the Embedded Checkout Protocol reference.
Anchor to Next stepsNext steps
- About Shopify Catalog: Learn how Catalog search and lookup work across the Shopify ecosystem.
- Checkout for agents: Learn how to embed checkout in agentic commerce apps.
- UCP Checkout specification: Explore the full UCP Checkout capability specification.
- UCP Order specification: Learn about confirmed transactions and post-purchase events, including line items, fulfillment expectations and events, and adjustments like refunds and returns.