Conversions and billing events

How billing events become conversions and commissions, and how to send them yourself with the generic conversion API.

Embed
Preview of the card
<a href="https://referalli.com/docs/conversions"><img src="https://referalli.com/docs/_api/public/orgs/07f432f4-34b9-46a7-aaad-c0e395d2e4f9/spaces/2bf4f614-3630-4fbf-85f9-5ecafa6b7d97/pages/4981282f-374b-47de-9c29-5ee444c4c633/social-card.svg?v=3e7ae33eec8b" alt="Conversions and billing events" width="600" height="315"></a>

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:

type

Conversion recorded

Commission effect

invoice_paid

payment

Earns commission under the program's rule.

refund

refund (negative amount)

Reverses commission in proportion to the refunded amount.

dispute

refund

Reverses everything that remains on the disputed invoice.

sub_created

trial, if the event's metadata has trial: true

A trial bounty, if the rule has one, on the referral's first trial only. See Commission rules.

sub_canceled

churn

none

sub_updated

none

none

customer_updated

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 customerRef equals the event's customer (see Linking billing customers),

  • the referral is active (not pending_review or void),

  • 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 referally_customer_ref

Stripe

Metadata on the customer, the subscription or the invoice. Subscription metadata that Stripe copies onto invoices (subscription_details.metadata) works too. Referalli reads it from customer.created and customer.updated events as well as payment and subscription events.

Paddle

The transaction or subscription's custom_data.

Lemon Squeezy

Checkout custom data: pass checkout[custom][referally_customer_ref], which arrives as meta.custom_data.

Polar

The checkout metadata, which Polar copies to the order or subscription.

Generic

The event's linkedCustomerRef field, or metadata.referally_customer_ref. Send a customer_updated event to record a link with no money attached.

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.

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

Event fields

Field

Required

Type

Notes

providerEventId

yes

string

Your unique id for this event. The idempotency key.

type

yes

string

One of the canonical types above.

customerRef

yes

string

The same value you passed to associate.

occurredAt

yes

string

ISO 8601 timestamp of the event.

amountGross

no

integer

Minor units. Default 0. For a refund, the refunded amount as a positive number.

tax, fees

no

integer

Minor units. Default 0. Used by rules that pay on net amounts.

currency

no

string

Three letters. Default usd.

invoiceRef

no

string

Needed to match later refunds and disputes.

subscriptionRef, productRef

no

string

productRef can select a product-specific commission override.

linkedCustomerRef

no

string

The referral's customerRef, when it differs from customerRef. See Linking billing customers.

mrrDelta

no

integer

Stored for reporting.

metadata

no

object

See below.

Metadata keys Referalli acts on:

  • couponCode on an invoice_paid event: if the code is a partner coupon, the buyer is associated with that partner before the payment is processed.

  • expansion: true on an invoice_paid event: marks the payment as expansion revenue for rules with newVsExpansion rates.

  • trial: true on a sub_created event: records a trial conversion and pays the rule's trial bounty on the referral's first trial.

  • referally_customer_ref on 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.

Updated