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.1. Configure the business
In the COPE dashboard, gather three values before writing any code:
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.
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.
createCart() is where you attach it — see step 4. 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:
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 for the full mount contract, every callback, and the security model. The 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 theorder_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-Signatureheader (t=<timestamp>,v1=<hmac>) — the HMAC covers"{timestamp}.{raw body}", not the body alone. - Ack
2xximmediately. Push real work to a queue. - Dedupe by
X-Cope-Event-Id—2xxdoes not guarantee at-most-once delivery.
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 tocreateCart():
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.idbefore granting anything.
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 ascope_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.