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 or iframe using the continue_url with ECP parameters.
  • 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 draws inspiration from the W3C Payment Request API, adapting its mental model for embedded checkout scenarios.

For the full protocol specification, see the ECP specification.

ConceptW3C Payment RequestEmbedded Checkout
Initializationnew PaymentRequest()Load with continue_url
UI Readyshow() returns Promiseec.start notification
Payment Changepaymentmethodchange eventec.payment.change notification
Address Changeshippingaddresschange eventec.fulfillment.address_change_request
Submit PaymentUser accepts → PaymentResponseec.payment.credential_request
Completionresponse.complete()ec.complete notification

Anchor to Construct the embedded URLConstruct the embedded URL

When you receive a requires_escalation status from 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 be 2026-01-11.

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', '2026-01-11');
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-01-11&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
}
}

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

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, including errors if the user cancels.
  • 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.

Broadcast when Embedded Checkout is ready. This initializes the communication channel and indicates which delegations were accepted.

Direction: Embedded Checkout → Host

Type: Request (requires response)

Payload:

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

Response:

  • checkout (optional): Additional display-only state, such as payment.instruments
  • upgrade (optional): A MessagePort to upgrade 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": {
"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": {
"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": {
"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": {
"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
  • order: Order confirmation details

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"
}
}
}
}

When a user cancels a delegated action, respond with an error:

Error response

{
"jsonrpc": "2.0",
"id": "credential_1",
"error": {
"code": "abort_error",
"message": "User cancelled the operation."
}
}