Skip to content

Conversions by click id

For senders that hold a click id but never saw an identify(). See Conversions by click id for when to use these.

POST /attribution/conversions

Stitches the click to a user and records the conversion in one transaction, then attributes it.

Auth: Admin API key OR scoped key with events:write.

Request

POST <base>/attribution/conversions
Authorization: Bearer <key>
Content-Type: application/json
{
"clickId": "01K7ZQ3M5X8V2N4P6R8T0W2Y4A",
"type": "loan_funded",
"value": 5000.00,
"currency": "USD",
"externalEventId": "loan-881:funded"
}
FieldTypeRequiredNotes
clickIdstringyesThe click id: the {click_id} macro’s value, or cref from the landing URL. A click from any program in your account.
typestringyesEvent name, standard or custom, e.g. loan_funded, opportunity_won, invoice_paid. Must match a commission rule’s event type exactly to pay.
externalEventIdstringyesYour stable id for this one conversion, up to 120 characters. A number is accepted as its string. Makes retries safe (see Idempotency).
valuenumberconditionalRequired for percent commissions. A JSON number in major units (49.99, dollars not cents), never a string. Omit it for a conversion with no value: null is refused.
currencystringnoISO 4217, any case (inr is stored as INR). Defaults to your default currency (Admin → Settings → Brand info, USD unless you changed it).
userIdstringnoYour own id for the user. Omit it and the conversion goes to the click’s own user, click:<clickId>.
metadataobjectnoFree-form. Stored on the Event row.
tsstringnoISO 8601 in UTC, ending in Z (2026-10-16T10:00:00Z). An offset such as +00:00 is refused. Defaults to server time.

Response

{
"ok": true,
"eventId": "01HXX...",
"userId": "click:01K7ZQ3M5X8V2N4P6R8T0W2Y4A",
"outcome": "attributed",
"attribution": {
"status": "attributed",
"model": "last_click",
"commission": "accrued",
"touches": [
{ "clickId": "01K7ZQ3M5X8V2N4P6R8T0W2Y4A", "partnerId": "01HXX...", "weight": 1, "attributionId": "01HXX...", "commissionId": "01HXX..." }
]
}
}
  • outcome says what the conversion came to: attributed (a partner was credited), no_matching_rule (credited, but no commission rule takes this type, so nothing is paid), or not_attributed (no partner credited).
  • reason, when present: with attributed, no_commission (a rule takes the type but nothing accrued: the program has ended, a first rule already paid, a cap was reached, or the amount was zero). With not_attributed, outside_window (the click is older than the program’s attribution window, or newer than ts), no_click (the partner was revoked) or corrective (a refund, chargeback, dispute_created or invoice_payment_failed type, recorded but never attributed). A replay of a conversion that wasn’t attributed carries no reason.
  • attribution has the same shape as for POST /attribution/events, plus commission: accrued, no_matching_rule or none.

Every recorded conversion is a 200, whatever it came to, so don’t retry on the outcome. Fix the cause and send the next conversion.

Errors

StatuserrorMeaning
400invalid_bodyA field is missing or malformed, such as a value sent as a string or null. detail says which.
404click_not_foundNo click with this id in your account. Nothing is recorded.
409external_event_id_conflictexternalEventId was already used for a different click, userId, type, value or currency. eventId names the original. This conversion is not recorded.
409recorded_from_stripeOnly with the OpenPartner Stripe App connected: externalEventId is a Stripe invoice or Checkout Session id, and that payment is recorded from Stripe itself.
422click_flaggedThe click was flagged (fraudFlag: velocity or revoked) and earns nothing. Nothing is recorded.

Idempotency

A retry with the same externalEventId returns the original eventId and attribution with "replayed": true, and records nothing new, including when duplicates arrive at the same moment, and even if the click has been flagged since.

Keys are unique across your whole account, in one namespace shared with /attribution/events, /postback, /coupons/redeem and the Stripe webhook. So:

  • One key per stage. A loan reported as approved and later loan_funded needs two keys, such as loan-881:approved and loan-881:funded.
  • Prefix keys per sender. Two suppliers’ order ids can collide: lender-a:loan-881:funded, lender-b:loan-881:funded.

Scope

Conversions are scoped to your account, not to a program. The click id already says which program the click came through, so there’s no programId to send. Keys with events:write, and the postback token, work across every program, so a sender holding one can report against any of your clicks.

GET /postback

