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

# Build a vendor integration

> End-to-end quickstart for a vendor adding COPE checkout and webhook handling to an existing site, with a reference Next.js sample app.

# Build a vendor integration

A complete COPE integration is three pieces: a small amount of dashboard configuration, a buyer-facing checkout page, and a backend webhook receiver that confirms each payment.

<Tip>
  **Try it deployed in a few minutes**:
  [`copecart/integration-samples`](https://github.com/copecart/integration-samples)
  is a Next.js app with both checkout styles, a webhook receiver, and
  Vercel / Railway deploy buttons in the README.
</Tip>

## 1. Configure the business

In the COPE dashboard, gather three values before writing any code:

| Setting                              | Where                               | Purpose                                                                                                                             |
| ------------------------------------ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Publishable key (`cope_pk_live_...`) | Settings → SDK                      | Safe to ship in the browser. Used by the Checkout SDK. Created with the SDK integration for the business.                           |
| Product UUID (`prod_...`)            | Catalog                             | The product the buyer will purchase.                                                                                                |
| Embed origin (iframe checkout only)  | Settings → Checkout → Embed domains | The exact parent origin that will host the iframe, for example `https://shop.example.com`.                                          |
| Redirect URLs                        | Settings → SDK                      | The complete `success_url` and `cancel_url` values you will pass to `checkout()`, for example `https://shop.example.com/thank-you`. |

Embed origins use scheme + host (+ port) only — no path, query, or fragment. `https://shop.example.com` and `https://www.shop.example.com` are different origins.

**Embed origins and `success_url` / `cancel_url` are independent allowlists.** Registering an origin authorizes a parent page to *iframe* the checkout (and registers the domain with Stripe Payment Method Domains so browser wallets work). The success and cancel URLs are registered separately, must be HTTPS, and are checked when the checkout is created.

The two use different matching rules: an embed origin is scheme + host (+ port), while a redirect URL is matched as a complete URL string including its path and any query string. Registering `https://shop.example.com` does **not** authorize `https://shop.example.com/thank-you`. See [redirect URLs](../checkout-sdk/overview#redirect-urls).

Failures look different too — an unregistered embed origin produces a browser CSP block (`frame-ancestors 'none'`), while an unregistered `success_url` produces a `422 invalid_redirect_url` from the checkout-creation call.

## 2. Add checkout to your page

Install the Checkout SDK. Iframe checkout requires `>=0.2.0` for `mountCheckout`, and `>=0.3.0` to mount a phone offer; hosted (redirect) checkout works on any `0.x`.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add @copecart/sdk
```

The setup is the same for both checkout styles. Build a cart, add a line, create the checkout session — only the last call differs.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { CopeCart } from "@copecart/sdk"

const cope = new CopeCart({ publishableKey: "cope_pk_live_..." })

const cart = await cope.createCart({ currency: "EUR" })
await cope.addLine(cart.id, { product_id: "prod_..." })

const checkout = await cope.checkout(cart.id, {
  embed_origin: window.location.origin, // iframe only — omit for redirect
  success_url: "https://shop.example.com/thank-you",
  cancel_url: "https://shop.example.com/cart",
  consents: [{ type: "buyer_tos" }],
})
```

If you already hold an order, lead, or booking ID for this purchase, `createCart()` is where you attach it — see [step 4](#4-tie-the-webhook-back-to-your-own-record). The SDK has no later call that changes it, so assemble the bag before you create the cart.

For hosted (redirect) checkout, send the buyer to COPE:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
cope.redirectToCheckout(checkout)
```

For embedded (iframe) checkout, add a container element to the page where the iframe should mount:

```html theme={"theme":{"light":"github-light","dark":"github-dark"}}
<div id="cope-checkout-frame"></div>
```

Then mount the checkout on it:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
cope.mountCheckout("#cope-checkout-frame", checkout, {
  fallback: "redirect",
  onReady: () => showCheckoutFrame(),
  onSuccess: () => showOrderConfirmation(),
  onError: ({ code, retryable }) => reportCheckoutError(code, retryable),
})
```

In both styles, COPE redirects the buyer to `success_url` once payment completes. Treat that landing page as cosmetic — the authoritative payment signal arrives on your backend as a webhook (next step).

See [Embedded hosted checkout](../checkout-sdk/embedded-checkout) for the full mount contract, every callback, and the security model. The [Checkout SDK overview](../checkout-sdk/overview) covers the cart-building APIs.

## 3. Receive signed webhooks

The thank-you page is decorative. A buyer can land there directly and the `order_uuid` in the URL can be forged. Treat the signed webhook delivery to your backend as the only authoritative payment signal — that is where you grant access, write to your database, and notify other systems.

Stand up an HTTPS endpoint and register it once with COPE using a server-side `ck_live_...` API key — that is the secret counterpart of the publishable `cope_pk_...` you put in the browser. Only the publishable key is safe to ship to a client; the secret key stays on your server and is rotated periodically. Once the endpoint is registered, COPE will POST every relevant event to your URL. The receiver should:

* Verify the `X-Cope-Signature` header (`t=<timestamp>,v1=<hmac>`) — the HMAC covers `"{timestamp}.{raw body}"`, not the body alone.
* Ack `2xx` immediately. Push real work to a queue.
* Dedupe by `X-Cope-Event-Id` — `2xx` does not guarantee at-most-once delivery.

[Consume webhooks](./consume-webhooks) shows the recommended receiver shape. [Webhook signing](../webhooks/signing) is the exact HMAC contract, and [Webhook event types](../webhooks/event-types) lists every event you might handle.

## 4. Tie the webhook back to your own record

A webhook tells you that a payment succeeded. It does not, by itself, tell you *which of your records* it belongs to. Attach your own reference to the cart, and COPE hands it back on the order and payment events that follow.

With the SDK, pass it to `createCart()`:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const cart = await cope.createCart({
  currency: "EUR",
  metadata: { external_reference: "order-4711" },
})
```

If you sell by sharing a COPE checkout link instead of building a page, append it to the link — no code on your side:

```txt theme={"theme":{"light":"github-light","dark":"github-dark"}}
https://app.cope.com/checkout?product=<product-id>&plan=<plan-id>&external_reference=order-4711
https://app.cope.com/products/<product-id>?metadata[order_id]=A-4471&metadata[campaign]=autumn
```

One identifier goes in `external_reference`. For a few more fields, add one `metadata[...]` parameter per key — readable in the link, but every value arrives as a string, and only a URL-encoded JSON object in a single `metadata=` parameter carries numbers, booleans and nesting.

It comes back as `metadata` on `cart.order.completed`, and as `order.metadata` on every payment, refund, chargeback and dispute event for that order — every one except `payment.failed`, which carries no order metadata at all. Three things bite people here:

* **Percent-encode every value.** A raw `#` or `&` truncates it silently — and in the JSON spelling it costs you the whole bag, not one field.
* **On a product link the bag belongs to the cart, not to the visit.** A buyer returning to a cart they already started picks up the metadata of the link they are on now, and a link carrying none leaves the earlier link's bag in place. Put the reference on every link that can reach the product.
* **Treat a value that arrived on a link as buyer-supplied.** It is visible and editable in the address bar, so look your own record up by `order.id` before granting anything.

[Your own reference on an order](../checkout-sdk/overview#your-own-reference-on-an-order) covers all three link spellings and how they combine, the size and nesting limits, the reserved keys, why your own reference is not unique per order, and the exact list of events that carry it.

## Before going live

A handful of things easy to miss until the first real buyer trips on them:

* The webhook handler grants access — not the thank-you page.
* If you generate checkout links, open one yourself and check the reference on the resulting event. A bag that breaks the metadata limits does not stop the checkout — COPE drops it, or leaves in place whatever bag a cart the buyer is resuming already held, and the buyer pays as normal — so a broken link silently costs you the reconciliation handle on the orders it produces.
* Rotate the secret API key (`ck_live_...`; a key issued earlier as `cope_sk_live_...` keeps working until you replace it) and the webhook signing secret on a schedule. Store both server-side only.
* For iframe checkout, every parent origin you deploy from — preview, staging, production, custom domains — must be registered separately under Settings → Checkout. They are different origins to the browser.
