Skip to content

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

EventUse for
signupUser account creation. No revenue yet — still useful for funnel analysis.
trial_startedTrial subscription started.
subscription_createdPaid subscription created.
invoice_paidRecurring 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'

See Conversions by click id.

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);
}