Skip to main content

Embedded Checkout Protocol

The Embedded Checkout Protocol (ECP) enables your application to embed a merchant's checkout UI, receive events as the buyer interacts with checkout, and delegate key actions like address and payment selection to your native experience.

ECP is a technical bridge that defines how your host application communicates with the embedded checkout:

  • Load the checkout in a web view using the continue_url from the UCP checkout response — returned by create_checkout and later checkout calls whenever status is requires_escalation — with ECP parameters appended.
  • Receive events as the buyer progresses through checkout via JSON-RPC 2.0 messages.
  • Delegate actions like payment instrument selection and address changes to your native UI.
  • Complete the purchase when checkout signals completion.

ECP is inspired by the W3C Payment Request API.

For the full protocol specification, see the ECP specification.


Anchor to Construct the embedded URLConstruct the embedded URL

When you receive a requires_escalation status from a UCP checkout response (via REST, MCP, or other transport), add ECP query parameters to the continue_url to enable embedded checkout.

See the UCP spec for more details.


ec_version•stringRequired

The UCP version for this session. Must match the UCP version from the checkout response, for example 2026-08-25.


ec_auth•string

Authentication token in merchant-defined format. Optional if not required by the merchant.


ec_delegate•string

Comma-delimited list of delegations the host wants to handle natively. See Delegations.


Embedded URL construction

async function createEmbeddedCheckoutURL(checkout) {
const checkoutURL = new URL(checkout.continue_url);

// Retrieve JWT from your server
const authToken = await fetchShopifyAuthenticationJWT();
checkoutURL.searchParams.set('ec_version', checkout.ucp.version);
checkoutURL.searchParams.set('ec_auth', authToken);
checkoutURL.searchParams.set(
'ec_delegate',
[
'fulfillment.address_change',
'payment.instruments_change',
'payment.credential',
].join(','),
);

return checkoutURL.toString();
}

Example URL

https://example.com/checkout/c/abc123?ec_version=2026-08-25&ec_auth=eyJ...&ec_delegate=fulfillment.address_change,payment.instruments_change,payment.credential

Anchor to Web view integrationWeb view integration

Load the embedded checkout URL in a web view. Communication between your Host and the Embedded Checkout happens through two JavaScript globals:

  • EmbeddedCheckoutProtocolConsumer: Created by your app. Checkout calls postMessage() to send events to your application.
  • EmbeddedCheckoutProtocol: Created by Shopify Checkout. Your app calls postMessage() to respond to events.

All messages use JSON-RPC 2.0 format. Messages with an id field are requests that require a response. Messages without id are notifications that don't expect a response.

import WebKit

class CheckoutViewController: UIViewController,
WKScriptMessageHandler {
var webView: WKWebView!

func loadEmbeddedCheckout(url: URL) {
let config = WKWebViewConfiguration()
// Register message handler
config.userContentController.add(
self,
name: "EmbeddedCheckoutProtocolConsumer"
)
webView = WKWebView(frame: view.bounds, configuration: config)
view.addSubview(webView)
webView.load(URLRequest(url: url))
}

// Handle messages from Checkout
func userContentController(
_ controller: WKUserContentController,
didReceive message: WKScriptMessage
) {
// Parse JSON-RPC message
guard let body = message.body as? String,
let data = body.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(
with: data
) as? [String: Any]
else { return }

let method = json["method"] as? String
let id = json["id"] as? String

switch method {
case "ec.ready":
handleReady(id: id)
case "ec.payment.credential_request":
handleCredentialRequest(id: id, json: json)
case "ec.complete":
handleComplete(json: json)
default:
break
}
}

// Checkout stays blank until it receives this response.
func handleReady(id: String?) {
guard let id else { return }
respond(id: id, result: [
"ucp": ["status": "success", "version": "2026-08-25"]
])
}

func respond(id: String, result: [String: Any]) {
let response: [String: Any] = [
"jsonrpc": "2.0",
"id": id,
"result": result
]
let jsonData = try! JSONSerialization.data(
withJSONObject: response
)
let jsonString = String(data: jsonData, encoding: .utf8)!
webView.evaluateJavaScript(
"EmbeddedCheckoutProtocol.postMessage(\(jsonString))"
)
}
}

