Tracking events
Events are how revenue (or any conversion you care about) enters OpenPartner. Each event references the user that triggered it; OpenPartner walks Identity → Click to determine attribution and compute commissions.
Events are server-side only — they drive money, so the browser never gets to assert
them. Report from your backend with the server SDK or a plain HTTP call, authenticated
with an API key (mint one in your dashboard under Admin → Integrations; it carries
the events:write scope).
Standard events
| Event | Use for |
|---|---|
signup | User account creation. No revenue yet — still useful for funnel analysis. |
trial_started | Trial subscription started. |
subscription_created | Paid subscription created. |
invoice_paid | Recurring revenue. Most programs commission on this. |
Server SDK
import { OpenPartnerServer } from '@openpartner/sdk/server';
const op = new OpenPartnerServer({ apiUrl: process.env.OPENPARTNER_API_URL!, // same base URL as the browser SDK apiKey: process.env.OPENPARTNER_API_KEY!,});
// On signup:await op.trackEvent({ userId: user.id, type: 'signup' });
// On a paid order:await op.trackEvent({ userId: user.id, type: 'invoice_paid', value: 49.0, // a number in MAJOR units: dollars, not cents currency: 'USD', externalEventId: order.id, // makes retries safe});Send externalEventId on every call: your system’s stable id for that one event. A retry
with the same key returns the original result with replayed: true instead of recording
and commissioning the event twice. See Idempotency. Its
TypeScript types arrived in SDK 0.2.0: on 0.1.0, build the event in a variable first, or the
object literal fails TypeScript’s excess-property check.
Only have the click id, not a user you identified? op.trackConversion({ clickId, type, value, currency, externalEventId }) records the conversion in one call. See
Conversions by click id.
userId must be the same identifier the browser SDK passed to identify() — that’s the
join key. value is a number, never a string or null, and is required for any event you
commission a percentage on. currency can be any case; leave it out and the event is
recorded in your default currency (Admin → Settings → Brand info). Runs on Node 18+,
Bun, Deno, and Cloudflare Workers.
Prefer raw HTTP? The equivalent call is
POST /attribution/events — same fields, same response.
The response tells you what happened
{ ok: true, eventId: '01K…', attribution: { status: 'attributed', // or 'no_identity' / 'no_click' / 'outside_window' / 'corrective' model: 'last_click', commission: 'accrued', // or 'no_matching_rule' / 'none' touches: [{ clickId, partnerId, weight: 1, attributionId, commissionId }], }}no_identity means no click was ever stitched to that user, by identify() or otherwise;
the event is attributed later if one is. no_click means the user’s clicks are all flagged
or belong to revoked partners. commission: 'no_matching_rule' means a partner was credited
but no commission rule takes that event type, which usually means a typo’d event name.
Multi-touch models (linear, position) return one touch per attributed click with
fractional weights. Every status is listed under Events.
Only have a click id?
When the system that sees the conversion has the click id but no identified user (a lender
funding a loan, a CRM closing a deal from a lead form), report it to
POST /attribution/conversions with the same API key. No
identify() needed. The published SDK has no helper for this call, so use fetch:
const res = await fetch(`${process.env.OPENPARTNER_API_URL}/attribution/conversions`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.OPENPARTNER_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ clickId, // from ?cref= or your destination's {click_id} parameter type: 'loan_funded', value: 5000, currency: 'USD', externalEventId: `${loan.id}:funded`, // required }),});const { outcome } = await res.json(); // 'attributed', 'no_matching_rule' or 'not_attributed'Custom events
Any event name works:
await op.trackEvent({ userId: user.id, type: 'demo_booked', value: 500, currency: 'USD' });Then target the name in a program’s commission rule in the admin UI.
If you bill with Stripe
You don’t need trackEvent for Stripe payments. On the hosted plan,
connect Stripe (beta) from Admin → Integrations: your Checkout and
Payment Link purchases, invoices, renewals and refunds are then recorded from your Stripe
account, and a payment you also send here under its Stripe invoice or Checkout Session id is
refused with 409 recorded_from_stripe. A self-hosted install records them from its own
/webhooks/stripe endpoint, so don’t also send them here or they count twice: see
Stripe Billing.
Keep trackEvent for what Stripe doesn’t see: signups, trials, CRM stages, payments taken
elsewhere, and payments your backend charges directly with a PaymentIntent. Report refunds
of those to POST /attribution/refunds.
Error handling
Failures throw an OpenPartnerError with status (HTTP), code (machine-readable
string, e.g. invalid_body), and detail (the full response body):
try { await op.trackEvent({ userId, type: 'signup' });} catch (err) { if (err instanceof OpenPartnerError) console.error(err.status, err.code, err.detail);}