Tracking links and attribution
How OpenPromoter credits the right partner with op.js, partner links, short links, coupon codes and server-side attribution. Read the setup.
OpenPromoter credits a sale to a partner using the strongest signal it has in this order:
- Coupon code the customer typed at checkout
- Click id passed to checkout (
client_reference_idorop_clickmetadata) - Partner code in customer or checkout metadata (
op_ref) - Visitor's clicks inside the cookie window
- Lead email recorded at signup
A coupon wins because it is the customer's explicit choice. After that, the most specific signal wins.
The script tag
Add op.js to the <head> of every page on your marketing site and app:
<script async src="https://app.openpromoter.com/op.js" data-program="acme"></script>
On a self-hosted instance, load it from your own APP_URL. The script is about 2 KB, has no dependencies and sets one first-party cookie (op_ref) on your domain.
| Attribute | Default | What it does |
|---|---|---|
data-program |
required | Your program slug or id |
data-param |
via |
Query parameter that carries the partner code (for example ?via=maya). ?via=, ?ref=, ?fpr= and ?gspk= are always read too |
data-domain |
current host | Cookie domain. Set .acme.com to share the cookie between acme.com and app.acme.com |
data-consent |
not set | required waits for consent before writing the cookie. See consent mode |
data-storage |
not set | none never writes a cookie. See cookieless mode |
data-api |
OpenPromoter URL | A first-party path on your own domain (such as /op) for tracking requests. See ad blockers |
When a visitor lands with ?via=maya, op.js records the click, stores the click id for the program's cookie window and then decorates checkout links.
Partner links
Every approved partner gets links in their portal. There are three kinds:
- Code links on your site:
https://acme.com/?via=maya. op.js records the click in the browser. - Redirect links:
https://app.openpromoter.com/r/<program>/<code>. The server records the click and redirects to your site withop_click,op_visitorand the partner code in the URL. UTM parameters are passed through. - Short links:
https://app.openpromoter.com/r/<program>/l/<key>. They can point at any page (deep links). With a custom domain on the program,https://partners.acme.com/<key>works too.
Add ?sub= to any link to tag the click with a sub-id (for example ?sub=youtube-nov). Sub-ids and UTM values are stored on the click and can be filtered in reports. Every partner link also has a QR code in the portal.
Because redirect and short links record the click on the server before your page loads, they still count when an ad blocker stops op.js.
Links from other tools
op.js and the server-side click tracker read the partner code from your configured parameter. They also read ?via= (Rewardful), ?fpr= (FirstPromoter), ?ref= (Tolt) and ?gspk= (PartnerStack). After you import your partners with their existing codes, links they already shared keep attributing.
Cookie window and first or last click
Each program sets a cookie window (60 days by default) and an attribution model in Settings > Program:
- Last click (default): a newer partner click replaces the older one.
- First click: the first partner keeps the customer for the whole window.
Signups (leads)
Record a signup as soon as you know the email and before any payment:
openpromoter.signup({ email: user.email, name: user.name });
signup() returns a promise and sends the current click id with it. Server-side, call POST /api/v1/leads or op.leads.create() from the SDK. Leads show up as referrals in the dashboard, and a later payment by that customer is credited to the same partner.
If you call OpenPromoter before op.js has loaded, queue the calls with this stub. op.js replays them on load:
<script>
window.openpromoter = window.openpromoter || function () {
(window.openpromoter.q = window.openpromoter.q || []).push(arguments);
};
</script>
Reading the click id
After a referred visit, op.js exposes:
| Property | Example | Use |
|---|---|---|
window.openpromoter.clickId |
clk_8f2k… |
Pass to checkout so the sale is attributed even without cookies |
window.openpromoter.visitorId |
vis_… |
Stable id for this browser |
window.openpromoter.code |
maya |
The partner code |
Stripe Payment Links and Pricing Table
Nothing to do. op.js adds client_reference_id=<clickId> to every link to buy.stripe.com or checkout.stripe.com on the page. It also sets client-reference-id on <stripe-pricing-table> and <stripe-buy-button> elements, including ones added after the page loads.
Stripe Checkout
If your server creates Checkout Sessions, send the click id from the browser and pass it to Stripe:
const session = await stripe.checkout.sessions.create({
// ...your existing options
client_reference_id: req.body.opClickId, // window.openpromoter.clickId
subscription_data: { metadata: { op_click: req.body.opClickId } },
});
OpenPromoter reads client_reference_id on checkout.session.completed and metadata.op_click (or metadata.op_ref with a partner code) on customers, subscriptions and invoices. With the SDK, stripeCheckoutParams(readReferralCookie(cookieHeader)) builds these fields for you.
Coupon codes
Give a partner a coupon code from their page in the dashboard. If you enter a Stripe coupon id, OpenPromoter creates the matching promotion code in your Stripe account. Any sale that uses the code is credited to that partner with or without a click.
Coupon codes also work with Paddle, Lemon Squeezy, Polar and Chargebee discount codes.
Consent mode
In the EU, affiliate cookies generally need consent. Set data-consent="required" and op.js keeps the click id in memory only and writes no cookie until your consent banner calls:
openpromoter("consent", true);
// or
openpromoter.consent(true);
Clicks are still recorded and the click id is still added to checkout links. A visitor who buys in the same session is attributed. Only the 60-day memory needs consent.
To never set a cookie at all, use cookieless mode. This is not legal advice; check with your own counsel.
Cookieless mode
Set data-storage="none" and op.js never writes a cookie with or without consent. The click id lives only on the page and still reaches checkout through Payment Links, the Pricing Table or your own client_reference_id. A visitor who buys in the same visit is attributed, and visitors who come back days later without a partner link are not.
Ad blockers
Some ad blockers drop requests to third-party tracking domains. Proxy them through your own domain and tell op.js where:
<script async src="/op/op.js" data-program="acme" data-api="/op"></script>
In Next.js, add a rewrite to next.config.ts:
const nextConfig = {
async rewrites() {
return [{ source: "/op/:path*", destination: "https://app.openpromoter.com/:path*" }];
},
};
Self-hosters use their own APP_URL as the destination. Any reverse proxy (Vercel, Netlify, Cloudflare, nginx) that forwards /op/* works the same way.
Other billing systems
For Paddle, Lemon Squeezy, Polar and Chargebee, pass the click id as custom data or metadata (see Billing providers). For anything else, report payments with POST /api/v1/conversions (see the API).
Checking the install
op.js pings OpenPromoter once per browser session. The setup wizard turns green as soon as the first ping arrives. That tells you the script is live on your site.
License
op.js and embed.js are MIT licensed. They can run on any site. The OpenPromoter app itself is AGPL-3.0.