Delegations declare which operations your Host handles natively instead of in the Embedded Checkout UI. Each delegation maps to a corresponding _request message:

ec_delegate valueCorresponding messageDescription
fulfillment.address_changeec.fulfillment.address_change_requestHost presents address selection UI
payment.credentialec.payment.credential_requestHost authenticates and provides payment credentials
payment.instruments_changeec.payment.instruments_change_requestHost presents payment method selection UI
window.openec.window.open_requestHost validates and opens external links (policies, help pages, payment app handoffs) itself instead of the web view navigating away

When you accept a delegation:

  • Embedded checkout fires the _request message when that action is triggered, and waits for your response.
  • Your host must respond to every request with a result carrying the ucp envelope (status and the negotiated version) — including when the buyer cancels, which is a result with ucp.status: "error" and a messages entry, not a JSON-RPC error.
  • Embedded checkout doesn't show its own UI for delegated actions.

All events use JSON-RPC 2.0 format. Events are either:

  • Requests (include id): Require a response from your host.
  • Notifications (no id): Informational only, no response expected.

This reference covers the events a host has to handle to run a checkout. The ECP specification lists the full set, including the informational change notifications (ec.buyer.change, ec.fulfillment.change, ec.line_items.change, ec.messages.change, ec.payment.change, ec.totals.change), ec.auth, ec.error, and ec.window.open_request.

Broadcast when Embedded Checkout is ready. This initializes the communication channel and indicates which delegations were accepted. Checkout doesn't render until your host responds, so answer it promptly — at minimum with the ucp envelope.

Direction: Embedded Checkout → Host

Type: Request (requires response)

Payload:

  • delegate: Array of accepted delegation identifiers (subset of what you requested via ec_delegate)

Response:

  • ucp (required): The UCP envelope, { "status": "success", "version": "2026-08-25" }
  • checkout (optional): Additional display-only state, such as payment.instruments
  • upgrade (optional): A MessagePort to upgrade the communication channel

ec.ready

{
"jsonrpc": "2.0",
"id": "ready_1",
"method": "ec.ready",
"params": {
"delegate": [
"fulfillment.address_change",
"payment.instruments_change",
"payment.credential"
]
}
}
{
"jsonrpc": "2.0",
"id": "ready_1",
"result": {
"ucp": { "status": "success", "version": "2026-08-25" },
"checkout": {
"payment": {
"instruments": [
{
"id": "pm_1234567890abc",
"handler_id": "gpay",
"type": "card",
"brand": "visa",
"last_digits": "4242"
}
],
"selected_instrument_id": "pm_1234567890abc"
}
}
}
}

Signals that checkout is visible and ready for buyer interaction.

Direction: Embedded Checkout → Host

Type: Notification (no response)

Payload:

  • checkout: The current state of the checkout

ec.start

{
"jsonrpc": "2.0",
"method": "ec.start",
"params": {
"checkout": {
"id": "gid://shopify/Checkout/abc123?key=xyz789",
"status": "requires_escalation",
"totals": [
{ "type": "subtotal", "amount": 8900 },
{ "type": "total", "amount": 9400 }
],
"line_items": [...]
}
}
}

Anchor to [object Object]ec.fulfillment.address_change_request

Requests the host to present address selection UI for shipping.

Direction: Embedded Checkout → Host

Type: Request (requires response)

Payload:

  • checkout: Current checkout state including fulfillment details

Response:

  • checkout.fulfillment.methods: Updated methods with selected_destination_id and destinations

ec.fulfillment.address_change_request

{
"jsonrpc": "2.0",
"id": "address_1",
"method": "ec.fulfillment.address_change_request",
"params": {
"checkout": {
"id": "gid://shopify/Checkout/abc123?key=xyz789",
"fulfillment": {
"methods": [...]
}
}
}
}
{
"jsonrpc": "2.0",
"id": "address_1",
"result": {
"ucp": { "status": "success", "version": "2026-08-25" },
"checkout": {
"fulfillment": {
"methods": [
{
"id": "method_1",
"type": "shipping",
"selected_destination_id": "gid://host/Address/789",
"destinations": [
{
"id": "gid://host/Address/789",
"street_address": "123 Main Street",
"address_locality": "Brooklyn",
"address_region": "NY",
"postal_code": "11201",
"address_country": "US",
"first_name": "Jane",
"last_name": "Smith"
}
]
}
]
}
}
}
}

