Tracking and billing

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:

  1. Coupon code the customer typed at checkout
  2. Click id passed to checkout (client_reference_id or op_click metadata)
  3. Partner code in customer or checkout metadata (op_ref)
  4. Visitor's clicks inside the cookie window
  5. 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.

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 with op_click, op_visitor and 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.

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.

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

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.

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.

Edit this page on GitHub