Identity stitching
The hard part of partner attribution isn’t the click. It’s the gap between click and conversion. A user clicks a partner link on their phone, plays around for a week, signs up on their laptop a month later. Did the partner earn that?
OpenPartner says yes. Here’s how.
The core trick: link userId to clickId
Attribution rests on two records:
- Identity — a row that links a
userId(the brand’s internal user ID) to theclickId(the click that brought them in) - Event — the conversion itself, tagged with the userId
Once an Identity exists, every future event from that userId can be attributed back to the original click without needing a cookie or referer.
When the Identity gets written
The brand calls identify when a user signs up or first signs in, either from the browser SDK:
await op.identify(user.id);or from its own server, with POST /attribution/identify and { cref, userId }. The SDK sends
the userId plus the cref it captured from the landing URL. Server-side:
insert into "Identity" (id, userId, clickId, stitchedAt)values (...)on conflict do nothing;The cref came from the click; the userId came from the brand. Now they’re linked.
From this moment forward, any event the brand reports with the same userId is attributable, even without a cookie, from any device:
// Months later, from the brand's server: no cookie, no browser involvedawait op.trackEvent({ userId: user.id, type: 'invoice_paid', value: 49, currency: 'USD', externalEventId: invoice.id });// Server attributes via Identity.userId → original clickId → partnerEvents always come from the brand’s server (or from Stripe, through the Stripe App), never from the browser. See Events.
Surviving Safari ITP
Safari’s Intelligent Tracking Prevention (ITP) caps first-party cookies at 7 days for domains it classifies as cross-site (anything you reached via a redirect, basically).
Without identity stitching, this would mean any conversion 8+ days after the click on Safari loses attribution. With Identity, the cookie is only needed once — at first identify. After that, attribution survives on the userId, which the brand controls server-side.
A practical impact: SaaS conversions with long trial periods (14-day, 30-day) work correctly on Safari. Same for ecommerce abandoned-cart flows where the user comes back weeks later.
Cross-device
User clicks on phone → click recorded, cref captured. User signs up on phone → identify runs, and an Identity row links that clickId to the userId.
Same user later signs in on a laptop. There’s no cref there, and none is needed: events the brand reports with the same userId attribute through the existing Identity row, whichever device they happen on. (Identifying the same click and user again writes nothing new; each clickId and userId pair is stored once. A new partner click followed by identify adds a second Identity row for the same user, which multi-touch models can use.)
Works because the brand’s own userId is the join key, not anything in the browser.
When stitching breaks
A few cases:
- User clicks but never identifies — never converts → no commission. Expected.
- User uses two different brand accounts — separate userIds → separate Identity rows. If they convert on the second account, the click on the first doesn’t transfer.
- Brand calls
identify()with the user’s email instead of internal ID — works if email is unique per user, but breaks if anyone shares an email or the brand allows email changes. Use a stable internal ID. - Cookie + cref both wiped before first identify — first conversion can’t link back to the click. Subsequent conversions can’t either (no Identity row to anchor to). Lost.
The first-identify case is the load-bearing one. Brands need to call identify() as early
as possible — ideally on signup landing, before the user navigates anywhere. The longer the
gap between click and identify, the more chances the cookie has to be cleared.
What gets attributed via Identity vs Click
A simplified version of the attribution query:
select c.partnerIdfrom "Event" eleft join "Identity" i on i.userId = e.userIdleft join "Click" c on c.id = i.clickIdwhere e.id = $eventId and c.ts > now() - interval '60 days' -- the campaign's windowAttribution always flows through Identity. An event whose userId has no Identity yet is
recorded but not attributed (no_identity). If that user is identified later, their earlier
events are reprocessed for attribution, with the same window and eligibility checks.
The customer’s first attributed conversion has to fall within the attribution window. With “Renewals stay with the partner who earned the customer” on (the default), what follows keeps that attribution after the window closes. A repeat of a conversion type (a renewal invoice, say) keeps the partners that conversion had. A new type of conversion with no click left in the window goes to the partners of the customer’s latest attributed conversion.
Multi-touch via Identity
For attribution models other than last-click (linear, position, first-click), the system needs all clicks the user made within the window. Identity gives us that:
select c.* from "Click" cwhere c.id in ( select i.clickId from "Identity" i where i.userId = $userId)and c.ts > now() - interval '60 days';Each click in that result is a touchpoint. The model decides who gets what share.
So Identity isn’t just for “did this convert” — it’s the cross-device, cross-session record of the user’s full attribution graph.
Privacy implications
Identity stores (userId, clickId). The userId is whatever the brand passes: usually its
internal database ID or a Stripe customer ID, and for coupon redemptions at a guest checkout,
the customer’s email address. Pass an opaque internal ID where you can.
Either way, this is personal data under laws like the GDPR. The userId is linked to the click, and the click carries the visitor’s user agent, referrer, landing URL and a hash of their IP address (a hash, not anonymous). The brand is the controller of this data and OpenPartner is its processor.
There’s no self-serve endpoint yet for deleting one person’s records. If one of your customers asks you to delete their data, email [email protected] with their userId and we’ll delete or de-identify their identities, events and the clicks linked to them. Deleting a userId in your own database doesn’t remove anything on our side.
How we handle this data is set out in the Privacy Policy and the data section of the Terms.