Stripe Connect payouts
When your app uses billing_mode: "user", end users pay for their own AI usage and the markup percentage you set accrues to your developer account. Stripe Connect is how that money reaches your bank.
01 Money flow
- A user-billed AI call charges the user’s balance for cost + markup. The cost portion stays with Quota; the markup portion accrues as a
developer_earningsrow owed to you. - Earnings become payout-eligible 7 days after the underlying API call (chargeback protection window).
- A daily batch job groups all eligible earnings per developer. If the total is below $10.00 (10,000,000 credits) the developer is skipped that day — the earnings carry over to tomorrow’s batch.
- On payout, 10% is withheld as a rolling reserve and the remaining 90% is transferred to your Stripe connected account. The reserve is released and rolled into a future payout 90 days later.
- The Stripe Transfer carries a deterministic idempotency key (
payout_<developer_id>_<hash>derived from the sorted set of earning IDs in the batch) so a retried batch run cannot create a duplicate transfer.
02 Onboarding
Before any earnings can be paid out, you create a Stripe Express connected account and complete Stripe’s hosted onboarding flow (KYC, bank account, tax info). Quota orchestrates this with two endpoints — one to start, one to resume.
Start onboarding
Creates the Stripe Express account, persists stripe_account_id against your developer record, and returns a Stripe-hosted onboarding URL. Redirect the user there.
curl -X POST https://api.usequota.ai/developers/connect/onboard \
-H "Cookie: quota_session=$SESSION_TOKEN"Response — 201 Created
{
"onboarding_url": "https://connect.stripe.com/setup/e/acct_1NxYz.../...",
"stripe_account_id": "acct_1NxYzABCDEFGHIJK"
}The onboarding_url is a single-use link that expires after a few minutes. If the user closes the tab or the link goes stale, use the refresh endpoint below to mint a new one.
Refresh an onboarding link
Call when an onboarding URL has expired or the user dropped out partway through KYC. Returns a fresh URL pointing at the same Stripe account — no state is lost. Returns 404 not_found if you have not started onboarding at all.
{
"onboarding_url": "https://connect.stripe.com/setup/e/acct_1NxYz.../..."
}03 Check status
Returns the live Stripe account state. Quota fetches it fresh from Stripe on each call and writes it back to the local mirror, so the body always reflects ground truth. If the Stripe API is unreachable, falls back to the local mirror and still returns 200.
Response — not connected
{
"connected": false
}Response — connected
{
"connected": true,
"stripe_account_id": "acct_1NxYzABCDEFGHIJK",
"charges_enabled": true,
"payouts_enabled": true,
"details_submitted": true,
"onboarding_completed": true,
"country": "US",
"default_currency": "usd",
"disabled_reason": null
}| charges_enabledboolean | Stripe will accept charges on this account. Always true for Quota's payout-only use, since Quota is the platform of record for the original charge. |
| payouts_enabledboolean | Stripe will deliver transfers to your bank. The daily batch job skips developers where this is false. |
| details_submittedboolean | The user finished filling out the hosted onboarding form. Independent of Stripe's downstream review. |
| onboarding_completedboolean | True when both details_submitted and payouts_enabled are true. Use this to decide whether to show the dashboard link. |
| disabled_reasonstring | null | Stripe-supplied reason if the account is in a degraded state — e.g. requires_more_information, rejected.fraud. |
04 Express Dashboard link
Returns a short-lived, single-use URL that signs the user into their Stripe Express Dashboard. From there they can update bank account details, view transfer history, and download tax forms.
{
"dashboard_url": "https://connect.stripe.com/express/..."
}05 Earnings & payout history
Returns the rollup that powers the dashboard’s Earnings page: a summary block plus the most recent payouts.
{
"summary": {
"total_earned_credits": 47820000,
"pending_payout_credits": 8450000,
"accumulating_credits": 1230000,
"reserve_held_credits": 2100000,
"total_paid_out_credits": 36040000
},
"recent_payouts": [
{
"id": "11111111-2222-3333-4444-555555555555",
"transfer_amount_credits": 13500000,
"reserve_amount_credits": 1500000,
"gross_amount_credits": 15000000,
"earnings_count": 312,
"status": "paid",
"period_start": "2026-04-22T00:00:00.000Z",
"period_end": "2026-04-29T00:00:00.000Z",
"created_at": "2026-05-06T08:00:14.000Z"
}
]
}Summary fields
| total_earned_creditsinteger | Lifetime gross markup earned across all eligible API calls. Includes amounts already paid out, currently in flight, and accumulating below the threshold. |
| pending_payout_creditsinteger | Earnings past the 7-day chargeback window and at or above the $10 threshold. Will be transferred on the next batch run. |
| accumulating_creditsinteger | Earnings past the 7-day window but below the $10 threshold. Carried forward to future batches. |
| reserve_held_creditsinteger | The 10% rolling reserve withheld from past payouts and not yet released. Released into the next batch 90 days after the originating payout. |
| total_paid_out_creditsinteger | Lifetime sum of transfer amounts (i.e. gross minus reserve) actually sent to your Stripe account. |
Payout fields
| gross_amount_creditsinteger | Total earnings rolled into this payout, before reserve withholding. |
| reserve_amount_creditsinteger | 10% of gross, withheld and held for 90 days. |
| transfer_amount_creditsinteger | What was actually sent to Stripe (gross minus reserve). |
| earnings_countinteger | Number of underlying developer_earnings rows folded into this payout. |
| statusstring | paid, pending, failed. Reflects Stripe Transfer state plus internal retry state. |
| period_startstring (ISO-8601) | Earliest usage_at across the batched earnings. |
| period_endstring (ISO-8601) | Latest usage_at across the batched earnings. |
06 Idempotency & retries
The daily batch job derives a deterministic Stripe idempotency key from the developer ID + the set of earnings being paid. Retrying a partial batch cannot create a duplicate Stripe Transfer. Concurrent runs are blocked at the database level — a second run that starts while the first is still in flight returns immediately with no-op.