Payouts
Connect your own payout provider, set up partner payout profiles, record exchange rates, and create, execute, cancel and schedule payout runs.
A payout run collects every partner's approved, unpaid commission, turns it into one payout per partner, and sends it through a payout provider. Referalli never holds funds: payouts move money from the provider account your org connects. Commissions become paid in the ledger only after the provider confirms the money moved.
If you pay partners yourself, for example as account credit, you don't need payout runs: settle the lines instead. Settled lines never enter a payout run and aren't in the 1099 export.
1. Connect a payout provider
Store your own provider credentials with PUT /v1/payout-connections/{provider}. They are encrypted at rest and never returned by the API.
curl -X PUT https://referalli.com/v1/payout-connections/paypal \
-H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"clientId": "AYx...", "secret": "EFk...", "env": "live"}'
# { "provider": "paypal", "env": "live", "status": "active" }
| Body fields | Pays partners with method | Settlement |
|---|---|---|---|
|
|
| async (webhook) |
|
|
| immediate |
|
|
| async (webhook) |
|
|
| async (webhook) |
|
|
| async (webhook) |
|
|
| immediate |
env is sandbox or live. It defaults to sandbox for every provider except stripe, which defaults to live. Sending PUT again replaces the stored credentials.
GET /v1/payout-connections lists provider, env, status and connectedAt. DELETE /v1/payout-connections/{provider} removes one. All three require the owner, admin or finance role.
Connect a provider before paying with its method
If a partner's payout method has no connected provider, that payout fails with an error such as No PayPal connection: connect PayPal under Payments to pay this partner. Its status is failed, a payout.failed webhook fires, and its commissions stay approved, so the next run picks them up again once you connect the provider. Nothing is marked paid unless money moved. The csv method is the deliberate manual option: it marks payouts sent so you can pay them yourself outside Referalli.
2. Set up each partner
A partner is paid only when they have a payout profile and a tax form on file.
Payout profile
curl -X PUT https://referalli.com/v1/partners/ptr_.../payout-profile \
-H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{
"method": "paypal",
"details": { "paypalEmail": "dana@example.com" },
"payoutCurrency": "usd",
"minThresholdMinor": 5000
}'Field | Default | Notes |
|---|---|---|
| required |
|
|
| Recipient details for the method (below). |
|
| Currency the partner is paid in. Omit it to keep the stored value. |
|
| Skip the partner until their payable total reaches this amount. Omit it to keep the stored value. |
PUT replaces method and details. payoutCurrency and minThresholdMinor are optional: when you leave one out, the profile keeps its stored value, and the defaults above apply only when the partner has no profile yet. So re-saving a profile, by you or by the partner in the portal, never resets a threshold you set.
Recipient details each method reads:
Method |
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
| none |
Partners can set their own profile in the portal (PUT /v1/partner/orgs/{orgId}/payout-profile, same method, details and optional payoutCurrency fields). Partners can't set minThresholdMinor; their call keeps whatever threshold the profile has (5000 for a new one), and an omitted payoutCurrency keeps the stored currency (usd for a new one). If a partner picks a currency other than the one their commissions are in, the run needs an exchange rate for that pair from your org, or skips them with missing_fx_rate (see Exchange rates). That endpoint rejects a profile the method could never pay with 400, for example paypal payouts need a PayPal email or wise payouts need a Wise recipient id. PayPal and Tremendous details must be email addresses; the others must be non-empty. If the partner has two-factor sign-in on, the call also needs their current code in x-totp-code (see Authentication). The merchant PUT above does not validate details.
GET /v1/partners/{id}/payout-profile returns the method, currency, threshold and tax form status; it does not return details. It answers 404 no payout profile when the partner has none.
Change only the threshold
To change a partner's minimum without touching their method or details, use PATCH:
curl -X PATCH https://referalli.com/v1/partners/ptr_.../payout-profile \
-H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"minThresholdMinor": 2500}'
# { "partnerId": "ptr_...", "method": "paypal", "payoutCurrency": "usd", "minThresholdMinor": 2500 }minThresholdMinor is an integer of 0 or more, in the partner's payoutCurrency. The partner must already have a profile, or the call returns 404 no payout profile. It requires the owner, admin or finance role and is recorded in the audit log.
Tax form
curl -X POST https://referalli.com/v1/partners/ptr_.../tax-form \
-H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"type": "w9", "data": {"name": "Dana Example", "tin": "123-45-6789", "address": "1 Main St, Springfield"}}'
| Required |
|---|---|
|
|
|
|
|
|
Partners can also complete these steps themselves in the partner portal.
Identity checks (optional)
Set PATCH /v1/org with { "payoutKycRequired": true } to pay only partners whose identity is verified and who are not flagged by sanctions screening.
3. Create a payout run
Approve any lines whose hold has passed with POST /v1/commissions/approve-due (see the ledger); scheduled runs do this for you. Then:
curl -X POST https://referalli.com/v1/payout-runs \
-H "Authorization: Bearer $KEY"{
"runId": "prun_...",
"payouts": [
{
"payoutId": "pout_...",
"partnerId": "ptr_...",
"amountMinor": 6480,
"currency": "usd",
"rail": "paypal",
"fxApplied": [],
"invoice": { "number": "SBI-2026-14", "issuedAt": "2026-10-21T15:00:00.000Z", "lines": [ ... ], "vatNote": "" }
}
],
"skipped": [{ "partnerId": "ptr_...", "reason": "missing_tax_form" }]
}Creating a run moves no money. It builds a draft with one pending payout per eligible partner and a self-billing invoice for each.
A run only takes lines that are approved, not yet paid and not already in another payout that is pending (created but not executed) or submitted (waiting for an async provider). So two runs never pay the same line, and a draft run holds its lines until you execute or cancel it. Lines from a failed or cancelled payout are free again for the next run.
Partners with approved lines are left out, with a reason, when:
| Meaning |
|---|---|
| The partner is |
| No payout profile. |
| No tax form submitted. |
|
|
|
|
| Some of the partner's commission is in a currency other than their |
| Payable total, after conversion, is under |
Exchange rates
Commissions in a currency other than the partner's payoutCurrency are converted at an exchange rate your org records. The conversion is recorded on the payout in fxApplied (from, to, rate, ts), and each converted amount is rounded to the nearest minor unit.
curl -X POST https://referalli.com/v1/fx-rates \
-H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"base": "eur", "quote": "usd", "rate": "1.0825"}'{ "base": "eur", "quote": "usd", "rate": "1.0825", "effectiveAt": "2026-10-21T15:00:00.000Z", "source": "manual", "scope": "org" }Field | Rules |
|---|---|
| Three-letter currency code the commission was earned in. Case doesn't matter; it is stored lowercase. |
| Three-letter currency code the partner is paid in. Must differ from |
| How many |
| Optional ISO 8601 timestamp with offset. Defaults to now. |
A run uses the latest rate for the exact base → quote pair whose effectiveAt is at or before the run. A eur → usd rate is not used for usd → eur; record each direction you need. Recording a new rate never changes payouts already created.
GET /v1/fx-rates lists up to 500 rates, optionally filtered with ?base= and ?quote=: your org's rates first, then the platform ones, each sorted by pair with the newest rate first. Each item has scope: org for rates you recorded, platform for the sample rates that ship with Referalli.
Production uses only your rates
In production, payouts and the 1099 export convert only at rates your org recorded. The platform sample rates are a fallback for development and testing only (a deployment can override this with FX_PLATFORM_FALLBACK=on or off). If you pay partners in the currency you bill in, you need no rates at all.
Both calls require the owner, admin or finance role. Recording a rate is written to the audit log.
The daily rate feed
Instead of recording every rate yourself, you can turn on the daily feed of European Central Bank reference rates. It's public data and needs no key.
curl -X PUT https://referalli.com/v1/org/fx-feed \
-H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"enabled": true}'
# { "enabled": true, "source": "ecb", "synced": 6 }Turning it on fetches the latest rates straight away; after that they update daily. Rates are stored for the currencies your org uses (those of its commission lines and partners' payout currencies), in both directions, with source: "ecb" and effectiveAt set to the ECB's date. Cross rates go through the euro and are computed exactly, to 10 decimal places. GET /v1/org/fx-feed returns enabled and lastRateDate; POST /v1/fx-rates/sync fetches now.
A run picks the rate for a pair in this order:
your latest manual rate (
POST /v1/fx-rates), which pins the pair over the feed,the latest feed rate, while the feed is on,
in development only, the platform sample rates.
To hand a pinned pair back to the feed, clear its override: POST /v1/fx-rates/clear-override with {"base": "usd", "quote": "eur"}. Cleared rates keep their history and show retiredAt. With the feed off (the default), feed rates are ignored and only your own rates are used. All of these calls require the owner, admin or finance role and are audited.
4. Execute the run
curl -X POST https://referalli.com/v1/payout-runs/prun_.../execute \
-H "Authorization: Bearer $KEY"
# { "sent": 12, "submitted": 3, "failed": 1 }Each pending payout in the run is sent through the rail for its method, using your connected credentials:
Payout status | Meaning | Ledger |
|---|---|---|
| An immediate rail (Stripe, Tremendous, CSV) moved the money, or an async provider later confirmed it. |
|
| An async provider (PayPal, Circle, Wise, Payoneer) accepted it; the outcome arrives later by webhook. | Unchanged until confirmed. |
| The provider rejected it. | Unchanged, so the commission is payable again in the next run. |
The run's status is processing while any payout is still submitted, failed if payouts failed and none were sent, and completed otherwise. Executing a cancelled run sends nothing and returns { "sent": 0, "submitted": 0, "failed": 0 }.
Async settlement
Async providers report the final outcome to POST /webhooks/payouts/{provider} (paypal, circle, wise or payoneer). Each request must carry x-payout-signature: sha256=<hex HMAC-SHA256 of the raw body>, computed with the payout webhook secret configured on the Referalli deployment; unsigned or mis-signed requests get 401, and if no secret is configured the endpoint answers 503. A confirmed payout moves to sent and writes its paid events exactly once; a failed one moves to failed.
Cancel a run
A run you created but don't want to send can be cancelled:
curl -X POST https://referalli.com/v1/payout-runs/prun_.../cancel \
-H "Authorization: Bearer $KEY"
# { "cancelled": 4 }Every pending payout in the run becomes cancelled and emits payout.cancelled, and the run's status becomes cancelled. No money moves and no ledger events are written: the lines go back to being approved and unpaid, and the next run picks them up. cancelled is the number of payouts cancelled.
Response | When |
|---|---|
| The run had no payout |
| No run with that id in your org. |
| Some payout in the run is already |
Cancelling requires the owner, admin or finance role and is recorded in the audit log.
Scheduled runs
An org can have runs created and executed automatically on a schedule. Pick one of four kinds:
| Fields | Runs on |
|---|---|---|
|
| That day each month; 29 to 31 fall on the last day of shorter months. |
|
| That weekday every week. |
|
| Both days each month, clamped like |
|
| Exactly those days. |
curl -X PUT https://referalli.com/v1/org/payout-schedule \
-H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"schedule": {"kind": "weekly", "dayOfWeek": 5}}'
# { "schedule": { "kind": "weekly", "dayOfWeek": 5 }, "nextRunDate": "2026-10-23" }{"schedule": null} turns scheduled runs off. The original body, {"dayOfMonth": 1} (or null), still works and means a monthly schedule. Setting a schedule requires the owner, admin or finance role, and every change is audited. All days are UTC.
Per-program schedules
A program follows the org schedule unless it has its own:
curl -X PUT https://referalli.com/v1/programs/$PROGRAM/payout-schedule \
-H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"schedule": {"kind": "twice_monthly", "days": [1, 15]}}'schedule takes the same kinds, {"kind": "off"} to never pay that program automatically, or null to follow the org schedule again. GET /v1/org/payout-schedule returns the org schedule, its nextRunDate, and each program's override and nextRunDate.
What a scheduled run does
The API checks schedules every hour. On a day when any schedule is due it:
approves every line whose hold has passed, exactly like
POST /v1/commissions/approve-due,creates one run for the programs whose schedule is due (lines from other programs wait for their own day), with the same eligibility and skip reasons as above, and
executes it through your org's own payout connections, the same rails as a manual run.
There is at most one scheduled run per org per UTC day, however many times the check runs. A run with no eligible partners is still created, with no payouts. If a run fails before it is created, the next hourly check retries it. POST /v1/payout-runs/run-scheduled runs this for your own org straight away if a schedule is due today, and returns the result per run (orgId, runId, programIds, payouts, sent, failed).
Payout records
GET /v1/payout-runslists runs withstatus(draft,processing,completed,failedorcancelled),summary,createdAtandexecutedAt.summaryholdspayoutsandskippedfrom creation,sent,submittedandfailedafter execution, andcancelledafter a cancel.GET /v1/payouts?partnerId=…lists payouts, newest first and paginated (the latest 200 under2026-10-21), withid,runId,partnerId,amountMinor,currency,status(pending,submitted,sent,failedorcancelled),rail,railRef,failureReasonandsentAt.GET /v1/payouts/{id}/invoicereturns the self-billing invoice for one payout.GET /v1/tax/1099?year=2026returns one entry per W-9 partner with payouts sent that year. To file the forms and deliver them to partners, see Tax.
Each 1099 entry has partnerId, taxFormType, totalPaidUsdMinor, fxMissingCurrencies and requiresForm. Payouts in another currency are converted to USD at your org's latest rate on or before the end of the year. If a currency has no rate, its payouts are left out of totalPaidUsdMinor, the currency is listed in fxMissingCurrencies, and requiresForm is true so you review that partner by hand. Otherwise requiresForm is true at $600 (60000) or more.
Creating, executing and cancelling runs requires the owner, admin or finance role, as does the 1099 export.