Conversions and billing events
How billing events become conversions and commissions, and how to send them yourself with the generic conversion API.
Referalli learns about money from billing events: a paid invoice, a refund, a cancelled subscription. Each event is normalized into one canonical shape, stored once, and matched against the customer's referral. The match produces conversions (what happened) and, for payments, commission lines (what the partner earned).
Canonical event types
Every provider's webhook is translated into these types:
| Conversion recorded | Commission effect |
|---|---|---|
|
| Earns commission under the program's rule. |
|
| Reverses commission in proportion to the refunded amount. |
|
| Reverses everything that remains on the disputed invoice. |
|
| A trial bounty, if the rule has one, on the referral's first trial only. See Commission rules. |
|
| none |
| none | none |
| none | none. Carries only a customer link (below). |
A signup conversion is also recorded for every new referral, independently of billing. Suspended partners keep earning from new events, but their payouts are held until they are reactivated. Banned partners, and partners whose account the platform has flagged, earn nothing from new events; the conversions are still recorded. See Commission rules.
How an event is matched
For an invoice_paid event, Referalli finds referrals where:
the referral's
customerRefequals the event's customer (see Linking billing customers),the referral is
active(notpending_revieworvoid),the referral was created no later than the payment (with 5 minutes of tolerance for provider clock skew), and
the payment is not after the referral's
windowExpiresAt.
Refunds and disputes are matched by invoiceRef to the earlier invoice_paid event for the same invoice, so send the same invoiceRef on both. A refund of 2,500 against a 9,900 invoice reverses about a quarter of each commission line from that invoice.
customerRef is the join key
The simplest setup passes associate the same customer id your billing provider sends, for Stripe the cus_… id. If the referral was filed under another id, such as your own user id or an auth platform's user id, link the billing customer to it as described below.
Linking billing customers
When a referral's customerRef is not the id your billing provider uses, tell Referalli which referral the billing customer belongs to by putting referally_customer_ref in the provider's metadata, set to the referral's customerRef:
Provider | Where to put |
|---|---|
Stripe | Metadata on the customer, the subscription or the invoice. Subscription metadata that Stripe copies onto invoices ( |
Paddle | The transaction or subscription's |
Lemon Squeezy | Checkout custom data: pass |
Polar | The checkout |
Generic | The event's |
The first verified webhook that carries the key records an append-only identity link from the provider's customer id to the referral's customerRef. From then on every event for that billing customer (renewals, refunds, disputes, trials, cancellations) resolves to the referral, even when the metadata is absent. If the key later names a different customerRef, a new link is recorded and the most recent one wins. A referral filed directly under the provider's own customer id always takes precedence over a link. Payments that arrived under the provider's id before the first link, and matched nothing then, are reprocessed when the link is recorded: they earn under the same rules as if the link had existed (only inside the referral's window, from 5 minutes before its signup to its signup-to-conversion expiry), and never twice. Their conversions and commission lines are counted in the linking event's result. Chargebee and Shopify events carry no such metadata, so associate those customers with the provider's own id.
A billing customer with no referral and no link earns nothing, which is the normal outcome for customers who weren't referred.
Idempotency
Events are stored once per org, provider and provider event id. A redelivered webhook is acknowledged with "duplicate": true and has no further effect, so it is always safe for a provider to retry.
The generic conversion API
If your billing system has no built-in connector, or you want to send conversions from your own backend, use a generic billing connection.
1. Create the connection
curl -X POST https://referalli.com/v1/billing/connections \
-H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"provider": "generic", "name": "backend"}'{
"id": "bcon_01JB...",
"provider": "generic",
"webhookSecret": "whsec_6a1f...",
"webhookUrl": "/webhooks/billing/bcon_01JB..."
}The secret is shown once. You may supply your own with "webhookSecret" (at least 8 characters).
2. Send signed events
POST a JSON body with an events array to https://referalli.com/webhooks/billing/{connectionId}. Sign the exact raw body with HMAC-SHA256 using the connection secret, hex-encode it, and send it in x-webhook-signature.
- Node.js
- Python
import { createHmac } from 'node:crypto';
const body = JSON.stringify({
events: [
{
providerEventId: 'pay_7731',
type: 'invoice_paid',
customerRef: 'cus_NffrFeUfNV2Hib',
invoiceRef: 'inv_7731',
productRef: 'pro_plan',
amountGross: 9900,
tax: 0,
fees: 0,
currency: 'usd',
occurredAt: new Date().toISOString(),
},
],
});
const signature = createHmac('sha256', process.env.REFERALLY_BILLING_SECRET).update(body).digest('hex');
const res = await fetch(`https://referalli.com/webhooks/billing/${connectionId}`, {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-webhook-signature': signature },
body,
});import hashlib, hmac, json, os, urllib.request
from datetime import datetime, timezone
body = json.dumps({"events": [{
"providerEventId": "pay_7731",
"type": "invoice_paid",
"customerRef": "cus_NffrFeUfNV2Hib",
"invoiceRef": "inv_7731",
"amountGross": 9900,
"currency": "usd",
"occurredAt": datetime.now(timezone.utc).isoformat(),
}]}).encode()
signature = hmac.new(os.environ["REFERALLY_BILLING_SECRET"].encode(), body, hashlib.sha256).hexdigest()
req = urllib.request.Request(
f"https://referalli.com/webhooks/billing/{connection_id}",
data=body, method="POST",
headers={"content-type": "application/json", "x-webhook-signature": signature},
)
urllib.request.urlopen(req)Event fields
Field | Required | Type | Notes |
|---|---|---|---|
| yes | string | Your unique id for this event. The idempotency key. |
| yes | string | One of the canonical types above. |
| yes | string | The same value you passed to |
| yes | string | ISO 8601 timestamp of the event. |
| no | integer | Minor units. Default 0. For a refund, the refunded amount as a positive number. |
| no | integer | Minor units. Default 0. Used by rules that pay on net amounts. |
| no | string | Three letters. Default |
| no | string | Needed to match later refunds and disputes. |
| no | string |
|
| no | string | The referral's |
| no | integer | Stored for reporting. |
| no | object | See below. |
Metadata keys Referalli acts on:
couponCodeon aninvoice_paidevent: if the code is a partner coupon, the buyer is associated with that partner before the payment is processed.expansion: trueon aninvoice_paidevent: marks the payment as expansion revenue for rules withnewVsExpansionrates.trial: trueon asub_createdevent: records atrialconversion and pays the rule's trial bounty on the referral's first trial.referally_customer_refon any event: links this billing customer to a referral filed under that id.
Response
{
"received": 1,
"results": [
{ "duplicate": false, "billingEventId": "bev_...", "conversions": 1, "linesCreated": 1, "reversals": 0 }
]
}A bad signature returns 401. An unknown or inactive connection returns 404. A body that fails the schema returns 400.
Inspect billing events
GET /v1/billing/events lists normalized events, newest first and paginated (the latest 200 under 2026-10-21), optionally filtered with ?customerRef=. Each item has id, provider, type, customerRef, invoiceRef, amountGross, tax, fees, currency and occurredAt. GET /v1/billing/connections lists your connections and their status.
Test without a billing provider
POST /v1/sandbox/simulate creates a fake customer referred by a partner and pushes generated billing history through the real pipeline:
curl -X POST https://referalli.com/v1/sandbox/simulate \
-H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"programId": "prg_...", "partnerId": "ptr_...", "scenario": "subscription", "priceMinor": 9900, "months": 3}'The response reports the generated customerRef and the totals events, conversions, linesCreated and reversals. scenario is subscription (default), refund or churn; priceMinor defaults to 9900 and months (1 to 24) to 3. The simulated customer gets a referral and history that starts months months in the past. These commissions are real ledger entries in your org, so run simulations in a test org.