> ## Documentation Index
> Fetch the complete documentation index at: https://docs.abconvert.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect the cart and checkout

> Write ABConvert's cart attributes so Shopify checkout applies the test, including for express checkout buttons.

Shopify checkout applies a test only when the cart carries the visitor's test groups. This page shows how to put them there for every way a visitor reaches checkout.

The samples use `abconvert`, the client your [stack guide](/headless-sdk/overview#pick-your-stack) creates from the request. The attributes describe the visitor, so create that client where you change the cart, not in a page a cache shares.

## Write the attributes

Write the SDK's attributes when you create the cart, each time you update it, and on your cart page when they changed. One call covers every test the visitor is in, whatever its type.

Shopify replaces the cart's whole attribute list on every update, so send the attributes the cart already has together with ABConvert's. Keep every attribute whose key the SDK did not return, and let the SDK's values replace the rest. Both methods return only ABConvert's attributes.

Pass the values to the Storefront API's [`cartLinesAdd`](https://shopify.dev/docs/api/storefront/latest/mutations/cartLinesAdd) and [`cartAttributesUpdate`](https://shopify.dev/docs/api/storefront/latest/mutations/cartAttributesUpdate) mutations, or to your framework's cart helper that calls them:

```ts Server (cart action) theme={null}
// Cart line attributes, each time you add a line: the lines input of cartLinesAdd
const lines = [
  {
    merchandiseId,
    quantity: 1,
    attributes: [...yourLineAttributes, ...abconvert.getCartLineAttributes()],
  },
];

// Cart attributes: keep the cart's, add or replace ABConvert's. The attributes input of cartAttributesUpdate
const ours = abconvert.getCartAttributes();
const keys = new Set(ours.map(({ key }) => key));
const attributes = [
  ...cart.attributes.filter(({ key }) => !keys.has(key)).map(({ key, value }) => ({ key, value: value ?? '' })),
  ...ours,
];
```

Your stack guide shows the same code in your stack's cart action: [Hydrogen](/headless-sdk/hydrogen#step-4-write-the-cart-attributes), [Next.js](/headless-sdk/nextjs#step-3-write-the-cart-attributes).

Every cart mutation returns `userErrors`. When the attribute update reports one, the cart still carries its old attributes, so return the error to your cart as you do for a failed add to cart instead of sending the visitor to checkout.

A cart keeps a test's attributes until the SDK returns different values for them. Nothing has to remove them: ABConvert ignores an ended test's attributes, and a request on which your tests could not load returns only the marker described in [Loading and failures](/headless-sdk/loading#if-loading-fails), which leaves the cart's test groups in place.

| You write | Effect |
| - | - |
| Cart attributes | Checkout applies the test price, shipping rate, offer, and checkout change through [Shopify Functions](https://shopify.dev/docs/api/functions/latest/cart-transform) and checkout extensions. The order is tied to the test group. |
| Cart line attributes | The same data on each line. Checkout reads a line's attributes only when the cart has no ABConvert attribute for that kind of test, so a checkout that loses the cart attributes still applies it. Both sets describe the same visitor, who stays in the same test groups, so they never disagree. |

### Prices by country

If you pass `country` to the client, give the cart the same country in its [buyer identity](https://shopify.dev/docs/api/storefront/latest/mutations/cartBuyerIdentityUpdate). Shopify prices the order by the cart's country and ABConvert sets the test price by the country in the attributes, so the two must agree. When the visitor switches country, update the buyer identity and ABConvert's attributes in the same action. See [Prices by country](/headless-sdk/prices#prices-by-country).

### On your cart page

Checkout charges what the cart attributes say. A test you launch after the visitor's last cart change shows its price on your cart page, but the cart does not carry it yet. Compare the attributes when you render the cart, and write them when they differ:

```ts Server (cart page) theme={null}
const attributes = cart?.attributes ?? [];
const ours = abconvert.getCartAttributes();
const changed = ours.some(({ key, value }) => attributes.find((attribute) => attribute.key === key)?.value !== value);
// No cart yet: nothing to update
if (cart && changed) {
  const keys = new Set(ours.map(({ key }) => key));
  const kept = attributes.filter(({ key }) => !keys.has(key)).map(({ key, value }) => ({ key, value: value ?? '' }));
  // cartAttributesUpdate with [...kept, ...ours]
}
```

The country is one of the attributes, so this check also catches a visitor who switched country since their last cart change. On Hydrogen 2026.10 and later, the component from [Step 4 of the Hydrogen guide](/headless-sdk/hydrogen#step-4-write-the-cart-attributes) already does this.

A cart drawer is a cart page too. If its checkout button links to `checkoutUrl` from any page, run the same check in the server action or route handler that returns the drawer's cart, so the button never carries attributes older than the prices the drawer shows. Price the drawer there too, with the client made from the request.

### Show what checkout charges

Once a cart carries ABConvert's attributes, Shopify prices it with the test price. In the Storefront API, a line's cost (`cost.amountPerQuantity`) and the cart's subtotal (`cost.subtotalAmount`) already include it, while the product variant's own `price` does not. Show the line cost in your cart and drawer: it is what checkout charges, even in the minutes after you launch or end a test.

```graphql theme={null}
cart(id: $cartId) {
  cost { subtotalAmount { amount currencyCode } }
  lines(first: 50) {
    nodes {
      cost { amountPerQuantity { amount currencyCode } }
      merchandise { ... on ProductVariant { id } }
    }
  }
}
```

For a product that is not in the cart yet, such as a recommendation in your drawer, use [`getPriceByVariantId`](/headless-sdk/prices#show-a-test-price).

### Express checkout

A visitor can reach checkout without your cart page. Whether the test applies depends on how the button creates the checkout.

| Button | Test applies | What to do |
| - | - | - |
| Shop Pay, Apple Pay, or Google Pay inside Shopify checkout | Yes | Nothing. The cart already carries the attributes. |
| Your own "Buy now" button | Yes | Create a cart with both sets of attributes, then send the visitor to its `checkoutUrl`. |
| A checkout URL from `getCheckoutUrl()` | Yes | Use it for a button that skips the cart, such as a Shop Pay button. See [Checkout URLs](#checkout-urls). |
| Hydrogen's [`ShopPayButton`](https://shopify.dev/docs/api/hydrogen/latest/components/shoppaybutton) with product variant IDs | No | It creates a checkout without your cart and accepts no attributes. Use a checkout URL or a "Buy now" button instead. |

```ts Server (cart action) theme={null}
// "Buy now": create a cart, then go straight to checkout
const CART_CREATE = `#graphql
  mutation CartCreate($input: CartInput!) {
    cartCreate(input: $input) {
      cart { checkoutUrl }
      userErrors { field message }
    }
  }
`;

const { cartCreate } = await storefront.mutate(CART_CREATE, {
  variables: {
    input: {
      lines: [{ merchandiseId, quantity: 1, attributes: abconvert.getCartLineAttributes() }],
      attributes: abconvert.getCartAttributes(),
      buyerIdentity: { countryCode: abconvert.getCountry() },
    },
  },
});
if (!cartCreate.cart) return { errors: cartCreate.userErrors };
return redirect(cartCreate.cart.checkoutUrl);
```

### Checkout URLs

`getCheckoutUrl()` builds a Shopify [cart permalink](https://shopify.dev/docs/apps/build/checkout/create-cart-permalinks) that carries the visitor's test groups. The visitor lands in checkout with the test applied, and you make no Storefront API call.

```tsx Server or browser theme={null}
const href = abconvert.getCheckoutUrl({
  lines: [{ variantId: '47522361606401', quantity: 1 }],
  payment: 'shop_pay', // Optional. Opens Shop Pay checkout
});

<a href={href}>Buy with Shop Pay</a>
```

Build the URL when you render the button, so it carries the current visitor's test groups. Do not share or cache it: everyone who opens it would land in the same test group.

A checkout URL has two limits that come from Shopify:

* It cannot sell a subscription. Create a cart for those. A [price test](/experiments/price-test) does not change the price of a subscription product, on your storefront as on a theme store.
* It carries cart line attributes for the first line only. Its cart attributes still cover every line, so the test applies to the whole order.

## Common mistakes

* **Writing the attributes once.** A cart created before a test launched has none for it, so your cart page shows the test price and checkout charges the original. The fix: write them each time you create or update the cart, and on your cart page or drawer when they changed.
* **Replacing the cart's other attributes.** Shopify replaces the whole list on every update. The fix: send the attributes the cart has and ABConvert's together, as in [Write the attributes](#write-the-attributes).
* **Sending the visitor to checkout after a failed update.** The cart still carries its old attributes, so checkout charges the old price. The fix: read `userErrors` and show the cart again.
* **Sharing or caching a checkout URL.** Everyone who opens it lands in the same test group. The fix: build the URL when you render the button.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.