Docs (Markdown)
Tip: this is plain markdown in a <pre> block for maximum inspectability.
# Billing (Stripe) — Checkout + Portal + Verified Webhooks
AI Power Progress iA supports Stripe for **services + SaaS** and (optionally) the Compute Exchange.
Stripe is implemented as:
1. **Checkout Sessions** (server-side) for one-time payments and subscriptions (preferred for correctness + metadata).
2. **Billing Portal** (server-side) for customer self-serve subscription/payment method management.
3. **Verified webhooks** (`/api/stripe/webhook`) for idempotent fulfillment (never trust client redirects).
4. **Payment Links** (optional) as a bootstrap/convenience path (still fulfilled via webhooks).
This doc describes what the codebase expects, how to configure it safely, and how to validate locally.
Reference architecture + maps: `docs/stripe_overhaul.md`
## Policy pages (required for payments)
These are first-party, public policy pages (served from `static/*.html`):
- `GET /privacy`
- `GET /terms`
- `GET /refund`
If you use Stripe, add these URLs in the Stripe Dashboard where applicable (e.g., business / checkout settings).
## Offers (repo-truthful)
Current public offers:
- Project Deposit: **$500** (one-time)
- Quick Help (1 hour): **$125** (one-time)
- Monthly Retainer: **$999/mo** (subscription)
## Services Desk payments (Checkout Sessions + optional Payment Links)
The main conversion surfaces are:
- `GET /pricing` (Quick order + optional “Pay now” buttons)
- `GET /assistant/pricing` (fallback pricing page when the Next.js assistant is down)
- `GET /services` / `GET /hire` (CTAs to Services Desk)
- `GET /assistant` (fallback desk intake + request/manual-invoice flow when needed)
- `GET /account` (signed-in billing history, entitlement state, Billing Portal entry)
### Configuration (env-only; never commit secrets)
Core Stripe keys:
- `STRIPE_SECRET_KEY` (starts with `sk_test_…` / `sk_live_…`)
- `STRIPE_WEBHOOK_SECRET` (starts with `whsec_…`)
Prices (recommended; enables Checkout Sessions):
- `STRIPE_PRICE_DEPOSIT_500`
- `STRIPE_PRICE_HOURLY_1H`
- `STRIPE_PRICE_RETAINER_MONTHLY`
Payment Links (optional; useful for bootstrapping without price IDs in env):
- `STRIPE_PAYMENT_LINK_KICKSTART`
- `STRIPE_PAYMENT_LINK_BUILD`
- `STRIPE_PAYMENT_LINK_RETAINER`
When configured, `/pricing` and `/assistant/pricing` will show “Pay” buttons and the APIs under `/api/billing/*` will create sessions/links.
## Public surface contract (single source of truth)
Do not maintain separate billing readiness logic per page.
- `GET /api/billing/status` is the canonical public readiness source for `/pricing`, `/services`, `/hire`, `/assistant`, `/assistant/pricing`, and `/account`.
- Public business pages should reuse the shared `static/site_billing.js` helpers (injected server-side before `static/site.js`) instead of duplicating fetch/click logic:
- `ppiaLoadBillingStatus()`
- `ppiaApplyBillingPlanCards()`
- `ppiaInstallBillingPayButtonHandler()`
- `ppiaBillingSummaryText()`
- Public checkout return-state copy should also reuse the shared helpers:
- `ppiaCheckoutReturnState()`
- `ppiaBillingReturnText()`
- `ppiaRefreshBillingReturnNotices()`
- Public status copy should render into `data-ppia-billing-summary="..."` targets so Stripe-ready and degraded/manual-invoice states stay aligned.
- Public success/cancel notices should render into `data-ppia-billing-return="pricing|assistant_pricing|assistant_thanks"` targets so `/pricing`, `/assistant/pricing`, and `/assistant/thanks` stay aligned.
- Ask AI entry points on business pages should keep using the existing site-wide widget via `data-ppia-open-widget="1"` plus `data-ppia-widgetcontext="pricing|services|hire|assistant|assistant_pricing"` so users stay on the main public AI surface and get bounded, page-aware help.
- If Stripe is absent or partially configured, prefer request/manual-invoice flows over optimistic or dead “Pay” buttons.
- Return-state semantics must be explicit:
- success URL: `/assistant/thanks?checkout=success&session_id={CHECKOUT_SESSION_ID}`
- cancel URL: `/assistant/pricing?checkout=cancel`
- Final paid state, entitlement changes, and provisioning must be described as webhook-verified only. Do not claim success based only on the redirect URL.
### Checkout return verification (webhook-truthful UX)
Return pages should not blindly treat `?checkout=success` as "paid".
The shared `static/site_billing.js` module will:
- render a baseline success/cancel banner from URL params, and
- when `session_id` is present, poll the local-only status endpoint until the webhook has been processed (bounded attempts).
Endpoint:
- `GET /api/billing/checkout-status?session_id=cs_...` (public-safe; `Cache-Control: no-store`; DB-only; does not leak emails/user IDs/request IDs)
### Optional: one-command bootstrap (creates Payment Links)
If you want the repo to generate the three Payment Links for you (Kickstart / Build deposit / Retainer subscription),
use the bootstrap script:
```bash
cd aipowerprogressia.com
python3 scripts/stripe_bootstrap_payment_links.py
```
By default it’s a dry-run. To create links and write them into `aipowerprogressia.com/.env`:
```bash
cd aipowerprogressia.com
STRIPE_SECRET_KEY="sk_test_..." python3 scripts/stripe_bootstrap_payment_links.py --apply --write-env
```
Safety:
- Prefer Stripe **test** mode first (use `sk_test_...`).
- The script refuses live keys unless you pass `--allow-live`.
- The script writes only `STRIPE_PAYMENT_LINK_*` URLs to `.env` (not your secret key).
## Compute Exchange (optional): API key + webhook
Compute credits checkout uses Stripe Checkout Sessions:
- `POST /api/compute/credits/checkout` (creates a Stripe Checkout Session)
- `POST /api/stripe/webhook` (settles paid sessions into credits)
Env vars: reuses `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET`.
Notes:
- The webhook route is `include_in_schema=False` by design.
- The server never exposes Stripe targets/keys in public status surfaces.
## Stripe CLI (recommended for local webhook testing)
The Stripe CLI is the easiest way to forward webhooks to your localhost while developing.
Install (macOS/Homebrew):
```bash
brew install stripe/stripe-cli/stripe
```
Authenticate (generates restricted keys):
```bash
stripe login
```
Forward events to the PPIA webhook route (adjust host/port as needed):
```bash
stripe listen --forward-to http://127.0.0.1:8000/api/stripe/webhook
```
Then set the webhook signing secret it prints (a `whsec_…`) in `aipowerprogressia.com/.env`:
```bash
STRIPE_WEBHOOK_SECRET="whsec_…"
```
Safety:
- Prefer **test** mode / sandbox for development (do not use live keys locally).
- Never paste secret keys into chat or intake forms—use env vars only.
## Local setup (safe)
From `aipowerprogressia.com/` create a local-only `.env` (this repo ignores it via `.gitignore`):
```bash
cd aipowerprogressia.com
cp .env.example .env
```
Then set your Payment Links and (optionally) Stripe secrets in `aipowerprogressia.com/.env`:
```bash
# Payment Links (services)
STRIPE_PAYMENT_LINK_KICKSTART="https://buy.stripe.com/…"
STRIPE_PAYMENT_LINK_BUILD="https://buy.stripe.com/…"
STRIPE_PAYMENT_LINK_RETAINER="https://buy.stripe.com/…"
# Optional: Compute Exchange
STRIPE_SECRET_KEY="sk_test_…"
STRIPE_WEBHOOK_SECRET="whsec_…"
```
Recommended for the canonical FastAPI Checkout path:
```bash
STRIPE_SECRET_KEY="sk_test_…"
STRIPE_WEBHOOK_SECRET="whsec_…"
STRIPE_PRICE_DEPOSIT_500="price_…"
STRIPE_PRICE_HOURLY_1H="price_…"
STRIPE_PRICE_RETAINER_MONTHLY="price_…"
```
## Validation checklist
- `GET /pricing` shows “Pay” buttons (only when billing is configured).
- `GET /assistant/pricing` shows “Pay” buttons (Next.js or fallback).
- `GET /pricing?checkout=cancel` shows an honest cancel banner and does not imply any paid state.
- `GET /assistant/thanks?checkout=success&session_id=cs_test_...` shows an honest success banner that still says verified webhook processing is authoritative.
- `GET /api/billing/checkout-status?session_id=cs_test_...` returns `state=processing|paid|fulfilled|failed` (public-safe, webhook-driven).
- `GET /api/billing/status` returns plan availability (public-safe; `Cache-Control: no-store`).
- `POST /api/billing/checkout` returns a URL (Payment Link or Checkout Session URL).
- `POST /api/billing/portal` returns a portal URL (auth required).
- `GET /account` shows entitlements/checkout history when signed in and can open the Billing Portal.
- `GET /admin/billing` shows webhook events + checkouts + subscriptions + entitlements (operator visibility).
- `POST /api/billing/admin/replay` can replay a Stripe `evt_...` by id (admin; idempotent; useful after fixing misconfig).
- `GET /api/services/status` includes `Stripe (Payments)` with a clear readiness note.
- `POST /api/saas/service-request` redacts obvious secrets in `website`/`details` and returns a redaction notice.
- `python3 scripts/run_core_release_gates.py` passes and writes a JSON report under `data/release_gates/`.
## Exact Stripe test-mode steps
1. Copy env and set test credentials:
```bash
cd aipowerprogressia.com
cp .env.example .env
```
Add test-mode values:
```bash
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
STRIPE_PRICE_DEPOSIT_500="price_..."
STRIPE_PRICE_HOURLY_1H="price_..."
STRIPE_PRICE_RETAINER_MONTHLY="price_..."
```
2. Start the app:
```bash
cd aipowerprogressia.com
bash scripts/run_app.sh
```
3. Start webhook forwarding in another terminal:
```bash
stripe listen --forward-to http://127.0.0.1:8000/api/stripe/webhook
```
4. Verify public readiness:
```bash
curl -s http://127.0.0.1:8000/api/billing/status
```
Expected:
- `stripe_configured: true`
- `checkout_enabled: true`
- the configured plans appear in the `plans` object
5. Verify checkout session creation:
```bash
curl -s -X POST http://127.0.0.1:8000/api/billing/checkout \
-H 'Content-Type: application/json' \
-d '{"plan_id":"deposit_500","success_path":"/pricing?checkout=success","cancel_path":"/pricing?checkout=cancel"}'
```
Expected:
- JSON contains `ok: true`
- `url` points to Stripe Checkout or a configured Payment Link
6. Complete the payment in Stripe test mode and then verify:
- `/pricing`, `/assistant/pricing`, and `/account` reflect the same billing-readiness copy
- `/pricing?checkout=cancel` and `/assistant/pricing?checkout=cancel` show cancel-state guidance
- `/assistant/thanks?checkout=success&session_id=...` shows success-state guidance but does not claim paid status without webhook confirmation
- `/admin/billing` shows the checkout and webhook event
- replaying the same event via `POST /api/billing/admin/replay` stays idempotent
7. Verify Billing Portal:
- sign in on `/account`
- use “Manage billing”
- confirm `POST /api/billing/portal` returns a Stripe Billing Portal URL only for the authenticated owner
8. Verify degraded/manual fallback by removing Stripe env vars and reloading:
- `/pricing`, `/services`, `/hire`, `/assistant`, and `/assistant/pricing` should show honest request/manual-invoice copy
- no dead pay button should remain
## Manual dashboard / go-live checklist
- Configure the three products/prices in Stripe:
- Project Deposit (`deposit_500`)
- Quick Help (`hourly_1h`)
- Monthly Retainer (`retainer`)
- Configure policy URLs:
- `/privacy`
- `/terms`
- `/refund`
- Configure the webhook endpoint:
- `https://<your-domain>/api/stripe/webhook`
- subscribe to:
- `checkout.session.completed`
- `checkout.session.async_payment_succeeded`
- `checkout.session.async_payment_failed`
- `invoice.paid`
- `invoice.payment_failed`
- `invoice.payment_action_required`
- `customer.subscription.created`
- `customer.subscription.updated`
- `customer.subscription.deleted`
- Confirm the live-mode env values are set server-side only:
- `STRIPE_SECRET_KEY`
- `STRIPE_WEBHOOK_SECRET`
- `STRIPE_PRICE_DEPOSIT_500`
- `STRIPE_PRICE_HOURLY_1H`
- `STRIPE_PRICE_RETAINER_MONTHLY`
- Confirm `/api/billing/status` reports the expected live readiness without exposing secrets.
- Confirm success redirects use `/assistant/thanks?checkout=success&session_id={CHECKOUT_SESSION_ID}` and cancel redirects use `/assistant/pricing?checkout=cancel`.
- Run one end-to-end live smoke only after explicit approval and with a real operator watching `/admin/billing`.
## Safety: do not paste secrets into intake forms
The Services Desk intake is designed to be privacy-first. It is not a secret transport.
If a user accidentally includes API keys (e.g., Stripe `sk_live_…`, webhook `whsec_…`), the server will redact them before storing the request and will return a warning in the API response.
## Resilience: post-payment “Thanks” pages
- The main conversion flow should prefer stable, first-party post-payment URLs (example: `GET /pricing`).
- The assistant proxy also serves a fallback `GET /assistant/thanks` page so a successful Stripe return URL stays usable even when the Next.js assistant UI is unavailable.
- Public return pages must stay webhook-truthful:
- success/cancel banners describe redirect state only
- final entitlements/provisioning are confirmed only after verified webhook processing