---
title: Shop Pay in Checkout Kit
description: >-
  Add Shop Pay to a checkout that you run. Shop Pay opens over your page, the
  buyer pays with the details saved in their Shop Pay account, and Shop Pay
  completes the checkout at each merchant.
source_url:
  html: 'https://shopify.dev/docs/agents/carts-and-checkout/shop-pay/checkout-kit'
  md: 'https://shopify.dev/docs/agents/carts-and-checkout/shop-pay/checkout-kit.md'
---

# Shop Pay in Checkout Kit

You build with [Checkout Kit](https://shopify.dev/docs/agents/carts-and-checkout/checkout-kit), Shopify's open-source SDK for embedding checkout in any surface. Shop Pay is one import from it, `@shopify/checkout-kit/shop-pay`, which adds a `<shop-pay>` element to your page. The element opens Shop Pay with your session and tells you how the purchase goes, and you never collect or hold the buyer's actual payment details.

Today, Shop Pay opens a checkout that Shopify creates from your carts, for merchants on Shopify, and the element runs in browsers and in your app's web view. Support for any merchant that accepts Shop Pay, and for Checkout Kit's Swift, Kotlin, and React Native SDKs, is coming soon.

## shop-pay.js

```javascript
import '@shopify/checkout-kit/shop-pay';


const shopPay = document.querySelector('shop-pay');
shopPay.src = continueUrl; // the Universal session URL that your server created
shopPay.button = true;
shopPay.addEventListener('complete', ({detail}) =>
  showConfirmation(detail.map(({checkout}) => checkout.order)), // the merchants that completed in this step
);
```

***

## How it works

Your server creates a cart at each merchant and combines the carts into one universal session. Shop Pay opens in its own window over your page, either from Shop Pay's button, placed where your other wallet buttons go, or from your own pay button, with Shop Pay as a row in your payment list. The buyer signs in and pays, Shop Pay completes the checkout at each merchant, and your page receives the completed checkouts with their orders.

![The calls between your page, Shop Pay, your server, and the merchant to complete a purchase.](https://shopify.dev/assets/assets/images/agents/shop-pay/shop-pay-button-web-vpZlXUfj.svg)

***

## Build your integration

**Early access:**

Universal Checkout is in early access and available by request. Tool names, inputs, and response fields might change before general availability. To request access for your API key, submit the [Universal Checkout early access request form](https://forms.gle/NcqC2P3snoCuQ5V78).

### Requirements

* API credentials from Catalogs in the Dev Dashboard, with your client ID and client secret. Your server exchanges them for an access token, as [Authentication](https://shopify.dev/docs/agents/get-started/authentication) describes, and sends it as a Bearer token in every request below.

***

### Step 1: Prepare the checkout

On your server, create a cart at each merchant with the `create_cart` tool on its [Cart MCP](https://shopify.dev/docs/agents/carts-and-checkout/cart-mcp) endpoint. Then create one Universal session from the carts with the `create_checkout` tool at `https://catalog.shopify.com/api/ucp/mcp`. Pass up to 15 carts, one for each merchant, all in the same currency.

`create_checkout` returns a `continue_url`, the Universal session URL, that your page hands to Shop Pay in the next step. It gives access to the buyer's checkout, so return it to your page only, and don't log it. Prepare a new checkout whenever the cart changes.

## server.js

```javascript
// Your profile must have ucp.version 'draft' and declare dev.shopify.shopping.universal_checkout.
const PROFILE = 'https://your-app.example.com/.well-known/ucp';


async function callTool(endpoint, name, args, {token, buyerIp}) {
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
      ...(buyerIp && {'Shopify-Buyer-IP': buyerIp}),
    },
    body: JSON.stringify({
      jsonrpc: '2.0',
      method: 'tools/call',
      id: 1,
      params: {name, arguments: {meta: {'ucp-agent': {profile: PROFILE}}, ...args}},
    }),
  });
  const {result, error} = await response.json();
  if (error) throw new Error(error.message); // a JSON-RPC error: fix the request
  return result;
}


/** The merchant's myshopify.com domain from your own records, never from the browser. */
function shopDomainFor(merchantId) {
  const shopDomain = merchants.get(merchantId)?.shopDomain; // for example, stored from Global Catalog results
  if (!/^[a-z0-9][a-z0-9-]*\.myshopify\.com$/.test(shopDomain ?? '')) throw new Error('Unknown merchant');
  return shopDomain;
}


/** lineItems: [{item: {id: 'gid://shopify/ProductVariant/…'}, quantity: 1}] */
async function createCart(shopDomain, lineItems, auth) {
  const result = await callTool(`https://${shopDomain}/api/ucp/mcp`, 'create_cart', {cart: {line_items: lineItems}}, auth);
  if (result.isError) throw new Error(`${shopDomain} didn't create a cart`);
  return result.structuredContent; // the cart: {id: 'gid://shopify/Cart/…?key=…', …}
}


const GLOBAL_UCP = 'https://catalog.shopify.com/api/ucp/mcp';


app.post('/api/shop-pay/session', async (request, response) => {
  // [{merchantId, lineItems}], one for each merchant. Resolve each domain on the server.
  const carts = request.body.carts.map(({merchantId, lineItems}) => ({shopDomain: shopDomainFor(merchantId), lineItems}));
  const auth = {token: await getAccessToken(), buyerIp: request.ip}; // getAccessToken: see Authentication
  const created = await Promise.all(
    carts.map(({shopDomain, lineItems}) => createCart(shopDomain, lineItems, auth)),
  );
  const result = await callTool(GLOBAL_UCP, 'create_checkout', {cart_ids: created.map((cart) => cart.id)}, {token: auth.token});
  const {ucp, continue_url, messages = []} = result.structuredContent;
  if (result.isError || ucp.status !== 'success') {
    console.warn('No combined checkout:', messages.map((message) => message.code)); // never log continue_url
    return response.status(422).json({});
  }
  response.json({continueUrl: continue_url});
});
```

## {} The result of create\_checkout

```json
{
  "structuredContent": {
    "ucp": {"version": "draft", "status": "success"},
    "continue_url": "https://shop.app/checkouts/uc/…",
    "carts": [
      {"id": "gid://shopify/Cart/shop1-cart?key=…", "continue_url": "https://shop1.myshopify.com/cart/c/shop1-cart?key=…"},
      {"id": "gid://shopify/Cart/shop2-cart?key=…", "continue_url": "https://shop2.myshopify.com/cart/c/shop2-cart?key=…"}
    ],
    "messages": []
  }
}
```

***

### Step 2: Open Shop Pay

Add a `<shop-pay>` element to your page. Importing `@shopify/checkout-kit/shop-pay` registers the element. When your server returns the combined checkout, set its `continue_url` as the element's `src`. Whenever the cart changes, take Shop Pay away and clear `src` before you ask for a new combined checkout, and offer Shop Pay again only with the new checkout. Otherwise Shop Pay can pay for a cart that your page no longer shows. If requests can overlap, then keep only the latest response.

With `target="popup"`, Shop Pay opens in a pop-up window over your page. Without it, Shop Pay opens in a new tab. While Shop Pay is open, the element shows an overlay over your page, with a button that brings Shop Pay's window to the front and a button that closes it.

What opens Shop Pay is up to you: Shop Pay's button, or your own pay button.

***

#### Shop Pay's button

Add the element where your other wallet buttons go, and set `button` after you set `src`. The element draws the Shop Pay button that buyers trust to accelerate checkouts, and a tap opens Shop Pay. Hide the button whenever you clear `src`.

![Shop Pay's button: the Shop wordmark on a purple button.](https://shopify.dev/assets/assets/images/agents/shop-pay/shop-pay-button-jan0gV_-.svg)

## checkout.html

```html
<div id="wallet-buttons">
  <shop-pay id="shop-pay" target="popup"></shop-pay>
</div>
```

## shop-pay.js

```javascript
import '@shopify/checkout-kit/shop-pay';


const shopPay = document.querySelector('#shop-pay');


let latest = 0;


/** Call it whenever the cart changes. carts: [{merchantId, lineItems}] */
async function showShopPay(carts) {
  const request = ++latest;
  shopPay.button = false;
  shopPay.removeAttribute('src'); // the old checkout no longer matches the cart
  const response = await fetch('/api/shop-pay/session', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({carts}),
  });
  if (!response.ok || request !== latest) return; // leave Shop Pay out, or a newer cart replaced this one
  const {continueUrl} = await response.json();
  if (request !== latest) return;
  shopPay.src = continueUrl;
  shopPay.button = true;
}
```

***

#### Your pay button

Add Shop Pay as a row in your payment list, and add the element without the `button` attribute anywhere in your page. The element draws nothing until Shop Pay opens, but it must stay in the page, because it shows the overlay and receives Shop Pay's messages. Hide the Shop Pay row whenever you clear `src`.

Create the combined checkout before the buyer presses your pay button, for example when your checkout page loads. When the buyer presses your pay button with Shop Pay selected, call `open()` from the click handler itself. Browsers open a window only in response to the buyer's click, so a click handler that waits for your server first loses the chance to open it.

![A payment method list with Shop Pay selected, above Apple Pay, PayPal, Venmo, and a credit or debit card.](https://shopify.dev/assets/assets/images/agents/shop-pay/payment-list-DkIpTsPv.png)

[](https://shopify.dev/images/agents/shop-pay/shop-pay-icon.svg)

[Download the Shop Pay icon (SVG)](https://shopify.dev/images/agents/shop-pay/shop-pay-icon.svg)

## checkout.html

```html
<fieldset id="payment-methods">
  <label><input type="radio" name="payment" value="shop_pay" /> Shop Pay</label>
  <label><input type="radio" name="payment" value="card" /> Credit card</label>
</fieldset>
<button id="pay-button" type="button">Pay now</button>
<shop-pay id="shop-pay" target="popup"></shop-pay>
```

## shop-pay.js

```javascript
import '@shopify/checkout-kit/shop-pay';


const shopPay = document.querySelector('#shop-pay');


let latest = 0;


/** Call it whenever the cart changes. carts: [{merchantId, lineItems}] */
async function offerShopPay(carts) {
  const request = ++latest;
  hideShopPayRow();
  shopPay.removeAttribute('src'); // the old checkout no longer matches the cart
  const response = await fetch('/api/shop-pay/session', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({carts}),
  });
  if (!response.ok || request !== latest) return; // leave Shop Pay out, or a newer cart replaced this one
  const {continueUrl} = await response.json();
  if (request !== latest) return;
  shopPay.src = continueUrl;
  showShopPayRow();
}


document.querySelector('#pay-button').addEventListener('click', () => {
  if (selectedPaymentMethod() === 'shop_pay') shopPay.open(); // in the click itself
});
```

***

### Step 3: Follow the purchase

Shop Pay opens, and the buyer signs in and reviews the purchase. When the checkouts become interactive, the element dispatches `start`. When a checkout changes, for example when the buyer's Shop Pay address replaces yours or the buyer picks a shipping option, the element dispatches `update`. Shop Pay applies each change at the merchant for you.

Each event's `detail` lists only the checkouts that it describes, as `{context, checkout}` entries. `context.shopId` names the merchant. The element keeps no state for you to read back, so keep the latest entry for each merchant yourself, and add your listeners before you call `open()`.

## shop-pay.js

```javascript
shopPay.addEventListener('start', ({detail}) => showProgress(detail));
shopPay.addEventListener('update', ({detail}) => showProgress(detail));


function showProgress(entries) {
  for (const {context, checkout} of entries) {
    const status =
      !context.status || context.status === 'active' ? checkout.status : context.status;
    showMerchantStatus(context.shopId, status, checkout.totals);
  }
}
```

***

### Step 4: Show your confirmation

When the buyer presses **Pay now**, which happens once for the whole cart, Shop Pay completes each checkout at its merchant. Each merchant that completes arrives in a `complete` event while the others can still send updates, so one `complete` doesn't end the session. The session ends after one merchant has completed and every other merchant has completed, failed, or left, and Shop Pay's window stays open until you call `close()`.

In each completed checkout, `status` is `completed`, and `order` contains that merchant's order, with its `id` and `label`. Shop Pay doesn't show an order confirmation. Show yours, with one order for each merchant, using `order.label` as the order number.

Use `order.id` on your server to follow each order with [Order MCP](https://shopify.dev/docs/agents/orders/order-mcp) and [order webhooks](https://shopify.dev/docs/agents/orders/order-webhooks), which return the order's `permalink_url` for the buyer's order status page. Each merchant also sends the buyer its usual order confirmation.

## shop-pay.js

```javascript
shopPay.addEventListener('complete', ({detail}) => {
  for (const {context, checkout} of detail) showOrder(context.shopId, checkout.order); // {id, label}; your server adds permalink_url
});
```

***

### Edge cases

The steps above are the happy path. Handle the following too.

***

#### The buyer closes Shop Pay

If the buyer closes Shop Pay's window before paying, or closes it from the overlay, then the element dispatches `close`. Nothing is completed at the merchants, and the buyer can open Shop Pay again with the same session, or pay another way. A `close` with no earlier `start`, `update`, or `error` means that the buyer left Shop Pay's sign-in page before any checkout was shown.

To close Shop Pay from your page, call `close()`. Changing `src` or `target` also closes it.

## shop-pay.js

```javascript
shopPay.addEventListener('close', () => {
  showPaymentMethods();
});
```

***

#### A merchant drops out

In a session with several merchants, one merchant can fail while the others continue. An `update` reports that merchant with `context.status` set to `failed`, which Shop Pay can still retry, or `ejected`, which means that the merchant left the session. A merchant that can't continue also arrives in `error`, as an entry whose `context.shopId` names it. The other checkouts carry on, and `complete` reports only the merchants that completed. Tell the buyer which items weren't bought.

## shop-pay.js

```javascript
shopPay.addEventListener('update', ({detail}) => {
  for (const {context, checkout} of detail) {
    if (context.status === 'ejected') showNotBought(checkout.lineItems);
  }
});
```

***

#### Shop Pay can't continue

If Shop Pay can't continue, then the element dispatches `error` with an entry that has no `context.shopId`, and the session ends. Each entry has an `error` with a `code` and a `message`. Use `code` to decide what to do, and treat `message` as diagnostic text. Offer another payment method, and create a new combined checkout if the buyer wants to try Shop Pay again.

The element also reports problems on your page this way, before it opens Shop Pay, as entries with no `context` at all: `unsupported_browser` when the browser lacks a feature that Shop Pay needs, and `sdk_error` when `src` is empty or isn't an HTTPS URL.

## shop-pay.js

```javascript
shopPay.addEventListener('error', ({detail}) => {
  for (const {context, error} of detail) {
    if (context?.shopId) continue; // one merchant: see A merchant drops out
    showPaymentError(error.code);
    showPaymentMethods();
  }
});
```

## {} The detail of an error event

```json
[{"error": {"code": "unsupported_browser", "message": "This browser does not support: …"}}]
```

***

#### The browser blocks Shop Pay's window

Browsers open a window only in response to the buyer's tap. Shop Pay's button opens Shop Pay from the tap itself. With your own button, call `open()` directly from the buyer's click, and never from a `blocked` listener, a timer, or after an `await`. If the browser blocks the window anyway, then the element dispatches `blocked`, and its overlay offers the buyer a button that opens Shop Pay again. If the buyer dismisses the overlay instead, then the element dispatches `close`.

## shop-pay.js

```javascript
shopPay.addEventListener('blocked', () => {
  showPopupHint(); // optional: tell the buyer to allow pop-ups
});
```

***

#### Your page runs in your app's web view

Checkout Kit for the web works the same in a native app's web view, and needs three things from the app:

* JavaScript and DOM storage, which Android web views don't enable by default.
* Your app's shared, persistent cookie store, so that the buyer signs in to Shop Pay on `shop.app` once per install.
* A window for Shop Pay. The `<shop-pay>` element opens Shop Pay with `window.open`. Create that window as a web view of its own, on the same cookie store, present it in your own UI with a close button, and dismiss it when the page closes it.

## CheckoutViewController.swift

```swift
import WebKit


final class CheckoutViewController: UIViewController, WKUIDelegate {
  private lazy var webView: WKWebView = {
    let configuration = WKWebViewConfiguration()
    configuration.websiteDataStore = .default() // your app's shared, persistent cookie store
    let webView = WKWebView(frame: .zero, configuration: configuration)
    webView.uiDelegate = self
    return webView
  }()


  // The <shop-pay> element opens Shop Pay with window.open. WebKit requires the configuration it passes here,
  // which shares the cookie store with your page.
  func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
               for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
    let sheet = WKWebView(frame: .zero, configuration: configuration)
    sheet.uiDelegate = self
    presentSheet(sheet) // your UI: a sheet with a close button
    return sheet
  }


  // The page closes the window when the purchase ends.
  func webViewDidClose(_ webView: WKWebView) {
    dismissSheet(webView)
  }
}
```

## CheckoutWebView\.kt

```kotlin
import android.os.Message
import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebView


fun configure(webView: WebView) {
  webView.settings.apply {
    javaScriptEnabled = true
    domStorageEnabled = true
    javaScriptCanOpenWindowsAutomatically = true
    setSupportMultipleWindows(true)
  }
  CookieManager.getInstance().apply {
    setAcceptCookie(true)
  }
}


class CheckoutChromeClient(private val activity: Activity) : WebChromeClient() {
  // The <shop-pay> element opens Shop Pay with window.open.
  override fun onCreateWindow(view: WebView, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message): Boolean {
    val sheet = WebView(activity).also(::configure) // the default CookieManager, so the same cookies
    sheet.webChromeClient = this
    showSheet(sheet) // your UI: a dialog or bottom sheet with a close button
    (resultMsg.obj as WebView.WebViewTransport).webView = sheet
    resultMsg.sendToTarget()
    return true
  }


  // The page closes the window when the purchase ends.
  override fun onCloseWindow(window: WebView) {
    hideSheet(window)
  }
}
```

***

### Reference

`<shop-pay>` is the whole interface: an element with attributes, two methods, and six events. Every event is dispatched on the element and doesn't bubble, so add your listeners to the element itself, before you call `open()`. Each attribute is also a property.

#### Attributes

***

src

• string Required (critical)

The Universal session URL: the `continue_url` that `create_checkout` returned to your server. Changing it closes Shop Pay if it's open.

***

button

• boolean

Draws Shop Pay's button in the element. A tap calls `open()`. Without it, the element draws nothing, and you call `open()` from your own button.

***

target

• string

Where Shop Pay opens: `popup` for a pop-up window over your page, `auto` (the default) or `_blank` for a new tab, or the name of a window. Changing it closes Shop Pay if it's open.

***

#### Methods

***

open()

• method

Opens Shop Pay with the session in `src`. Shop Pay's button calls it on tap. With your own pay button, call it from the click handler itself.

***

close()

• method

Closes Shop Pay and the overlay. The element dispatches `close`.

***

#### Events

***

start

• event

The checkouts became interactive. `detail` is an array of `{context, checkout}` entries.

***

update

• event

Checkouts changed, or a merchant's `context.status` changed. `detail` is an array of `{context, checkout}` entries for the merchants that changed.

***

complete

• event

Merchants completed. `detail` is an array of `{context, checkout}` entries for the merchants that completed in this step. The others can continue, so a session can dispatch `complete` more than once.

***

error

• event

`detail` is an array of `{context, checkout, error}` entries. An entry with `context.shopId` belongs to that merchant and leaves the session open. Any other error ends it.

***

close

• event

Shop Pay closed. `detail` is `null`. Closing says nothing about whether the purchase completed.

***

blocked

• event

The browser blocked Shop Pay's window. `detail` is `{code: 'popup_blocked'}`. The overlay offers the buyer a retry, and dismissing it dispatches `close`.

***

## The shop-pay element

```typescript
import '@shopify/checkout-kit/shop-pay'; // registers <shop-pay>


interface ShopPay extends HTMLElement {
  src: string;
  button: boolean;
  target: 'auto' | 'popup' | '_blank' | string;
  open(): void;
  close(): void;
}


// The detail of start, update, and complete
interface ShopPayResourceEventEntry {
  readonly context: {
    readonly sessionId: string;
    readonly revision: number;
    readonly shopId: string;
    readonly status?: 'active' | 'completed' | 'failed' | 'ejected' | 'unknown';
  };
  readonly checkout: Checkout; // a UCP checkout, camelCase, without ucp
}


// The detail of error
interface ShopPayErrorEventEntry {
  readonly context?: {readonly sessionId: string; readonly revision: number; readonly shopId?: string}; // absent before a session
  readonly checkout?: Checkout; // the merchant's final checkout, when there is one
  readonly error: {readonly code: string; readonly message: string};
}
```

## {} The detail of a complete event

```json
[
  {
    "context": {"sessionId": "…", "revision": 7, "shopId": "79270248665", "status": "completed"},
    "checkout": {
      "id": "gid://shopify/Checkout/abc123",
      "status": "completed",
      "currency": "USD",
      "lineItems": [...],
      "totals": [...],
      "order": {
        "id": "gid://shopify/Order/1042",
        "label": "#1042",
        "permalinkUrl": "https://…"
      }
    }
  }
]
```

***

## Next steps

* [Checkout Kit](https://shopify.dev/docs/agents/carts-and-checkout/checkout-kit): The SDK that adds Shop Pay to your page, and embeds a merchant's full Shopify checkout too.
* [Cart MCP](https://shopify.dev/docs/agents/carts-and-checkout/cart-mcp): Create the carts that you combine into one checkout.
* [Build a Universal Checkout](https://shopify.dev/docs/agents/carts-and-checkout/universal-checkout): The `create_checkout` tool in full, with its requests, responses, and message codes.
* [Order MCP](https://shopify.dev/docs/agents/orders/order-mcp): Follow the orders that buyers place with Shop Pay.
* [Shop Pay in UCP](https://shopify.dev/docs/agents/carts-and-checkout/shop-pay-handler): The handler that merchants advertise to accept Shop Pay.

***