The same conversion as a URL, for senders that can only fire one. Authenticated by your account’s conversion postback token, minted under Admin → Integrations → Postback URL (or with the endpoints below), never by an API key.

GET <base>/postback?click_id=01K7ZQ3M5X8V2N4P6R8T0W2Y4A&type=sale&amount=49.00&currency=USD&txn_id=T-1001&token=opcp_…
ParameterRequiredNotes
click_idyesThe click id. cref also works.
typeyesEvent name.
txn_idyesThe sender’s transaction id, used as externalEventId, with the same rules. transaction_id also works.
amountnoDecimal in major units. value also works. An empty value (an unfilled macro) means no amount.
currencynoISO 4217, any case. Empty or omitted: your default currency.
user_idnoYour own id for the user.
tokenyesThe postback token (opcp_…). Redacted from OpenPartner’s request logs.
pixelno1 to answer with a 1×1 GIF instead of text.

The token is in the URL, and proxies, browser history and the sender’s own logs keep query strings. Treat a postback URL as public once you’ve handed it out, always use https://, and prefer POST /attribution/conversions with a Bearer key when the sender can set a header.

Responses are plain text, not JSON, with Cache-Control: no-store. A 200 is ok followed by what the conversion came to:

BodyMeaning
ok attributedA partner was credited and earned commission.
ok attributed no_commissionCredited, but nothing accrued.
ok no_matching_ruleCredited, but no commission rule takes this type. Usually a typo’d stage name.
ok not_attributed outside_windowNo partner credited: the click is older than the program’s attribution window.
ok not_attributed no_clickNo partner credited: the partner was revoked.
ok not_attributed correctiveA corrective type, recorded but never attributed.
ok not_attributedA replay of a conversion that wasn’t attributed the first time.

All of these are 200 because the conversion is recorded, so match on the ok prefix. Errors are the bare code:

StatusBodyMeaning
400invalid_parameters: …, repeated_parameterNames the missing or malformed parameters, e.g. invalid_parameters: click_id, txn_id.
401missing_token, invalid_tokenNo token, or not your account’s live token.
403ip_not_allowedThe request didn’t come from an allowed source IP.
404click_not_foundNo click with this id in your account.
405HEAD isn’t accepted, so link checkers can’t record conversions.
409external_event_id_conflict, recorded_from_stripetxn_id was already used for a different conversion, or (with the Stripe App connected) is a Stripe invoice or Checkout Session id.
422click_flaggedThe click was flagged and earns nothing.

The token is checked before anything else, so a caller without one learns nothing about the parameters or your clicks.

Postback token

Auth: Admin only (admin API key or session). Scoped keys can’t manage the token.

Your account has one live token, and it covers every program. Rotate it when a sender leaves or a URL may have leaked.

EndpointDoes
GET <base>/conversion-postbackThe live token’s prefix, ipAllowlist, createdAt and lastUsedAt, or {"token": null}. Never the token itself.
POST <base>/conversion-postback/tokenMints a token, revoking the current one, which stops working immediately. Body: optional {"ipAllowlist": [...]}; without it, the current list carries over. Returns the plaintext token once.
PATCH <base>/conversion-postback{"ipAllowlist": ["203.0.113.0/24", "2001:db8::1"]}. Replaces the allowlist without rotating the token, so URLs already handed out keep working. IPv4, IPv6 and CIDR ranges, up to 50. An empty list accepts any source. 404 no_postback_token when postbacks are off.
DELETE <base>/conversion-postback/tokenRevokes the live token, turning postbacks off.

The allowlist is matched against the client address the API sees through one trusted proxy hop, so point senders at the API host directly.

GET /attribution/unmatched-event-types

Auth: Admin only.

Event types from the last 30 days that were credited to a partner but that no commission rule on the partner’s terms takes, so the partner earned nothing. A sender reporting funded when the program pays on loan_funded shows up here. Admin → Integrations shows the same list as a warning.

{
"since": "2026-09-17T10:00:00.000Z",
"eventTypes": [
{ "type": "funded", "events": 4, "lastSeenAt": "2026-10-16T09:12:00.000Z", "programs": [{ "id": "01J...", "name": "Personal loans" }] }
]
}

It’s checked against the rules as they are now, so adding a rule for the type, or fixing the sender, clears the entry for later events. OpenPartner’s own types (signup, trial_started, subscription_created, invoice_paid, refund, dispute_created, invoice_payment_failed) are left out, because a program that pays only on payments is meant to pay nothing on signups.