Anchor to [object Object]ec.payment.instruments_change_request

Requests the Host to present payment instrument selection UI.

Direction: Embedded Checkout → Host

Type: Request (requires response)

Payload:

  • checkout: Current checkout state including payment details

Response:

  • checkout.payment.instruments: Updated array of payment instruments
  • checkout.payment.selected_instrument_id: ID of the selected instrument

ec.payment.instruments_change_request

{
"jsonrpc": "2.0",
"id": "instruments_1",
"method": "ec.payment.instruments_change_request",
"params": {
"checkout": {
"id": "gid://shopify/Checkout/abc123?key=xyz789",
"payment": {...}
}
}
}
{
"jsonrpc": "2.0",
"id": "instruments_1",
"result": {
"ucp": { "status": "success", "version": "2026-08-25" },
"checkout": {
"payment": {
"instruments": [
{
"id": "pm_1234567890abc",
"handler_id": "gpay",
"type": "card",
"brand": "visa",
"last_digits": "4242",
"billing_address": {
"street_address": "123 Main Street",
"address_locality": "Charleston",
"address_region": "SC",
"postal_code": "29401",
"address_country": "US"
}
}
],
"selected_instrument_id": "pm_1234567890abc"
}
}
}
}

Anchor to [object Object]ec.payment.credential_request

Requests a payment credential for the selected instrument during checkout submission. This typically involves user authentication (biometric/PIN).

Direction: Embedded Checkout → Host

Type: Request (requires response)

Payload:

  • checkout: Current checkout state including payment details

Response:

  • checkout.payment.instruments: Updated instrument array with credential field added to the selected instrument

ec.payment.credential_request

{
"jsonrpc": "2.0",
"id": "credential_1",
"method": "ec.payment.credential_request",
"params": {
"checkout": {
"id": "gid://shopify/Checkout/abc123?key=xyz789",
"payment": {
"selected_instrument_id": "pm_1234567890abc"
}
}
}
}
{
"jsonrpc": "2.0",
"id": "credential_1",
"result": {
"ucp": { "status": "success", "version": "2026-08-25" },
"checkout": {
"payment": {
"instruments": [
{
"id": "pm_1234567890abc",
"handler_id": "gpay",
"type": "card",
"brand": "visa",
"last_digits": "4242",
"credential": {
"type": "payment_gateway",
"token": "examplePaymentMethodToken"
}
}
]
}
}
}
}

Indicates successful checkout completion.

Direction: Embedded Checkout → Host

Type: Notification (no response)

Payload:

  • checkout: Final checkout state, including the resulting order

ec.complete

{
"jsonrpc": "2.0",
"method": "ec.complete",
"params": {
"checkout": {
"id": "gid://shopify/Checkout/abc123?key=xyz789",
"status": "completed",
"order": {
"id": "gid://shopify/Order/123456789"
}
}
}
}

Anchor to [object Object]ec.window.open_request

Sent when checkout wants to open an external link — a policy page, help content, or a payment app handoff — and you accepted the window.open delegation. The request doesn't guarantee anything about the URL: validate it yourself before opening it — require https:, and ideally allow only origins you expect — then open it in your own browser surface and respond with a UCP envelope. Reject anything else, including custom-scheme links.

Direction: Embedded Checkout → Host

Type: Request (requires response)

Payload:

  • url: The URL checkout wants opened. Treat it as untrusted input

Response:

  • ucp: { "status": "success", "version": "2026-08-25" } when you opened it, or { "status": "error", "version": "2026-08-25" } plus a messages entry with code: "window_open_rejected_error" when you didn't

When the buyer cancels a delegated action or your host can't fulfil it, respond with a result whose ucp.status is error and describe why in messages. Reserve JSON-RPC error objects for malformed requests.

Error response

{
"jsonrpc": "2.0",
"id": "credential_1",
"result": {
"ucp": { "status": "error", "version": "2026-08-25" },
"messages": [
{
"type": "error",
"code": "abort_error",
"content": "Buyer cancelled the operation.",
"severity": "recoverable"
}
]
}
}

Was this page helpful?