Home/Docs/Stripe Connect Affiliate Payouts
Docs

Stripe Connect Affiliate Payouts

Affiliates onboard to Stripe Connect Express via a one-tap link from Ezra. Stripe owns KYC/compliance; brand transfers funds directly to the affiliate's Stripe account when payouts run.

Overview

When an affiliate accepts an invite to a brand's program, they need a way to receive payouts. Trcker uses Stripe Connect Express accounts:

  • Stripe owns all KYC, identity verification, and bank linking — we never see SSN, bank info, or tax forms
  • Affiliates onboard via Stripe's hosted flow — one tap from Ezra, ~90 seconds end-to-end
  • Brand transfers funds directly to the affiliate's connected Stripe account (we don't custody funds — affiliates are paid by the brand, not by us)
  • Tax reporting (1099-NEC for US payouts ≥ $600/year) handled by Stripe

Onboarding flow

Step 1 — Affiliate accepts invite

(See Inviting Known Affiliates) Once they tap "Accept" on the landing page, a real partners row materializes for them.

Step 2 — Ezra requests an onboarding link

` POST /api/public/publishers/stripe-onboard Authorization: Bearer

{ "partnerId": "uuid", "country": "US", "returnUrl": "https://textezra.com/setup-done", "refreshUrl": "https://textezra.com/setup-resume" } `

  • On first call: Trcker creates a Stripe Express account, stores its ID on the partner row, generates a fresh Account Link
  • On subsequent calls: reuses the existing account, generates a fresh Account Link (Stripe's Account Links expire after ~15 minutes)
  • country defaults to US. Affiliates in other countries: pass the ISO 3166-1 alpha-2 code (e.g. GB, CA)
  • returnUrl and refreshUrl are optional — defaults to Trcker-hosted confirmation pages if Ezra doesn't override

Response (201 Created on first call, 200 OK on reuse): `json { "data": { "onboardingUrl": "https://connect.stripe.com/setup/abc123", "stripeAccountId": "acct_1234ABCD", "createdAccount": true, "expiresAt": 1715000900 } } `

Step 3 — Affiliate completes onboarding

Ezra texts them: "Tap to complete payout setup, takes about 90 seconds — connect.stripe.com/setup/abc123"

The affiliate enters their details on Stripe's hosted flow (legal name, DOB, bank account or debit card, SSN last 4). Stripe handles KYC.

Step 4 — Stripe fires account.updated webhook

Once the affiliate finishes, Stripe sends an account.updated event to: ` POST /api/webhooks/stripe Stripe-Signature: t=,v1= `

Trcker: 1. Verifies the Stripe signature (HMAC-SHA256 over ${timestamp}.${rawBody}, with 5-minute replay tolerance) 2. Re-fetches the account from Stripe (defense in depth — webhook payloads can be stale) 3. Updates the partner row's stripe_onboarding_complete, stripe_charges_enabled, stripe_payouts_enabled flags

Step 5 — Brand can now pay out

A partner is ready for payouts when: `sql stripe_onboarding_complete = true AND stripe_payouts_enabled = true AND stripe_account_id IS NOT NULL `

The payouts engine (existing Trcker infrastructure) checks these before initiating a Transfer to the affiliate's Stripe account.

Schema

Migration 0038 adds these columns to partners:

| Column | Type | Purpose | |---|---|---| | stripe_account_id | varchar(64), nullable, unique | Stripe Express account ID (acct_*). Null until first onboarding call. | | stripe_onboarding_complete | boolean, default false | Mirrors Stripe's details_submitted | | stripe_charges_enabled | boolean, default false | Mirrors Stripe's charges_enabled | | stripe_payouts_enabled | boolean, default false | Mirrors Stripe's payouts_enabled | | stripe_onboarded_at | timestamptz, nullable | First time details_submitted flipped true |

Required env vars

Set in Vercel before deploying:

| Var | Source | Purpose | |---|---|---| | STRIPE_SECRET_KEY | Stripe Dashboard → Developers → API keys | Server-side Stripe API auth | | STRIPE_WEBHOOK_SECRET | Stripe Dashboard → Developers → Webhooks → endpoint signing secret | Signature verification | | EZRA_SERVICE_KEY | (already set by PR #212) | Onboarding endpoint auth |

After deploy: create the webhook endpoint in Stripe Dashboard at: ` https://trcker.io/api/webhooks/stripe ` Subscribe to event: account.updated. Copy the signing secret into STRIPE_WEBHOOK_SECRET.

Why Express (not Custom or Standard)?

  • Standard accounts: affiliate has their own Stripe account, separate dashboard, full Stripe relationship. Too heavy for affiliates.
  • Custom accounts: we own all of Stripe's UI — we'd build the onboarding form, handle compliance, etc. Heavy compliance lift, defeats the point.
  • Express (chosen): Stripe handles the onboarding UI + KYC + dashboard, but presents it as part of our flow. Affiliate sees a co-branded Stripe page, comes back to us when done. Sweet spot.

Operational notes

  • Webhook signature replay protection: 5-minute tolerance. Events older than that are rejected.
  • Defense in depth on webhook: we re-fetch the account from Stripe rather than trusting the payload. Catches race conditions where the webhook arrives before the API state settles.
  • Account-link expiry: ~15 minutes. If the affiliate doesn't finish in time, Ezra can regenerate via the same endpoint — same account, fresh link.
  • Country support: Stripe Connect Express supports 40+ countries. For unsupported countries, Trcker should fall back to Wise transfers (Sprint 1.5+).
  • Test vs live: Stripe's test mode (sk_test_ keys) creates acct_test_ accounts. These can't actually receive transfers. Production needs live keys.
  • Account deletion: if a brand archives the partner, we should also delete the Stripe Connect account via DELETE /v1/accounts/:id. Sprint 1.5+.

Troubleshooting

| Symptom | Likely cause | Fix | |---|---|---| | 503 on /stripe-onboard | STRIPE_SECRET_KEY env var missing | Set in Vercel | | 401 on webhook | Signature mismatch — wrong STRIPE_WEBHOOK_SECRET | Check Stripe Dashboard → Webhooks → endpoint → signing secret | | Webhook 200 but partner_not_found | Stripe account exists but no Trcker partner has that stripe_account_id | Usually a test/live key mismatch. Confirm the account was created with the same Stripe mode | | account.updated fires repeatedly with same state | Normal — Stripe emits these on most account changes, even no-op | Idempotent; safe to handle multiple times | | Affiliate finishes onboarding but stripe_payouts_enabled stays false | Stripe is still reviewing their account (can take minutes to hours for some countries) | Wait — webhook will fire again when they enable |