Reporting payments, credit and costs
Feed Referalli the cash, credit and costs behind each payment so the credit ROI figures are exact: billing providers, the value API, CSV import, mapping profiles, precedence and inference.
Credit ROI answers one question: did the credit you gave away come back as real money? To answer it, Referalli needs to know, for each payment, how much was cash (money you collected) and how much was covered by credit (not money). This page covers every way to supply that, from nothing at all to a fully reported feed. For the merchant's view of the numbers, see Is your referral credit paying off?.
How the data fits together
Referalli keeps one append-only ledger of value events: payments, refunds, credit granted, credit redeemed, credit expired or revoked, costs and adjustments. Rows are never edited; a correction is a new event.
Each fact has a confidence:
Confidence | Where it comes from |
|---|---|
| You sent it: the value API, CSV import, or the redeem endpoints. |
| Read from a billing provider's webhook payload, by the built-in extractor or by your mapping profile. |
| Worked out by Referalli because nothing said how much credit a payment used. Inferred credit use is an assumption, shown as such, and never stored as a fact. |
You get a first number with no setup: connect a billing provider and Referalli reads cash from each payment and mirrors your referee rewards and settled commission credit as credit grants. Everything below sharpens that number.
Precedence
When several sources describe the same payment, exactly one row counts:
reportedbeatsextracted, which beatsinferred.At equal confidence, the first row recorded wins.
The exception is a re-extraction: a row with a higher
extraction_versionsupersedes a lower one at the same or lower confidence (this is how a new mapping profile replaces the built-in extractor). A re-extraction never overrides areportedfact.
Losing rows are kept, marked superseded, and left out of every total. A payment you report is matched to the billing payment it describes by provider plus invoiceRef, else by externalId (rows Referalli extracts from billing events carry their billing event internally; the API and CSV import don't take a billingEventId). Send provider and invoiceRef (the invoice or order id the provider uses) and your report replaces the extracted row for that payment instead of counting twice.
Stripe
Automatic. Connect Stripe as described in Billing providers; no further setup is needed. For each invoice.paid, Referalli reads:
Cash:
amount_paid.Credit applied: Stripe stores a customer's credit balance as a negative number. Credit consumed by an invoice is
ending_balance - starting_balance, counted only whenstarting_balanceis below zero, and capped at the balance the customer held. A customer with a $50.00 balance (starting_balance: -5000) and a $30.00 invoice that Stripe settles from the balance ends at-2000: credit applied is 3000, cash is 0.Discount: the sum of
total_discount_amounts[].amount.
A payment covered entirely by credit therefore counts as zero cash, which is the point.
Field reference by provider
This is what the built-in extractor reads from each provider's payload. All amounts end up in integer minor units; major-unit providers are scaled by the currency's exponent (JPY 0, USD 2, KWD 3) from the text, so no float error reaches the ledger.
Provider | Cash | Credit applied | Discount |
|---|---|---|---|
Stripe |
|
| sum of |
Chargebee |
|
| sum of |
Shopify |
| successful | sum of |
BigCommerce |
|
|
|
Recurly |
| sum of |
|
Paddle |
|
|
|
WooCommerce |
| not reported (0) |
|
Lemon Squeezy |
| not reported (0) |
|
Polar |
| not reported (0) |
|
Braintree |
| not reported (0) |
|
Notes:
"Not reported" does not mean "no credit". Referalli can't tell, so for those providers it falls back to inference, or to your own reports through the value API. Add a mapping profile (in the dashboard: Settings → Billing providers, connection menu, Map credit fields) if the provider's payload carries the credit somewhere else.
Shopify gift cards need the order's
transactions. If an order paid with agift_cardgateway arrives without them, Referalli claims nothing and lets inference apply rather than tag the payment as exact cash. Shopify store credit and loyalty points are not in the built-in fields; map them or report them.Providers or payloads with none of the fields above fall back to the event's gross amount as cash.
genericconnections have no built-in extractor. Send the amounts in a mapping profile or through the value API.
Shopify
Connect Shopify as in Billing providers. Cash and gift-card credit come from the order payload as above; discount codes still attribute buyers to partner coupons. To report what Shopify can't (store credit your app issued, cost of goods), add either a mapping profile that points at a field in the order, such as a note_attributes or metafield value you already include, or report from your backend with the value API, using the order's id as invoiceRef and provider: "shopify".
Custom platform
If you run your own billing, report every payment and credit event from your server with the value API. Send in batches and make every event idempotent with a stable externalId, so a retry never counts twice.
curl -X POST https://referalli.com/v1/value/events \
-H "Authorization: Bearer rk_live_..." \
-H "content-type: application/json" \
-d '{
"events": [
{
"kind": "credit_granted",
"role": "referee",
"customerRef": "cus_123",
"faceMinor": 1000,
"currency": "usd",
"externalId": "grant-cus_123-welcome",
"occurredAt": "2026-10-20T09:00:00Z"
},
{
"kind": "payment",
"customerRef": "cus_123",
"cashMinor": 2400,
"creditAppliedMinor": 1000,
"currency": "usd",
"productRef": "pro-monthly",
"provider": "acme-billing",
"invoiceRef": "inv_8841",
"externalId": "pay-inv_8841",
"occurredAt": "2026-10-21T09:00:00Z"
},
{
"kind": "cost",
"customerRef": "cus_123",
"costMinor": 600,
"currency": "usd",
"productRef": "pro-monthly",
"externalId": "cost-inv_8841",
"occurredAt": "2026-10-21T09:00:00Z"
}
]
}'{
"results": [
{ "index": 0, "id": "val_01JB...", "created": true, "superseded": [] },
{ "index": 1, "id": "val_01JB...", "created": true, "superseded": [] },
{ "index": 2, "id": "val_01JB...", "created": true, "superseded": [] }
],
"errors": []
}Send the same batch again and every result comes back "created": false: nothing is recorded twice. Each event is validated and recorded on its own, so a bad event appears in errors as { "index": 3, "message": "..." } while the rest are recorded. A request holds 1 to 500 events. Reported facts are always source: "api" and confidence: "reported"; you can't choose either.
The value API
POST /v1/value/events needs an API key with the analytics:write scope (or a full key). The body is { "events": [ ... ] }, and unknown fields are rejected.
Field | Type | Notes |
|---|---|---|
| string | Required: |
| string | Required. Your unique id for this fact; the idempotency key. |
| string | Required. ISO 8601 date or datetime. |
| string | Required. Three letters; stored lowercase. |
| string | At least one is required. |
| integer | Real money. For a |
| integer, 0 or more | Part of the payment covered by credit. For |
| integer, 0 or more | Discount, tax and processor fees contained in the payment. |
| integer, 0 or more | Your cost: the cost of the payment, or a |
| integer, 0 or more | Face value of the credit. Required and positive for |
| string |
|
| string | The Referalli credit grant a redemption, expiry or revocation closes. |
| string | Optional links. |
| string | Together they match this payment to the billing payment it describes, so the two aren't counted twice. |
| object | Free-form. |
How each kind is used:
paymentrecords cash, credit applied, discount and optionally a cost.refundreturns real money. Credit refunded to the customer's balance iscreditAppliedMinor.credit_grantedregisters free value you gave, as anexternalcredit grant. Referee credit costs you when it is used; referrer credit costs you when you settle it (see cost model).credit_redeemedrecords credit used outside a payment. WithcreditGrantIdit closes that grant; without an amount it uses the grant's whole remaining balance.credit_expiredandcredit_revokedclose the rest of a grant, so it costs nothing further.costis a cost of goods or of serving a customer, linked to a payment byinvoiceReforproductRef.adjustmentcorrects an earlier fact with a signedcashMinor.
Import from a CSV
If you can't call an API, upload a CSV. POST /v1/value/import takes up to 10,000 rows and 5 MB, and the same scope as the value API.
curl -X POST https://referalli.com/v1/value/import \
-H "Authorization: Bearer rk_live_..." \
-H "content-type: application/json" \
-d '{
"csv": "order_id,email,paid,store_credit,date\nA-1001,ada@example.com,24.00,10.00,2026-10-21\nA-1002,bo@example.com,49.00,0,2026-10-22\n",
"kind": "payment",
"defaultCurrency": "usd",
"dryRun": true,
"mapping": {
"order_id": "externalId",
"email": "customerRef",
"paid": "cashMajor",
"store_credit": "creditAppliedMajor",
"date": "occurredAt"
}
}'mapping maps each CSV column header to a field. Targets are the text fields of the table above (kind, customerRef, partnerId, partnerExternalRef, creditGrantId, referralId, programId, productRef, currency, externalId, invoiceRef, provider, occurredAt, role) and each money field as <name>Minor (an integer) or <name>Major (a decimal like 12.34, converted by the currency's exponent; more decimals than the currency allows is an error). The money names are cash, creditApplied, discount, tax, fees, cost and face.
kindapplies to every row unless a column is mapped tokind. One is required.defaultCurrencyfills rows with no currency column. Major-unit columns need a currency.Use a
Minoror aMajorcolumn for a field, not both.With no
externalIdcolumn, Referalli derives one from the row's content (csv_followed by a hash), so uploading the same file twice records nothing new. Rows that are identical in every mapped column collapse into one, so include a unique id column when real orders can repeat.With
"dryRun": truenothing is recorded. The response lists the first 20 normalised rows, every row error and thetotalrow count, so you can check the mapping first.
{ "rows": [ { "kind": "payment", "customerRef": "ada@example.com", "cashMinor": 2400, "creditAppliedMinor": 1000, "currency": "usd", "externalId": "A-1001", "source": "api", "confidence": "reported", "occurredAt": "2026-10-21T00:00:00.000Z" } ], "errors": [], "total": 2 }A real run returns { "recorded": 2, "duplicates": 0, "errors": [] }. Row numbers in errors are spreadsheet rows, with the header as row 1. A bad row never blocks the others. A file over 5 MB or 10,000 rows gets 413.
A file of more than 500 rows is recorded in the background: the response is 202 with { "jobId": "vjb_..." }. Poll GET /v1/value/import/{jobId} until status is done (or failed); it returns recorded, duplicates and errors in the same shape, plus processed and total rows so far. A dry run always answers at once.
Mapping profiles
When your billing provider sends credit or cost in a field the built-in extractor doesn't read, tell Referalli where it is. A mapping profile belongs to one billing connection and says which path in the provider's payload holds each amount.
curl -X PUT https://referalli.com/v1/billing/connections/bcon_01JB.../value-mapping \
-H "Authorization: Bearer rk_live_..." \
-H "content-type: application/json" \
-d '{
"cash": "data.object.amount_paid",
"creditApplied": "data.object.metadata.store_credit_used",
"discount": "data.object.discounts[].amount",
"cost": "data.object.metadata.cogs",
"productRef": "data.object.lines.data[].price.product",
"unit": "minor"
}'{
"id": "bcon_01JB...",
"valueMapping": {
"cash": "data.object.amount_paid",
"creditApplied": "data.object.metadata.store_credit_used",
"discount": "data.object.discounts[].amount",
"cost": "data.object.metadata.cogs",
"productRef": "data.object.lines.data[].price.product",
"unit": "minor",
"version": 2
}
}Paths. A path is dot-separated keys into the raw webhook body, such as data.object.amount_paid. A key ending in [] walks an array and sums every number it reaches: data.discounts[].amount adds up all discount amounts. Allowed characters are letters, digits, _, $ and - in each key. productRef reads the first non-empty string at its path.
Units. unit: "minor" means the numbers are already minor units. unit: "major" means decimals such as "12.34" or 12.34, scaled by the payment currency's exponent and rounded half-even on the exact digits.
Defaults. An unmapped or unreachable path reads as 0 for credit and discount. If cash is not mapped, the event's gross amount is used. Credit, discount and cost are stored as magnitudes. A path that holds something unreadable (not a number) is read as 0 and reported as a warning.
Versions. The built-in extractor is version 1, so your first mapping saves as version 2, and every later save adds one. The version is stored on each fact it extracts. Because a higher version supersedes a lower one, a new mapping's values replace the older version's for the same payment (see precedence).
Only events after the first mapping. A raw provider payload is stored only for connections that have a mapping. So a mapping applies to events Referalli receives after the connection first had a mapping, and a later version re-maps those on Rebuild numbers (or POST /v1/profitability/rebuild). Events received before the connection's first mapping keep the value the built-in extractor gave them, because there's no payload to re-read.
Check a mapping before you save it. The preview writes nothing, and takes either the saved mapping or a draft:
curl -X POST https://referalli.com/v1/billing/connections/bcon_01JB.../value-mapping/preview \
-H "Authorization: Bearer rk_live_..." \
-H "content-type: application/json" \
-d '{
"currency": "usd",
"payload": { "data": { "object": { "amount_paid": 2400, "metadata": { "store_credit_used": 1000 } } } },
"mapping": { "cash": "data.object.amount_paid", "creditApplied": "data.object.metadata.store_credit_used", "unit": "minor" }
}'The response is { mappingVersion, currency, value, wouldRecord }: the amounts the mapping reads, the raw numbers it used, any warnings, and the value events it would record. For a generic connection that batches events ({ "events": [...] }), it returns one result per element. Saving and previewing a mapping needs the owner or admin role, and saves are audited.
Inference
When a payment doesn't say how much credit it used, Referalli still has to decide whether that payment was cash. By default it infers, and labels the result:
Next payments, first in first out. For a credit grant with no reported use, each later payment that reports no credit applied is treated as paid, in part, with the credit: Referalli takes the smaller of the grant's remaining balance and that payment's cash, oldest payment first, until the grant is used up. The cash that credit covered is not counted as real money.
Same currency only. A grant is drawn down only by payments in its own currency.
Credit from reported payments is drawn first. When a payment does report credit applied, it is taken from the customer's open grants oldest grant first (
grants_first, the default), or spread by remaining balance if you setcreditDrawtopro_rata. Anything beyond the open grants' balances is "other credit" and is recorded nowhere.Reports win. Once a grant has a reported use (a linked redemption, or a payment that reported credit applied), inference stops for that grant.
The profitability settings choose the rule: fifo_next_payments (default), on_grant (the whole face counts as used when granted) or report_only (never infer). The data-quality report shows what share of the figures is inferred. Reporting credit applied removes the guess.
Margin and cost settings
Cash becomes profit through a margin. There is no default margin: with none set, revenue is shown and profit stays unknown. Merchants set these under Settings → Credit ROI and Product margins in the dashboard; the API below does the same.
curl -X PUT https://referalli.com/v1/profitability/settings \
-H "Authorization: Bearer rk_live_..." \
-H "content-type: application/json" \
-d '{ "defaultMarginBps": 6000, "windowDays": 180, "targetRoiBps": 20000 }'Add "programId": "prg_..." to override a program (a program override can't change the reporting currency). Requires owner or admin; every change is audited. GET /v1/profitability/settings?programId=... returns the resolved settings.
Setting | Meaning |
|---|---|
| Currency every converted figure is shown in. Default |
| Margin in basis points (6000 is 60%). |
|
|
|
|
|
|
| How long after a grant to count payments: 30, 90, 180 (default), 365, or 0 for lifetime. |
| Days without a payment after credit runs out that count as churn. Default 30. |
| Count a linked referrer's own payments toward their credit. Default off. |
| Optional cost to serve one active customer per month. |
| Subtract tax and fees before margin. Default off. |
| 0 to 1: how hard the credit patterns weigh on a partner's quality score. Default 0 (off). |
| Target ROI as a multiple in basis points (20000 is 2.0×). A program can set |
The margin for a payment comes from the first of these that exists:
A
costMinoron the payment itself.costevents linked to the payment.The product catalog entry for its
productRef.The program's
defaultMarginBps.The org's
defaultMarginBps.None: profit is unknown.
The product catalog is PUT /v1/profitability/products/{productRef} with exactly one of marginBps or unitCostMinor (the cost of one payment), plus GET and DELETE on the same path and GET /v1/profitability/products.
After changing settings, mappings or history, Rebuild numbers in the dashboard, or POST /v1/profitability/rebuild replays your billing history into the value ledger and recomputes everything derived (allocations and rollups). It returns a job id; poll GET /v1/profitability/rebuild/{jobId} for status, processed and total.
Mobile app
Never put an API key in a mobile app: it can be extracted and used by anyone. Report from your own server, which already knows what the customer paid. The app tells your server about the purchase (or your server hears from the app store); your server calls Referalli.
import { ReferallyClient } from '@referally/sdk-js';
const referalli = new ReferallyClient({
apiKey: process.env.REFERALLI_API_KEY!, // rk_live_..., server-side only
baseUrl: 'https://referalli.com',
});
// In the handler your app calls after a purchase is verified:
await referalli.reportPayment({
customerRef: user.id,
cashMinor: 999,
creditAppliedMinor: 0,
currency: 'usd',
externalId: `iap-${transactionId}`, // the store's transaction id: a retry won't double count
occurredAt: new Date().toISOString(),
});The server client has one helper per kind: reportPayment, reportCreditGranted (needs role and faceMinor), reportCreditUsed (needs creditAppliedMinor), reportCost (needs costMinor), and reportValueEvents for a batch. See the SDKs page for the client options. For attributing the purchase to a referral in the first place, see In-app purchases.
Zapier
The Report payment or credit action in the Zapier app sends one value event. Its amount is entered in the currency's main unit (49.00) and converted for you, and its Event id is the externalId, so a replayed Zap records nothing twice.
Read the results
All of these accept from, to, programId, partnerId, windowDays and currency, and return CSV with format=csv. They need the analytics scope.
Call | Returns |
|---|---|
| Credit cost, real money after, profit, net, ROI, share paid back, median payback days, coverage, and the outstanding credit liability. |
| The same figures per |
| Every credit grant with its status, use and payback. Filter by |
| One customer's timeline of grants, uses, payments and costs, with a running net. |
| The same for a partner and their referrals, including commissions. |
| All four named patterns in one call. |
| Turn one match into a fraud case. Owner or admin. |
| How complete the data is: payments reporting credit, products with a margin, partners linked to a customer id, and the gaps with a fix for each. |
In the summary, profitAfterMinor, netMinor and roi are null when profit is unknown. null means unknown, never zero. roi is profit after credit divided by credit cost (2.1 means each unit of credit returned 2.1).
Webhooks and tools
The webhook topics billing_event.recorded, credit.redeemed, roi.payback_reached and roi.pattern_detected let you react to this data; referally trigger <topic> sends a sample of each. The MCP server has read-only roi_summary, roi_trace and roi_patterns tools, so an assistant can explain a figure from the same data.