Quota
/ docs
Dashboard
Docs/Developers/Stripe Connect payouts

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_earnings row 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.
Credits, dollars, and cents
1,000,000 credits = $1.00. The Stripe Transfer amount is computed by floor-dividing transfer credits by 10,000 to produce cents, so a fractional cent is dropped (Quota will not overpay you). If the transfer rounds to $0, the batch skips you.

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

POSThttps://api.usequota.ai/developers/connect/onboard

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

POSThttps://api.usequota.ai/developers/connect/onboard/refresh

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

GEThttps://api.usequota.ai/developers/connect/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_enabledbooleanStripe 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_enabledbooleanStripe will deliver transfers to your bank. The daily batch job skips developers where this is false.
details_submittedbooleanThe user finished filling out the hosted onboarding form. Independent of Stripe's downstream review.
onboarding_completedbooleanTrue when both details_submitted and payouts_enabled are true. Use this to decide whether to show the dashboard link.
disabled_reasonstring | nullStripe-supplied reason if the account is in a degraded state — e.g. requires_more_information, rejected.fraud.

04 Express Dashboard link

POSThttps://api.usequota.ai/developers/connect/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.

Onboarding must be complete
Returns 400 onboarding_incomplete if onboarding_completed is false. Returns 404 not_found if no Stripe account exists yet.
{
  "dashboard_url": "https://connect.stripe.com/express/..."
}

05 Earnings & payout history

GEThttps://api.usequota.ai/developers/earnings

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_creditsintegerLifetime gross markup earned across all eligible API calls. Includes amounts already paid out, currently in flight, and accumulating below the threshold.
pending_payout_creditsintegerEarnings past the 7-day chargeback window and at or above the $10 threshold. Will be transferred on the next batch run.
accumulating_creditsintegerEarnings past the 7-day window but below the $10 threshold. Carried forward to future batches.
reserve_held_creditsintegerThe 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_creditsintegerLifetime sum of transfer amounts (i.e. gross minus reserve) actually sent to your Stripe account.

Payout fields

gross_amount_creditsintegerTotal earnings rolled into this payout, before reserve withholding.
reserve_amount_creditsinteger10% of gross, withheld and held for 90 days.
transfer_amount_creditsintegerWhat was actually sent to Stripe (gross minus reserve).
earnings_countintegerNumber of underlying developer_earnings rows folded into this payout.
statusstringpaid, 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.

07 Errors

401unauthorizedNo valid developer session cookie.
404not_foundNo Stripe Connect account on this developer (refresh, status fallback path, dashboard-link).
400onboarding_incompleteDashboard link requested before onboarding finished.
409already_connectedPOST /onboard called when a Stripe account already exists. Use /onboard/refresh instead.
502stripe_errorStripe API call failed. Safe to retry — onboarding endpoints are idempotent at Stripe's layer for the same developer.