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

# Checkout Extension

> Install OrderProtection's Shopify checkout UI extension and point each checkout at a partner A/B cohort.

This page is for **partners who render OrderProtection inside Shopify checkout** through the `checkout-widget` UI extension and want each checkout to resolve a specific A/B cohort. It builds on the [Partner Integration Quickstart](/developer/partner-integration-quickstart) — register your OAuth app and create your variants there first, then use this page to wire the extension to them.

<Note>
  The checkout extension calls the **path-based partner endpoints** (`/v2/.../partner/:applicationId`), not the header-based [public read with cohort headers](/developer/endpoints#public-widget-read-with-cohort). The three block settings below are what bind a checkout to your cohort.
</Note>

## Prerequisites

* An **approved OAuth app** — see [Creating an app](/developer/creating-an-app). Note your `Application.id` (looks like `cmpn5in6p000484flmfk9ns4d`).
* A **partner context** created for the merchant's store — see [Quickstart, Step 3](/developer/partner-integration-quickstart#step-3-create-your-partner-context).
* A **price set** ([Step 4](/developer/partner-integration-quickstart#step-4-create-a-price-set-per-cohort)) and a **widget config** ([Step 5](/developer/partner-integration-quickstart#step-5-create-a-widget-config-per-cohort)) created for each cohort, keyed by your `variantKey`.
* The `checkout-widget` extension **installed on the store** — see below.

## How the extension gets installed

The OrderProtection Shopify app is **not listed in the Shopify App Store**. It's distributed as a **custom app per store**: OrderProtection creates the app in its Shopify Partners account, deploys the extensions to it, and generates a custom-distribution install link scoped to that store (or, for Shopify Plus, that organization's stores). After install, the `checkout-widget` app block is added to the checkout in the Shopify checkout editor, which is where the block settings below live.

For merchant stores this is part of OrderProtection onboarding. For **partner development stores**, contact your OrderProtection engineering contact to have the app provisioned and the install link sent over — there is no self-serve install path.

## Configure the block settings

When the `checkout-widget` extension is added to a store's checkout, configure three fields under **checkout-widget → Block settings**. A partner (e.g. PDQ) sets these at install time.

<Frame>
  <img src="https://mintcdn.com/orderprotectioncom/70Pwd3NQ5_STOrgU/images/developer/checkout-extension-block-settings.png?fit=max&auto=format&n=70Pwd3NQ5_STOrgU&q=85&s=540a2c75cd146ed116d8da752ccbce8e" alt="Shopify checkout editor showing the checkout-widget Block settings: Application ID, Price Set Variant Key, and Widget Config Variant Key, with the OrderProtection — Cohort A widget rendering in the checkout preview at $9.80." width="2000" height="1578" data-path="images/developer/checkout-extension-block-settings.png" />
</Frame>

| Setting | Example | What it does |
| - | - | - |
| **Application ID** | `cmpn5in6p000484flmfk9ns4d` | Your OAuth `Application.id`. Used as the path segment on the partner-scoped endpoints. Set it together with **both** variant keys (static mode) or with **both empty** (runtime mode) — see the mode table below. |
| **Price Set Variant Key** | `test-a` | Routes pricing to `GET /v2/quote/insurance/partner/:applicationId?key=:variantKey` for partner-scoped pricing. |
| **Widget Config Variant Key** | `test-a` | Routes the widget config fetch to `GET /v2/widget/config/partner/:applicationId?key=:variantKey` instead of the default per-store endpoint. |

The block settings state selects one of three modes:

| Block settings | Mode |
| - | - |
| No Application ID | Merchant checkout — the extension uses the store's default widget and pricing; the fields below and the runtime attributes are ignored. |
| Application ID + variant keys filled | **Static partner mode** — the checkout is pinned to that cohort. The two keys are independent, so you can pin pricing and appearance separately. |
| Application ID + variant keys **empty** | **Runtime mode** — the cohort is supplied per session via cart attributes, and the widget renders nothing until it arrives. See [Runtime cohort binding](#runtime-cohort-binding-cart-attributes). |

## Runtime cohort binding (cart attributes)

Static block settings pin every session on a checkout to one cohort. Partners whose experimentation assigns cohorts **during the session** (for example, after the buyer enters their email and address) bind at runtime instead, using two cart attributes:

| Attribute | Value |
| - | - |
| `_op_price_variant` | The `variantKey` of the price set to apply |
| `_op_widget_variant` | The `variantKey` of the widget config to apply |

Values are the same partner-chosen cohort strings used when creating price sets and widget configs through the API (`^[a-zA-Z0-9_.:-]{1,64}$`) — not ids OrderProtection generates, and not Shopify product variant ids. Your extension writes them with Shopify's [Attributes API](https://shopify.dev/docs/api/checkout-ui-extensions/latest/target-apis/checkout-apis/attributes-api) (`applyAttributeChange`, `type: "updateAttribute"`). Cart attributes are shared checkout state across apps, which is what makes them the cross-extension channel.

### Session lifecycle

1. **Setup, once per store**: the `checkout-widget` block is added with Application ID set and both variant-key fields empty. One `checkout-widget` block — no per-variant blocks, and no checkout-editor changes per test.
2. **Per test, API only**: create a price set and a `CHECKOUT` widget config per `variantKey`. A control arm is a `variantKey` whose price set has `noOp: true`.
3. **Per session**: the widget renders nothing while it waits, and makes no cohort pricing or widget-config requests for a surface until that surface's key exists. When your extension allocates, it writes **both** attributes — the widget stays hidden until both exist.
4. The widget fetches that cohort's pricing and widget config from the [partner endpoints below](#what-the-extension-fetches), renders, and applies the cohort's auto-add behavior. A `noOp` cohort renders nothing and removes any protection line.
5. **Re-allocation** mid-session is supported — the widget refetches and, on toggle-layout cohorts, swaps the protection line (tile-layout cohorts keep the existing line until the buyer interacts) — but it never re-runs the auto-add decision (a buyer's explicit choice is not overridden). Don't clear the attributes after allocating, and never express the control arm by *not* writing them: always allocate explicitly, so "not allocated yet" and "control" stay distinguishable.
6. Cart attributes become note attributes on the resulting Shopify order (standard Shopify behavior), so the cohort keys are available in order payloads and webhooks for your per-order test attribution.

### Rules and edge cases

* **Write both attributes at allocation time.** One without the other keeps the widget hidden.
* **Don't half-fill the block settings.** Application ID with only one variant-key field filled leaves the other surface waiting for an attribute that may never come, so the widget stays hidden. Fill both fields (static mode) or neither (runtime mode).
* **Invalid values are treated as absent** (client-side validation against the key pattern), falling back to the block-setting value if one exists, otherwise back to the hold.
* **Attributes override filled block settings** when both are present. But leave the fields empty on runtime checkouts: a filled field pins a cohort that renders immediately on page load and then swaps when the allocation lands — the hold only engages when the fields are empty. Filled fields are useful for debugging a cohort statically, not for live tests.
* **Missing cohort data falls back visibly, per surface**: a key with a widget config but no `SHIPPING` price set renders the cohort's appearance with the store's **merchant fallback pricing** (the pricing fetch 404s); a key with a price set but no `CHECKOUT` widget config renders the **merchant's appearance** with cohort pricing. If either half looks wrong during setup, check both resources exist for that exact `variantKey`.
* **Wallet checkouts (Apple Pay, Google Pay)** reject extension cart changes — attribute writes and protection line-adds alike — so protection can't be sold in those sessions by any binding mode; in runtime mode they simply stay unallocated and hidden. **Shop Pay is expected to work** — it's the full checkout surface, and buyer details arriving prefilled means allocation can fire immediately — but it hasn't been verified end-to-end yet.
* **Unallocated sessions stay hidden indefinitely** by design. The partner owns allocation reliability; there is no timeout fallback to a default widget.

### Impression signal (write-back)

On runtime-allocated sessions, the widget reports back through two more cart attributes so your extension can verify — in-session — that the impression actually landed:

| Attribute | Written when |
| - | - |
| `_op_rendered` = `"true"` | Once per session, at the moment the widget first renders the allocated cohort. |
| `_op_not_rendered_reason` | Only when the widget is running but **definitively declines** to render after your allocation. Retracted when the widget subsequently renders. |

Reason values: `no_op` (the cohort is a noOp control arm — expected behavior), `out_of_range` (cart subtotal outside the cohort's price rules), `country_excluded` (destination or geo exclusion), `not_eligible` (pickup, B2B, hidden shipping method, nothing shippable), `error` (the pricing fetch failed).

Read it as a tri-state after writing your allocation attributes:

* **`_op_rendered` present** — the impression happened. Treat this as authoritative even if a stale reason briefly coexists (the retraction is best-effort).
* **Reason present, no `_op_rendered`** — the widget ran and declined; the value says why. `no_op` is your control arm working correctly, not a failure.
* **Neither appears** — the widget never ran: missing block, misconfigured checkout, broken extension, or a wallet flow. This is the setup-problem signal, and it works precisely *because* it's an absence — a missing renderer can't self-report.

Two guarantees behind the signal: reasons are only emitted for **definitive** decisions (a country or subtotal that simply hasn't resolved yet never produces a false decline), and neither attribute is ever written on merchant checkouts or statically pinned cohorts — they exist only when both cohort keys arrived via runtime attributes. Like the allocation attributes, they persist onto the order.

<Note>
  **Channel longevity**: `applyAttributeChange` is deprecated in Shopify's 2026+ checkout API versions in favor of cart metafields (it remains fully supported on the API versions the extensions currently pin). When pinned versions sunset, this channel migrates to a shared cart metafield with the same contract — coordinate the swap with OrderProtection before bumping your extension's `api_version`.
</Note>

## What the extension fetches

Once configured, on each checkout render the extension calls the partner-scoped endpoints below. Both are **public** (no OAuth token) and resolve the cohort from the path `applicationId` + `key` query param.

### Pricing — `GET /v2/quote/insurance/partner/:applicationId`

| Param | In | Required | Description |
| - | - | - | - |
| `applicationId` | path | Yes | Your OAuth `Application.id`. |
| `key` | query | Yes | The **Price Set Variant Key** (`variantKey`). Must match the partner key format. |
| `store_url` | query | Yes | The merchant's store domain, e.g. `example.myshopify.com`. |
| `country` | query | No | ISO 2-letter country code. Defaults to `US`. |

```bash theme={null}
curl -G https://api.production.orderprotection.com/v2/quote/insurance/partner/cmpn5in6p000484flmfk9ns4d \
  --data-urlencode "key=test-a" \
  --data-urlencode "store_url=example.myshopify.com" \
  --data-urlencode "country=US"
```

The response is the same shaped quote payload as the [public widget read](/developer/endpoints#public-widget-read-with-cohort), computed from the cohort's `SHIPPING` price set. Responses are cached for \~1 hour per `(store_url, country, applicationId, key)` (plus the A/B session token when one is present).

**Errors**

* `400 Bad Request` — `key` is missing, or `applicationId`/`key` don't match the partner key format.
* `404 Not Found` — the store isn't found, the app isn't installed, or the cohort has no `SHIPPING` pricing rules.

### Widget config — `GET /v2/widget/config/partner/:applicationId`

| Param | In | Required | Description |
| - | - | - | - |
| `applicationId` | path | Yes | Your OAuth `Application.id`. |
| `key` | query | Yes | The **Widget Config Variant Key** (`variantKey`). Must match the partner key format. |
| `store_url` | query | Yes | The merchant's store domain. |
| `type` | query | No | Widget type: `CART`, `CHECKOUT`, `INFO_MODAL`, or `SUBSCRIPTION`. Defaults to `CHECKOUT`. |

```bash theme={null}
curl -G https://api.production.orderprotection.com/v2/widget/config/partner/cmpn5in6p000484flmfk9ns4d \
  --data-urlencode "key=test-a" \
  --data-urlencode "store_url=example.myshopify.com" \
  --data-urlencode "type=CHECKOUT"
```

```json theme={null}
{
  "id": "cmokr9v3t000284gkq2m8xw1e",
  "type": "CHECKOUT",
  "status": "ACTIVE",
  "variant": "test-a",
  "name": "Cohort A — flat-rate",
  "config": { "...": "schemaless widget configuration — the renderer validates on read" },
  "customizationValues": { "...": "read-only mirror of config, for legacy consumers" },
  "updatedAt": "2026-05-26T18:46:25.987Z"
}
```

`config` and `customizationValues` always hold the same value; `customizationValues` is the legacy column name. Responses are cached for \~1 hour per `(store_url, applicationId, key, type)`.

**Errors**

* `400 Bad Request` — `key` is missing, or `applicationId`/`key` don't match the partner key format.
* `404 Not Found` — the store isn't found, the app has no active context on that store, or no widget config exists for that `(variantKey, type)`.

<Warning>
  These endpoints **throw** `400`/`404` rather than silently falling back. Treat any non-`200` as "no partner cohort for this checkout" and render the store's default widget — a partner cohort must never break checkout.
</Warning>

## Default vs. cohort render

The explainer below walks the whole flow — authenticate, create variants, configure the extension, and render per shopper:

<Frame>
  <video controls width="100%" src="https://mintcdn.com/orderprotectioncom/70Pwd3NQ5_STOrgU/images/developer/partner-flow.mp4?fit=max&auto=format&n=70Pwd3NQ5_STOrgU&q=85&s=8b0e691121ed607c0aab8f9db89763a4" data-path="images/developer/partner-flow.mp4" />
</Frame>

With no partner keys set, the extension renders the store's **default** widget:

<Frame>
  <img src="https://mintcdn.com/orderprotectioncom/70Pwd3NQ5_STOrgU/images/developer/checkout-widget-default.png?fit=max&auto=format&n=70Pwd3NQ5_STOrgU&q=85&s=d84450b9a6e9347205e483d153dc5bdf" alt="Default Order Protection checkout widget: shield icon, 'Order Protection — Protection for Damage, Loss, Theft & More!', priced at $10.00, toggled on." width="798" height="176" data-path="images/developer/checkout-widget-default.png" />
</Frame>

With Application ID + variant key `test-a`, the same checkout resolves the cohort's price set and widget config — for example **OrderProtection — Cohort A** at `$9.80` with flat-rate protection, as shown in the checkout preview above.

## Next steps

<CardGroup cols={2}>
  <Card title="Partner Integration Quickstart" icon="rocket" href="/developer/partner-integration-quickstart">
    Register your app and create your cohorts' pricing and widget configs — the setup this page depends on.
  </Card>

  <Card title="Partner endpoints reference" icon="key" href="/developer/endpoints">
    Full schema for every `/v2` partner endpoint, including the contexts, pricing, and widget-config writes.
  </Card>

  <Card title="Creating an app" icon="plus" href="/developer/creating-an-app">
    Register an OAuth app and capture your `Application.id`, client ID, and secret.
  </Card>

  <Card title="Scopes" icon="shield-halved" href="/developer/scopes">
    Scopes your app needs to create contexts, price sets, and widget configs.
  </Card>
</CardGroup>


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