Attribution

How Referalli decides which partner gets credit for a customer, and how to configure and replay it.

Embed
Preview of the card
<a href="https://referalli.com/docs/attribution"><img src="https://referalli.com/docs/_api/public/orgs/07f432f4-34b9-46a7-aaad-c0e395d2e4f9/spaces/2bf4f614-3630-4fbf-85f9-5ecafa6b7d97/pages/9df5d8e5-bc02-43b1-bb8b-2d5d5a589e64/social-card.svg?v=3e7ae33eec8b" alt="Attribution" width="600" height="315"></a>

Attribution runs once per customer, inside POST /v1/referrals/associate. It gathers every candidate signal for that customer, applies the program's model and window, and either names a winning partner or returns no match. The decision and its reasoning are stored on the referral, and they can be recomputed at any time from the raw facts.

Channel priority

The engine tries four channels in a fixed order and stops at the first that produces a match:

Order

matchedVia

Signal

confidence

1

coupon

couponCode matches a coupon in the program. The coupon's partner wins outright.

1

2

deterministic

Clicks tied to this customer: the click named by referralToken, clicks recorded with the customer's anonId, and the last click the tracking script saw for that anonId.

1

3

email_hash

emailHash matches a hash recorded earlier (through identify or a previous association) for an anonymous visitor who has eligible clicks in the program. The most recent such click's partner wins.

0.9

4

probabilistic

No referral token or cookie, but clicks in the last 72 hours share the customer's exact hashed IP and user agent. Requires ip and ua on the associate call.

0.6

The email-hash channel only sees clicks that were stored with a visitor id: clicks from ?via= landings handled by the tracking script and clicks recorded with anonId through POST /v1/track/events.

Fingerprint matches

What happens to a probabilistic match depends on the program's probabilisticPolicy setting (see Program settings):

probabilisticPolicy

Effect

review (default)

The referral is created with status pending_review. It earns nothing until someone approves it with POST /v1/referrals/{id}/approve.

auto

The referral is created active, like any other match.

off

No referral is created; associate returns referral: null.

Partners who can't win

Before the channels run, associate drops candidates who can't be credited, and the next eligible channel or touch wins instead. Each skipped partner adds a line to reasoning.notes.

Partner

Why

banned (or archived) in your org

Banned partners earn nothing new, so their links and coupons stop attributing new customers. A suspended partner still wins: they keep earning, with payouts held.

Flagged by the platform

Same as banned.

externalRef equals the customer's customerRef

The customer is that partner, and nobody is credited for referring themselves.

This applies to live associate calls only. A referral that already exists keeps its partner, and unbanning a partner doesn't change any earlier decision. Replays don't apply it either.

When every candidate is skipped, referral is null and the response says why:

{ "referral": null, "reasoning": { "notes": ["partner ptr_... is banned; their touches were skipped"] } }

Self-referrals

When the customer's emailHash equals the hash of the winning partner's own email, it's a self-referral, whatever the channel (coupon, click, email hash or fingerprint). The program's selfReferralPolicy decides what happens:

selfReferralPolicy

Effect

review (default)

The referral is created pending_review, in the associate response itself, with a self-referral note. A self_referral fraud case opens and fraud.case_opened fires. Approve it with POST /v1/referrals/{id}/approve, or resolve the case.

reject

The self-referring partner is skipped like a banned one. Another eligible partner can still win; otherwise referral is null with a self-referral note.

For the click-based channels, a click counts only if it is:

  • eligible, meaning its fraud score is below 70 (see Links and clicks), and

  • inside the window: no more than cookieWindowDays days before the signup, and not after it.

Excluded clicks are listed in reasoning.notes, for example "2 outside the 60-day window" or "1 excluded by fraud filtering".

Burner emails and datacenter traffic

Referalli checks two signals itself, on top of what an auth platform sends in risk:

  • Burner emails. A sign-up whose email domain is on Referalli's disposable-email list (about 10,000 domains, from the community-maintained disposable-email-domains list, and their subdomains) is created as pending_review with an identity_risk fraud case. The domain comes from email on an Atlas sign-up, or from emailDomain on associate and on sign-ups that send only a hash. Turn it off per program with riskPolicy: { "holdDisposableEmail": false }. Each case also lowers the referring partner's quality score.

  • Datacenter IPs. A click from a published cloud or hosting range (AWS, Google Cloud, Azure, DigitalOcean, OVH, Hetzner, Linode, Vultr) adds 35 to its fraud score. That alone doesn't make it ineligible, but combined with another signal (high click velocity, for example) it crosses 70.

