Metadata and attribution
COPE carries two kinds of data through a checkout for you and hands both back once the buyer has paid:- Your own data.
metadatais a small bag of keys and values, andexternal_referenceis one identifier in it. Use them to match a COPE order to the record it belongs to in your system: an invoice, a CRM lead, a seat booking, a campaign. - Marketing attribution. The UTM parameters and ad click ids of the link the buyer arrived on, and the affiliate code of the link (
?aff=). Use them to see which campaign or partner brought the buyer.
What COPE carries for you
Embedded checkout and the hosted checkout page (a checkout’s
checkout_url) read no parameters of their own: they carry what the cart behind the checkout already holds, whichever way it was created. An offer’s link opens its checkout without the query string it was opened with, so parameters appended to it are not read.
Your own data: metadata and external_reference
On a checkout link
Both links the COPE dashboard hands out take it: Copy Product URL, and Copy 1-click Checkout URL where 1-click checkout is enabled for your business, on the product’s menu in the catalog. Take the link you copied and append a parameter. For a single identifier, the common case, useexternal_reference:
metadata[...] parameter per key:
{"order_id":"A-4471","campaign":"autumn","channel":"email"}. Reach for this spelling first. It stays readable in the link, so you can see at a glance what a buyer is being sent, and a mistake in one value costs you that value rather than everything.
- Every value arrives as a string.
?metadata[seat_count]=3comes back as"3", never3. A query string carries no types, so COPE does not invent one: the value is stored on the order and repeated on every payment webhook for the life of that order, and a guessed type would reach you in a shape you never chose. - It is one level deep.
metadata[campaign]is read;metadata[campaign][id]is ignored rather than flattened into a key of its own, as aremetadata[]and an unclosedmetadata[order_id. An empty value (?metadata[note]=) is kept as an empty string: you typed the key, so the key travels.
{"campaign":{"id":7},"seats":3} before encoding. Values may be strings, numbers, booleans, null, arrays and further objects. A metadata value that is not a JSON object (a bare string, a number, an array, anything that fails to parse) is ignored rather than guessed at. Parameters beside it are still read.
The parameter names are lower-case and matched exactly: Metadata[order_id] is not read. Your own key names inside them are kept exactly as you write them, so orderId comes back as orderId.
How the three spellings combine
They compose, in a fixed order:- The JSON object is read first.
- Each
metadata[key]parameter merges over it, key by key. - A standalone
external_referenceparameter wins over both, wherever it sits in the link, provided it carries a value. An empty one is passed over, and a key of that name from another spelling survives instead.
{"campaign":"autumn","external_reference":"order-4711"}.
Within any one spelling, the last occurrence wins, so appending &external_reference=order-4711 to a template that already carries a placeholder replaces it. What the later one replaces follows the size of what the parameter names:
Encode the value, every time
A link is not a string you can paste a raw value into. Two characters end the parameter early, and neither produces an error you can see:#starts the URL fragment. The browser never sends it to COPE, so everything from#onwards is silently dropped, including any parameters written after it.&starts the next query parameter, so the rest of your value is read as a different parameter and discarded.
encodeURIComponent() in JavaScript, urlencode() in PHP, urllib.parse.quote() in Python), including the whole JSON string for metadata. Generate these links from code rather than editing them by hand.
What the mistake costs you depends on the spelling, which is the strongest practical argument for the bracket form:
If you will not encode, put the value most likely to contain a stray character last in the link.
Keep a link’s bag to a short identifier and a few small values: the whole link has to pass mail clients, link shorteners and the web servers that open it, and a link too long for them may not open at all. Use the SDK or your server when you need the whole budget.
With the Checkout SDK
Pass the bag tocreateCart(), with your identifier as the external_reference key:
createPrefilledCheckout() takes the same metadata, and your identifier as its own externalReference field.
From your server
An offer and a checkout from your server takeexternal_reference and metadata as two separate members. Send your identifier as external_reference, never inside metadata, where it is refused with 422 reserved_metadata_key. A checkout from your server requires it and refuses a request without it with 422 validation_error; on an offer it is optional. Where you send it, it is 1 to 255 characters and not only spaces; otherwise the request is refused with 422 validation_failed. The buyer cannot change either on the checkout page (see offers and checkouts from your server).
Limits
The nesting limit counts containers, not values:
{"a":{"b":{"c":{"d":1}}}} is four objects deep and is accepted, while wrapping 1 in a fifth object is not. A field you send beside the bag, external_reference on an offer, a prefilled checkout or a checkout from your server, and intendedPaymentMethod on a prefilled checkout, is added after the 50-entry check, so the bag can hold 50 entries of your own beside it.
COPE rejects the whole bag rather than trimming it. Where a call of yours sends it, the call fails and nothing is created:
On a checkout link there is no call of yours to fail, and the buyer is not stopped. COPE would rather lose your reference than the sale: the rejected bag is dropped and the buyer checks out normally, and on a cart they are resuming, whatever bag that cart already held stays. Either way the order arrives without the reference you meant to put on it, and nothing in the webhook says why. Test a link before you send it out.
Reserved keys
Two key names are reserved so that COPE, the SDK and the link parameter agree on where a value lives. Both are yours, and both are returned to you like any other key:
COPE reserves four further names for its own internal flows:
authority_kind, clerk_user_id, event_sales_session_id and seller_device_id. They are never returned in any webhook’s metadata, so do not use them for values of your own. Treat any key you did not set as absent. An offer and a checkout from your server refuse all six inside metadata with 422 reserved_metadata_key.
UTM parameters and ad click ids
On a link
A product link and a 1-click checkout link read these parameters from the URL the buyer opens, and COPE stores them with the cart:- Only these nine are carried. Parameter names are lower-case and matched exactly, so
UTM_Sourceis not read. Other parameters,utm_id,gbraidandwbraidamong them, are not captured. - Send each parameter once, and only with a value.
- Only these two kinds of link read them. An offer’s link, a checkout’s
checkout_urland embedded checkout do not.
Attribution with the Checkout SDK
When your own page builds the cart, read the parameters from your page’s URL and pass them asattribution to createCart() or createPrefilledCheckout(). COPE stores the object as you send it:
attribution on the webhooks carries only the nine keys in the table above. COPE stores the other keys you send in attribution, among them landing_url, referrer and captured_at, but does not return them there. The cart API also keeps fbclid, gclid, ttclid and msclkid when you send them in attribution. The SDK’s CartAttribution type does not declare them. An affiliate_id or promo_code inside attribution is not applied: see affiliate links.
An offer and a checkout from your server take no attribution.
A second link or a returning buyer
A buyer who opens a product link, leaves, and comes back (a reload, a second tab, a phone that dropped the page from memory) returns to the cart they already had, with their email, tax location and quantity intact. What the link they come back on does to that cart:- Reloading the same page sends nothing again. A tab delivers its link to the cart once; a reload of that tab does not deliver it a second time.
- A 1-click checkout link starts a fresh cart on every visit, so the order carries exactly that link’s
metadata, UTM parameters and click ids. Its affiliate click follows the seven-day rule: a link without?aff=can still take the seller’s latest promoter click from the same browser. - Two tabs are one cart. Two product links for the same product, open in the same browser, normally share a single cart, and the order carries what reached it last: usually the link opened most recently, not the one whose tab the buyer checked out in. Hand a buyer one link at a time.
- The order keeps what the cart held when it was placed. A link opened after the order exists changes nothing on that order.
- If a value has to be on the order, put it on every link that can reach that product: a later link without your
external_referenceleaves the earlier link’s in place, but a later link without your UTM parameters clears them.
Affiliate links
Add?aff=<code> to a product link or a 1-click checkout link, with one of the seller’s promoter codes, and COPE records the buyer’s click on that promoter’s link:
?ref=<code>is read the same way when the link carries noaff. A link that usesreffor anything else has that value read as an affiliate code, so do not usereffor your own purposes on a COPE link.- The last click counts, for seven days, per seller. A later link with another of the seller’s promoter codes replaces the earlier click; a link with none keeps the latest one for seven days after its click. A click for one seller never replaces another seller’s.
- A code that is not one of the seller’s promoter codes is ignored.
- A click comes only from a link. An
affiliate_idorpromo_codeinsideattributionis not applied, and UTM parameters and click ids are never read as an affiliate click.
first_click_ref on the order and payment webhooks and affiliate on your thank-you page each carry the raw affiliate code of the buyer’s link click, whichever promoter was credited; reading it back says when each is left out.
Offers and checkouts from your server
A cart you create with your secret API key, an offer or a checkout from your server, keeps what you set:- Your
metadataandexternal_referencecannot be changed from the checkout page: a request that would change them is refused with422 metadata_locked. Re-sending the stored bag unchanged is accepted. - Attribution cannot be added. A request from the checkout page that sends
attributionis refused with422 attribution_locked, whatever it says, and creating a checkout from your server with anattributionmember is refused with400 invalid_request. Such an order carries no UTM parameters, click ids or affiliate link click.
Reading it back
On the webhooks
attributionis an object holding the UTM parameters and click ids that were captured, and no other key:{}when none was. The field also allowsnull, so treatnulland{}alike.first_click_refis the affiliate code of the buyer’s link click (see affiliate links). It is absent when there was none, when the buyer is the seller, and on a post-purchase upsell order. Treat absent andnullalike. It is not necessarily the promoter credited with the sale.landing_url,referrerandcaptured_atare not on any webhook.payment.failedalso declares a top-levelmetadata, which is never filled: readorder.metadata.
- Every payment on the order repeats it, instalments and subscription renewals included, and so do the refunds, chargebacks and disputes that follow. So a refund carries the same reference and attribution as the sale it refunds. Use the reference to group them with your own record, and match each event to the order by
order.id, which every one of these events carries. - An empty object means you attached nothing.
metadatais{}when you sent none. Parse defensively and treat absent and empty as the same thing. external_referenceis not unique per order. A post-purchase upsell is a separate order that repeats the originating order’s metadata. Key your records onorder.idand treat your own reference as a grouping handle.- It is stored with the order and delivered verbatim to every webhook endpoint you register. Keep personal and sensitive data out of it, and out of links especially, where the buyer can read and change it.
On your thank-you page
When the buyer is sent to your page after paying (the checkout’ssuccess_url, or the product’s custom thank-you page) COPE appends the same parameters whichever way the buyer paid:
A buyer who opened
?metadata[ref]=A-4471&metadata[campaign]=autumn&aff=partner42 arrives at:
%5B and %5D are [ and ], encoded; your framework decodes them like any other parameter, so metadata[ref] reads A-4471 (PHP $_GET['metadata']['ref'], Rails params[:metadata][:ref], JavaScript new URLSearchParams(location.search).get("metadata[ref]")).
- Keys arrive sorted by name, character by character (
Zbeforea,a_bafteraB), not in the order you wrote them. - Every value is text. Numbers and booleans are written as
3andtrue,nullas an empty value, and an object or array as its JSON, which a checkout link reads back through the?metadata={json}spelling. - The URL is kept to 2,000 characters, the length every proxy and server in front of a page accepts. A key whose value would take it past that is left out (the smaller ones are still sent), and
metadata_truncated=trueis added when it still fits, so your page knows. The whole bag is always on the order’s webhooks.affiliateandorderare never left out to save length. - Keep
orderandaffiliateout of your own success URL. COPE appends its parameters last and addsaffiliateonly when there is one, so a parameter of the same name in your URL makes the two indistinguishable. - A key containing
[or]is left out (and counted as truncated), becausemetadata[a][b]would be read as a nested parameter and clash withmetadata[a]. - No UTM parameters or click ids are added. A conversion tag on your thank-you page takes the order from
order, and that order’s attribution from the webhooks your server receives for it, not from the redirect. - A URL is not private. It lands in the buyer’s history, your server and CDN logs, and your analytics. That is why COPE never adds the buyer’s email, and one more reason to keep personal data out of the bag.
Through the API
- Retrieving an offer returns the
external_referenceandmetadatait was created with, and theorder.idonce it is paid. GET /v1/commerce/checkouts/{id}returns theexternal_referenceandmetadataof a checkout you created from your server, and itsorderonce the buyer has paid.
metadata and external_reference cannot be changed through the Commerce API: no operation updates them on an offer, a checkout or an order.
Match the order by its ID
Anything in a link is visible in the buyer’s address bar and can be edited there before they reach checkout, and a value your page sends with the publishable key comes from code running in the buyer’s browser. So:- Never make an access decision from it. Do not grant an entitlement, apply a price or unlock an account because
external_reference,metadataorattributionsays so: a buyer who changes one character in the URL would be granting it to themselves. - Match a paid order to your record by
order.idfrom the webhook: for an offer, theorder.idthat retrieving the offer returns; for a checkout from your server, theorderthat retrieving the checkout returns; for an order bought from a cart you created with the SDK,cart.idoncart.order.completed. An offer’s bag cannot be changed by the buyer, but a link can imitate it. - Put nothing private in a link. No email addresses, no internal notes, nothing you would not show the buyer. Use an opaque identifier that means something in your system and nothing outside it.
- Treat what arrives on your thank-you page as buyer-controlled input. Anyone can open your page with any parameters, so confirm the order by its
orderID before acting on it.
Test a link before you send it
A link whose bag broke a limit, or whose parameters were not encoded, still checks the buyer out, so the first sign of a mistake would be orders without your reference. Check one generated link first:- Generate the link from code, with every value percent-encoded.
- Open it in a browser that has no cart of its own for that product, such as a private window.
- Read the
cart.cart.createdwebhook it produces.metadataholds your keys andattributionyour UTM parameters and click ids. A bag that was refused is missing frommetadata, and a parameter COPE does not capture is missing fromattribution; that is the only warning you get.