Payouts & the Money Lifecycle
This page explains how donation money moves through FundlyHub — from the moment a donation is captured to the moment it lands in a creator's bank account — and documents the payout endpoints the API exposes: the creator surface, the Stripe Connect surface, and organization payouts.
Money units are not uniform
There is no single currency unit across these endpoints. Read the field name:
- Fields ending in
_cents/Centsare integers of cents (platform_held_settled_cents,amountCents, …). The suffix is a reliable signal that a field is cents, but not that a field without it isn't —amounton a Stripe payout row is cents too. GET /payouts/earningsreturns integer cents — all six fields, each named with a_centssuffix.- Every amount on the creator and Stripe Connect payout surface is integer cents.
The lifecycle: held → settled → released → paid out
A paid donation passes through a sequence of states. FundlyHub's ledger is the source of truth; Stripe is the custodian of the actual money.
| Stage | What it means |
|---|---|
| Held | The net amount sits on FundlyHub's platform balance, owed to the creator. This happens when the donation was captured before the creator finished Stripe Connect onboarding — Stripe's own balance knows nothing about it. |
| Settled | The underlying card charge has cleared (Stripe's available-on date has passed). Only settled held funds are picked up by the automatic release. |
| Released | A Stripe transfer moves the settled net amount from the platform balance into the creator's connected account. |
| Paid out | Stripe pays the creator's connected-account balance out to their external bank, either on their automatic schedule or via a manual payout. From the creator's perspective the money has "left Stripe" once a payout is paid or in_transit. |
For creators who onboarded before receiving donations, money is captured directly as a Stripe destination charge — it never enters the held bucket and shows up in the creator's Stripe balance immediately (subject to the usual card-settlement delay).
Three vocabularies, one lifecycle
The four words above describe the lifecycle. The API's field and status names map onto them but are not the same strings (there is no settled or paid_out value anywhere in a response). Creators see a third, deliberately plainer set of words in the product. Before quoting an API value to a creator — or reading a support ticket back into the API — check the mapping table: What the words mean in the creator Payouts guide.
Creator endpoints
All of these require an authenticated session and are rate-limited to 100 requests per minute per account (see Rate Limits). They always derive the target creator from the authenticated principal — a ?userId query parameter is ignored, never honoured (#1072).
| Endpoint | Purpose |
|---|---|
GET /payouts/earnings | The Earnings/Funds tab summary for the authenticated creator. |
GET /payouts/earnings/pending-breakdown | Per-donation attribution of the pending money, lazy-loaded by the tile's dialog. |
GET /stripe/accounts | The creator's Stripe Connect account rows plus live onboarding flags. |
GET /payouts/earnings
Returns 200 with six integers, in cents:
{
"total_cents": 105116,
"pending_cents": 2022,
"available_cents": 0,
"withdrawn_cents": 0,
"ready_to_pay_out_cents": 103094,
"fees_cents": 3471
}| Field | How it is computed |
|---|---|
total_cents | Canonical ledger figure: donation credits minus Stripe processing fees, for every fundraiser the caller owns. Platform tips are a different ledger bucket and are not included. |
fees_cents | The Stripe processing fees subtracted above. |
pending_cents | With a payouts-enabled connected account: Stripe's balance.pending plus the net of any still-held platform rows (Stripe's balance knows nothing about those). Without one: total_cents - ready_to_pay_out_cents. |
available_cents | Stripe's balance.available, summed across currencies. Deliberately 0 when there is no connected account — this value gates the withdraw button, and a non-zero one with nowhere to send it produced "available, but withdraw errors". |
withdrawn_cents | Sum of the account's Stripe payouts whose status is paid or in_transit — money that has left the Connect balance toward the bank, including automatic-schedule payouts. 0 without a connected account. |
ready_to_pay_out_cents | Only non-zero for creators without a payouts-enabled connected account: the net of held rows that have already settled, clamped to total. This money is the creator's and has cleared; the sole blocker is their own onboarding. Stays 0 once they onboard, because those rows are then folded into pending. |
If Stripe cannot be reached, available / pending / withdrawn are estimated from FundlyHub's own records so the page still renders; total and fees are unaffected. Errors: 401 unauthenticated, 500 on failure.
GET /payouts/earnings/pending-breakdown
Returns 200 with the money that is not yet available, attributed donation by donation:
{
"pending_breakdown": {
"has_payout_account": false,
"total_cents": 105116,
"destination_charge_cents": 0,
"platform_held_unsettled_cents": 2022,
"platform_held_settled_cents": 103094,
"donations": [
{
"donation_id": "…",
"amount_cents": 4500,
"fundraiser_title": "Rebuild the community kitchen",
"fundraiser_slug": "rebuild-the-community-kitchen",
"state": "platform_held_settled",
"available_on": "2026-08-30T00:00:00.000Z",
"donated_at": "2026-08-24T13:02:11.000Z"
}
]
}
}amount_cents is the net for that donation (the donation minus its Stripe fee). Each row carries one of exactly three states:
state | Meaning |
|---|---|
destination_charge_pending | Stripe already holds it for this creator; the card charge has not settled yet (typically 2–7 days). |
platform_held_unsettled | On FundlyHub's platform balance, awaiting settlement. Releases automatically once the charge settles. |
platform_held_settled | On the platform balance and already cleared — eligible for release now. |
has_payout_account reports whether the creator has a connected account that is charges_enabled, payouts_enabled and not paused — i.e. whether the automatic release will ever act on these rows. Clients must branch on it: with no such account, settled held money is waiting on the creator's onboarding and will not move on its own.
Donations already transferred, settled destination charges, rows with a non-positive net, and rows under review are excluded. Errors: 401 unauthenticated, 500 on failure.
GET /stripe/accounts
Returns 200 with an array (usually zero or one element) of the caller's connected accounts — stripe_account_id, charges_enabled, payouts_enabled, details_submitted, onboarding_complete, country, default_currency, created_at — with requirements (Stripe's currently_due / past_due / eventually_due / disabled_reason) attached to the first row when Stripe could be reached.
Each call refreshes from Stripe, with two deliberate behaviours worth knowing before you build on it:
- Transient downgrades are suppressed. If Stripe reports
charges_enabled: false/payouts_enabled: falsefor a previously enabled account but names no concrete reason (nodisabled_reason, nothing inpast_dueorcurrently_due), the previously enabled state is kept and nothing is persisted. The freshrequirementsare still returned. - Deleted accounts are cleaned up. If Stripe answers
account_invalid/ "No such account", FundlyHub forgets the account and the response is[], which clients should treat as "onboarding not started".
Errors: 401 unauthenticated, 500 on failure.
Stripe Connect endpoints
The onboarding, transfer-history and payout-schedule surface behind the creator's Funds tab. All require an authenticated session and the same 100/minute limit. These do not have generated reference pages yet, so they are listed here without links.
| Method & path | What it does |
|---|---|
POST /stripe/connect/accounts | Create (or return) the caller's Connect account. |
GET /stripe/connect/accounts/{accountId}/status | Live status for one account. |
POST /stripe/connect/sessions | Mint an Account Session for Stripe's embedded onboarding components. |
GET /stripe/connect/transfers | Transfers to the caller's Stripe balance. limit (default 10, max 100) and offset. |
GET /stripe/connect/payouts | Payout history, read live from Stripe (automatic-schedule payouts included), with FundlyHub's own records as a fallback when Stripe is unreachable. |
POST /stripe/connect/payouts | Manual "Transfer to Bank". |
GET /stripe/connect/payout-settings | External accounts (bank accounts, debit cards) and the current payout schedule. |
PUT /stripe/connect/payout-schedule | Change the payout schedule. |
GET /stripe/connect/express-login | A Stripe Express Dashboard login link. |
POST /stripe/connect/payouts
Body: { "amountCents"?: number }. Omit amountCents to pay out the entire available balance. Returns 200 with { payoutId, amount, currency, status, arrivalDate }, where status is Stripe's payout status and arrivalDate is an ISO string or null.
Guards, all answering 400:
- no payouts-enabled Connect account for the caller;
availablebalance is zero ({ error, availableBalance: 0 });amountCentsdoes not parse to a positive number;amountCentsexceeds the available balance (the response echoesavailableBalancein cents);amountCentsis below the $1.00 minimum (100).
Stripe errors are surfaced as 400 with Stripe's own message and code.
This endpoint sits behind a killswitch
POST /stripe/connect/payouts is the one payout endpoint gated on the features.payouts feature flag. While FundlyHub has it switched off, the response is 403 { error: "Feature disabled", message, feature_key: "features.payouts" }.
PUT /stripe/connect/payout-schedule
Body: { interval, weeklyAnchor?, monthlyAnchor? }. interval must be one of manual, daily, weekly, monthly; anything else is 400. weekly requires weeklyAnchor (e.g. "monday") and monthly requires monthlyAnchor (1–31), each 400 when missing. Returns { interval, weeklyPayoutDays, monthlyPayoutDays, delayDays }. 404 when the caller has no connected account.
Organization payouts
Organizations pay out through their own Connect account, reached from the org-admin surface. Both are scoped by org slug and gated by organization permissions (see Roles & Permissions); neither has a generated reference page yet.
| Method & path | Permission | Behaviour |
|---|---|---|
POST /org-admin/{slug}/payouts/connect | org.manage_payouts (org owner) | Create-or-refresh the org's Connect account and return a fresh onboarding link. Idempotent. 201 with { data: { accountId, onboardingUrl, status } }, where status is existing or pending. |
GET /org-admin/{slug}/payouts/status | org.read_payouts (any member) | Live Stripe status: { data: { connected, accountId?, chargesEnabled?, payoutsEnabled?, detailsSubmitted?, defaultCurrency?, country?, businessType?, requirementsCurrentlyDue? } }. Returns { connected: false } when no account exists, and degrades to the locally cached flags if Stripe is unreachable. |
Both answer 503 when the org Stripe Connect feature is disabled.