Both lists ship with Referalli and are refreshed with each release.

Partners banned elsewhere

When a partner account has been banned by at least two other merchants, or flagged by Referalli, the partner (GET /v1/partners/{id}) and enrollment lists carry networkWarning:

{ "flagged": true, "reason": "banned_by_other_merchants", "bannedByOtherMerchants": 2 }

It never says which merchants, and a single ban elsewhere isn't revealed (bannedByOtherMerchants stays 0). Your own ban doesn't count. The dashboard shows it as a badge on the partner page and on pending applications. It's a warning only: Referalli doesn't block the partner in your org.

Attribution models

When several eligible clicks qualify, the program's attributionModel assigns each a weight. The click with the highest weight wins; ties go to the most recent click. All weights are stored in touchCredits.

Model

Weights

last_click (default)

The most recent click gets 1, the rest 0.

first_click

The earliest click gets 1, the rest 0.

position_based

First and last clicks get 0.4 each and the middle clicks share the remaining 0.2. With two clicks, 0.5 each.

time_decay

Each click's weight halves every 7 days of age, then weights are normalized to sum to 1.

Set the model and windows on the program:

curl -X PATCH https://referalli.com/v1/programs/prg_... \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"attributionModel": "time_decay", "cookieWindowDays": 30}'

Field

Range

Default

Effect

attributionModel

see above

last_click

How multiple clicks are weighted.

cookieWindowDays

1 to 730

60

How far back before signup a click can count.

signupToConversionDays

1 to 1095

365

How long after signup payments still earn commission.

Program settings

Six more settings live on the program and are set with the same PATCH /v1/programs/{id} call (owner or admin). Send only the ones you want to change; the others keep their values. GET /v1/programs/{id} returns all six under settings, with defaults filled in.

curl -X PATCH https://referalli.com/v1/programs/prg_... \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{
    "probabilisticPolicy": "auto",
    "selfReferralPolicy": "reject",
    "splitCommissions": true,
    "allowPaidSearch": false,
    "challengeLinks": true,
    "utmTemplate": { "utm_source": "partner", "utm_campaign": "{{code}}" }
  }'

Field

Type

Default

Effect

probabilisticPolicy

auto, review or off

review

What a fingerprint-only match does (above).

selfReferralPolicy

review or reject

review

What a self-referral does: hold it for review, or skip that partner (Self-referrals).

splitCommissions

boolean

false

When on and a referral's touchCredits name more than one partner, each payment's direct commission is split across those partners by credit weight. The shares always add up exactly to the full commission, and each line's memo notes the partner's percentage. Sub-affiliate lines are not split.

allowPaidSearch

boolean

true

When off, a /go click that looks like paid-search traffic (a gclid, msclkid, fbclid or ttclid parameter, utm_medium of cpc, ppc or paid, or an ad-click referer from a search engine) gets 70 added to its fraud score, which makes it ineligible for attribution.

challengeLinks

boolean

false

When on, a /go link first answers with a small page that runs an invisible JavaScript check, then redirects. Clients that don't run JavaScript, such as headless click floods, never reach the destination and record no click.

utmTemplate

object

{}

UTM parameters added to every /go redirect. Keys must look like utm_source (utm_ plus lowercase letters and underscores), values are up to 500 characters, at most 20 keys. {{code}} in a value becomes the link code. A parameter already on the destination URL is not overwritten. Send {} to clear it.

allowPaidSearch, challengeLinks and utmTemplate apply to /go redirects only, not to ?via= landings handled by the tracking script. A value of the wrong shape returns 400 validation_error.

Explain a decision

Every referral stores reasoning: the model, the number of touchpoints considered, the window and notes. GET /v1/referrals/{id}/journey returns the referral with each touchpoint's timestamp, URL, fraud score, eligibility, weight, and which one won. Use it to answer "why did this partner get the credit".

Replay attribution

Raw facts (clicks and associations) are append-only. Referrals are derived from them, so after changing a program's model or window you can recompute every referral in the program:

curl -X POST https://referalli.com/v1/attribution/replay \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"programId": "prg_..."}'
# { "recomputed": 412, "changed": 9 }

Replay re-runs the engine for each customer as of their original signup time, using the program's current settings. It is deterministic: running it twice in a row changes nothing the second time. It only revisits customers that already have a referral; it never creates new ones. Referrals that were reassigned by hand (matchedVia: "manual") are left alone. Replay updates who the referral belongs to going forward; commission lines already earned are not moved.

Updated