# Apiway billing & Stripe integration > How Apiway's billing layer works end-to-end: Stripe Checkout, webhooks, plan upgrades and downgrades, prorations, refunds, credit grants. Source of truth: `lib/stripe-server.ts`, `lib/credits.ts`, `docs/STRIPE_SUBSCRIPTIONS.md`, `docs/STRIPE_CREDITS.md`. For customer-facing language see https://localhost:10000/pricing. ## Pricing model in one sentence Apiway sells **monthly subscriptions** that grant a fixed credit allowance. **1 credit = $0.01 USD** by definition. Each generation deducts credits before the call to Gemini; failed generations are auto-refunded with the same amount and the same source bucket (plan vs marketplace). See https://localhost:10000/llms/pricing.txt for the plan-tier table. ## Plan checks (DO and DON'T) - **DO** call `isPaidPlan(plan)` from `lib/credits.ts` to gate paid features. - **DO** use `PLAN_DISPLAY_NAMES[plan]` for any UI label. - **DO** look up the plan for a Stripe price via `getPlanForPriceId(priceId)` in `lib/stripe-server.ts`. Price IDs have safe defaults baked into the file; env vars override them. - **DON'T** compare `plan === "pro"` anywhere — there are seven plans (free, starter, basic, pro, studio, business, scale). Hardcoded comparisons silently break new tiers. - **DON'T** put Stripe price IDs or webhook secrets in `render.yaml`. All secrets live in Render Dashboard → Environment. ## Credits flow 1. `try_deduct_credits(amount)` Postgres RPC runs **before** the generation call. Returns `true` on success, `false` if balance is too low. 2. Generation fires (Gemini call or worker offload). If anything throws, `try_refund_credits(amount)` is called with the same amount and the same `source_id` so balance is restored. 3. Marketplace generations carry a **separate balance** for the photographer/co-authors. The buyer pays creator pool + 20% markup; both buckets are settled atomically. ## Webhooks Stripe webhooks land at `POST /api/webhooks/stripe`. The handler verifies the signature with the secret in `STRIPE_WEBHOOK_SECRET` and dispatches: - `customer.subscription.created` / `customer.subscription.updated` → upserts the subscription, sets the plan, grants the new period's credits. - `customer.subscription.deleted` → drops user back to free at end of period. - `invoice.paid` → refresh credit balance on monthly renewal. - `invoice.payment_failed` → flag the account; remaining credits stay usable until period end. - `checkout.session.completed` → fallback path for first-time Stripe Checkout completion. The success redirect from Stripe Checkout also calls `/api/account/sync-subscription` to grant credits within seconds (without waiting for the webhook race). ## Plan upgrade / downgrade / cancel - **Upgrade**: from `/app/account` Billing tab. Stripe prorates the difference. Credits for the new tier are granted on the next `customer.subscription.updated`. - **Downgrade**: same flow; new credits land at the next billing period. - **Cancel**: stays active until end of period, then drops to Free (100 one-time credits, no refill). For card / address / mid-cycle changes, the customer portal at `/api/account/billing-portal` redirects to Stripe. ## Refunds (manual + automatic) - **Automatic credit refund**: every failed generation calls `try_refund_credits()` within seconds. No customer action needed. - **Stripe refund**: handled by the team via Stripe Dashboard. Apiway does not auto-refund subscription payments. - **Disputed charges**: Stripe handles the chargeback; the account is paused until resolved. ## Hardcoded safe defaults `lib/stripe-server.ts` ships price IDs as safe defaults so a missing env var on a fresh deploy does not break checkout. **Env vars override the hardcoded values** — set them in Render Dashboard → Environment, not in `render.yaml` (it's in git). ## Common bugs to look for - Comparing `plan === "pro"` instead of using `isPaidPlan(plan)`. Breaks the moment a new plan ships. - Forgetting to refund on failure. Always wrap generation calls so an exception path calls `try_refund_credits()`. - Hardcoded credit amounts in UI. Use `PLAN_CREDITS[plan]`. - Webhook handler not idempotent. Stripe retries; the handler must tolerate duplicate events. ## Cross-references - Customer pricing page: https://localhost:10000/pricing - Plan + credit table (machine-readable): https://localhost:10000/llms/pricing.txt - Doc on commercial-use rights: https://localhost:10000/docs/legal/commercial-use-rights - Doc on plans + credits: https://localhost:10000/docs/account/plans-and-credits - Long-form site brief: https://localhost:10000/llms-full.txt --- *Generated on 2026-05-01. Source: `lib/stripe-server.ts`, `lib/credits.ts`, `docs/STRIPE_SUBSCRIPTIONS.md`, `docs/STRIPE_CREDITS.md`. Update those files first; this slice will reflect changes on the next deploy.*