SOP — Stripe Founders Setup (Product, Price, Webhook, Vault, Heroku Wiring)
Owner: Operator (Kristerpher)
Last updated: 2026-05-31 UTC
Refs: #204 (Founders Promo epic), #2743 (PA SaaS sales-tax — CPA determination), docs/architecture/stripe-customer-billing.md, docs/architecture/queue-stripe-webhook-design-2026-05-14.md
When to run this
You need this runbook the first time the Founders product is wired up in a Stripe account — once for Test mode to validate, then again for Live mode when the operator is ready to accept paying customers.
This runbook covers:
- Creating the Founders Product + Price in the Stripe dashboard
- Configuring tax (PA SaaS posture; CPA review pending per #2743)
- Pointing webhook events at the Raptor endpoint
- Test-mode validation
- Live-mode swap
- Writing
product_id,price_id, andwebhook_secretto the Infisical vault - Setting Heroku config on
raxx-api-prod(andraxx-api-stagingfor test-mode)
iOS subscriptions are out of scope — those go through Apple IAP per the project memory lock. This runbook is web/desktop only.
Referral bonuses are out of scope — attribution only at v1 per the project memory lock; no referral coupons or promo codes are configured here.
Pre-conditions
Before starting, confirm:
- Stripe account is approved out of restricted mode (business info, EIN, beneficial-ownership, bank account for payouts).
- The MooseQuest LLC EIN is on file in Stripe (the LLC was formed 2026-05-22 UTC; verify the EIN appears in Stripe's
Settings → Business → Tax details). - The bank account for payouts is verified in Stripe.
- Infisical vault is reachable from this workstation (CF Access service token +
INFISICAL_TOKENexported in shell). - The
STRIPE_RESTRICTED_KEYfor the agent has been minted at the Stripe API-keys page with at least:read+writeon Products, Prices, Customers, Subscriptions, Invoices, Webhook Endpoints (least-privilege; never a full-access secret key in any env).
Step 1 — Create the Product
Stripe Dashboard → Products → Add product.
| Field | Value |
|---|---|
| Name | Raxx Founders |
| Description | Founders cohort — every feature in every tier, pricing locked for 6 months. |
| Image | Leave blank (brand placeholder period per project_brand_six_month_defer.md) |
| Statement descriptor | RAXX FOUNDERS (22-char limit; Stripe will reject longer) |
| Tax behavior | Tax is calculated on the price — leave the price-level tax-behavior setting to Step 3 |
| Unit label | (blank) |
| Metadata | cohort=founders, tier=all_features, lock_months=6 |
After save, copy the prod_... ID. You will write it to vault in Step 7.
Step 2 — Create the Price
On the Product page → Add another price.
| Field | Value |
|---|---|
| Pricing model | Standard pricing |
| Price | 29.00 USD |
| Billing period | Monthly, recurring |
| Trial period | None — Founders is paid-on-signup, no free trial (per operator pricing lock 2026-05-28) |
| Lookup key | founders_29_monthly_v1 |
| Tax behavior | Exclusive if PA collection is required (Step 3 outcome); Inclusive only if CPA advises otherwise |
| Metadata | lock_months=6, converts_to=pro_standard_after_lock |
The 6-month minimum commitment is not a Stripe-side configuration. Stripe Billing does not natively express "no cancellation before month N." Per docs/architecture/founders-grace-transition.md, the 6-month lock is enforced application-side by Raptor (and Queue after migration): the subscription is created with cancel_at_period_end=false and any customer-initiated cancellation before month 6 is routed through a custom dunning flow rather than firing subscription.delete on Stripe. Document this as a non-obvious gotcha when handing off.
After save, copy the price_... ID. You will write it to vault in Step 7 and to Heroku in Step 8.
Step 3 — Configure tax (PA SaaS posture — CPA review pending)
PA Act 84 of 2016 taxes "canned computer software" at 6% state (plus 1% Allegheny / 2% Philadelphia local). #2743 tracks the CPA determination on whether Raxx must collect from the first PA-resident subscriber as a domestic PA LLC.
Until CPA written guidance is on file (link the guidance memo in this section once it lands), set Stripe as follows:
- Enable Stripe Tax at
Settings → Tax → Settings. Use it as the calculation engine — do not commit to remitting via Stripe Tax until CPA approves. - Add Pennsylvania as a registered jurisdiction in
Settings → Tax → Registrationsonce myPATH registration is complete (PA Department of Revenue). Per #2743, the registration step is a separate operator action. - Set the product tax category to
txcd_10000000(General — SaaS / digital services). Confirm with CPA whethertxcd_10103000(SaaS) is a better fit; Stripe's PA-specific guidance flips occasionally. - Leave California, New York, Texas, Washington disabled at v1 — economic-nexus thresholds are not crossed and the registration overhead is not warranted pre-launch.
- Tax is shown to the buyer at checkout. Customer-facing copy says "tax calculated at checkout" on the pricing page.
Until #2743 closes, treat Stripe Tax in "calculate but do not remit automatically" mode — the operator manually files the PA-3 sales-tax return monthly. Switch to Stripe Tax auto-remit only after CPA sign-off.
Step 4 — Webhook endpoint
The webhook endpoint lives in Raptor at https://api.raxx.app/webhooks/stripe (per docs/architecture/queue-stripe-webhook-design-2026-05-14.md §3). Note: this endpoint moves to Queue post-migration; Raxx-side path stays stable, the implementing service changes.
Stripe Dashboard → Developers → Webhooks → Add endpoint.
| Field | Value (Test mode) | Value (Live mode) |
|---|---|---|
| Endpoint URL | https://api-staging.raxx.app/webhooks/stripe |
https://api.raxx.app/webhooks/stripe |
| Description | Raxx Founders test webhook |
Raxx Founders prod webhook |
| API version | Match the Stripe SDK pinned in backend_v2/requirements.txt (verify before save) |
|
| Events to send | See list below |
Events to send (subset of Founders-relevant events; do NOT enable "send all events" — webhook noise breaks idempotency dashboards):
customer.createdcustomer.updatedcustomer.deletedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.subscription.trial_will_end(not used today, future-proof)invoice.createdinvoice.finalizedinvoice.paidinvoice.payment_failedinvoice.payment_action_requiredcharge.refundedcharge.dispute.createdpayment_method.attachedpayment_method.detached
After save, click Reveal signing secret and copy the whsec_... value — visible exactly once. You will write it to vault in Step 7.
Step 5 — Test-mode validation
Before flipping live mode:
- In Stripe Test mode dashboard → Developers → Customers → Add test customer. Use email
test+founders@raxx.appand the Stripe test card4242 4242 4242 4242(any future exp, any CVC, any ZIP). - Subscribe the test customer to the Founders price.
- Confirm webhook delivery — Stripe Dashboard → Developers → Webhooks → [endpoint] → Recent attempts. Both
customer.subscription.createdandinvoice.paidshould show HTTP 200 from staging. - Confirm Raptor staging logs (
heroku logs --tail --app raxx-api-staging | grep stripe) show signature-verified events and a row in the billing-event audit table. - Cancel the test subscription in the dashboard. Confirm
customer.subscription.deletedarrives and the staging app downgrades the test user. - Trigger a payment-failure scenario with test card
4000 0000 0000 0341(charge succeeds, subsequent invoice fails). Confirminvoice.payment_failedlands and a dunning email queues.
If any step fails, fix in staging before moving to Step 6.
Step 6 — Live mode swap
Once Step 5 is clean:
- Toggle the Stripe dashboard from Test to Live mode (top-right).
- Repeat Step 1, Step 2, Step 3, Step 4 in Live mode. The lookup key is the same (
founders_29_monthly_v1) but theprod_...,price_..., andwhsec_...IDs will be different from the test values. Live IDs start with the same prefix; the difference is the Stripe account context. - Confirm the Live API key starts with
sk_live_...(operator never copies this anywhere — Stripe stores it; the restricted key Raxx uses starts withrk_live_...). - Update vault (Step 7) with the live values.
- Update Heroku config (Step 8) for
raxx-api-prod. - Smoke-test one real subscription with the operator's own card — refund immediately after the webhook lands and the subscription record is written to Raptor.
Step 7 — Write vault entries
Per feedback_secrets_in_vault_sop.md and feedback_no_inline_secrets_in_repo.md, every secret lives in Infisical and is read at runtime — never inlined into repo files, never echoed to logs.
Vault folder per operator instruction: /raxx/stripe/. The folder must exist before any secret write (per feedback_vault_folder_must_exist.md). The legacy path /MooseQuest/stripe/ and the Queue-future path /Raxx/Queue/Billing/Stripe/ both also exist in the vault per docs/architecture/queue-stripe-webhook-design-2026-05-14.md §I-10 — coordinate with the Queue cutover plan before re-pathing existing secrets.
Create the folder first if missing:
curl -X POST "https://app.infisical.com/api/v1/folders" \
-H "Authorization: Bearer $INFISICAL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"workspaceId":"<wsid>","environment":"prod","name":"stripe","path":"/raxx"}' \
>/dev/null 2>&1
Then write the three secrets (use Infisical CLI or REST — values from Step 1, Step 2, Step 4):
| Secret name | Path | Value source |
|---|---|---|
STRIPE_FOUNDERS_PRODUCT_ID |
/raxx/stripe/ |
Step 1 prod_... |
STRIPE_FOUNDERS_PRICE_ID |
/raxx/stripe/ |
Step 2 price_... |
STRIPE_FOUNDERS_WEBHOOK_SECRET |
/raxx/stripe/ |
Step 4 whsec_... |
STRIPE_RESTRICTED_KEY |
/raxx/stripe/ |
The rk_live_... restricted key minted in pre-conditions |
For Live mode, label each secret with the prod environment in Infisical. For Test mode, write to the staging environment with the test-mode IDs and rk_test_... key.
Never paste any of these values into a chat, ticket, PR description, or commit message. Per feedback_heroku_config_set_echoes_secrets.md, even reading them back through tooling can leak — always silence stdout.
Step 8 — Heroku config
Set the flag and the price ID on raxx-api-prod. The webhook secret and restricted key are read from vault at runtime (no Heroku config value); only the price ID and feature flag need to be set as Heroku config because they are read at boot rather than per-request.
heroku config:set FLAG_STRIPE_FOUNDERS=1 STRIPE_FOUNDERS_PRICE_ID=price_xxx \
--app raxx-api-prod >/dev/null 2>&1
Replace price_xxx with the Live-mode price_... from Step 2. The >/dev/null 2>&1 is mandatory per feedback_heroku_config_set_echoes_secrets.md — heroku config:set echoes the value back by default, and even non-secret IDs in shell history are a hygiene cost.
Same pattern for staging with the test-mode price ID:
heroku config:set FLAG_STRIPE_FOUNDERS=1 STRIPE_FOUNDERS_PRICE_ID=price_xxx \
--app raxx-api-staging >/dev/null 2>&1
To verify the flag is set without exposing the value to history:
heroku config:get FLAG_STRIPE_FOUNDERS --app raxx-api-prod
# Expected output: 1
Note: this still echoes the value (which is 1, not a secret). For secret values, use wc -c to confirm length only, never reveal contents.
Step 9 — Console flag promotion (same-PR migration)
Per feedback_new_flag_needs_b1_migration_same_pr.md, every new entry in feature_flags.yaml requires a console_flag_promotions migration in the same PR. If FLAG_STRIPE_FOUNDERS does not yet exist in YAML, the engineering PR that lands the Founders signup wiring must add both the YAML row and the promotion migration — this runbook step is informational, not the PR's job.
Common pitfalls
-
6-month minimum commitment is application-side, not Stripe-side. Stripe Billing does not enforce a minimum-commitment window. The customer can cancel any time in the Stripe customer portal. Application-side logic (Raptor today, Queue post-migration) must intercept cancellation requests pre-month-6 and route them through the dunning UX rather than firing
subscription.delete. Without this enforcement, the Founders pricing-lock promise breaks silently. -
Automatic conversion to regular Pro after 6 months. Stripe Billing does not natively switch a subscription from one Price to another after N billing periods. The 6-month-to-Pro transition is implemented as a scheduled job (
scheduled_subscription_change) in Stripe's Subscription Schedules API. When the regular Pro/Pro+ prices are finalized (BLR research pending perproject_pricing_tiers_locked.md), create the schedule at signup so the transition is queued from day one rather than retrofitted month 5. -
Refund window is operator policy, not a Stripe setting. Stripe allows refunds up to 180 days after a charge. Raxx's published refund policy lives in the Terms of Service draft (
docs/legal/terms-of-service-draft-2026-05-14.md); the operator approves or denies refunds case-by-case through the dashboard. There is no automatic refund-on-cancel for Founders subscriptions — the value proposition is the locked $29 price, not a 30-day money-back guarantee. -
Test-mode webhook signature secret is different from Live-mode. When swapping from Test to Live in Step 6, the
whsec_...must be re-fetched from the Live-mode endpoint page. Reusing the Test secret in prod will cause every webhook to fail signature verification and you will spend an hour debugging "401 Unauthorized" before realizing. -
Stripe Tax registration is per-state, not per-account. Enabling Stripe Tax for PA does not register MooseQuest LLC with the PA Department of Revenue — that is a separate myPATH registration (#2743). Stripe will calculate tax but cannot file the PA-3 return without operator action.
-
Restricted-key scope is "deny-by-default." When minting the
rk_live_..., every permission must be explicitly granted. If Webhook Endpoints is left atNone, the application cannot reconcile webhook subscriptions in code. Audit the scope list against the application's actual needs before clicking save. -
iOS subscribers do not appear in Stripe. Per
project_ios_billing_iap.md, iOS sub revenue flows through Apple IAP. The Stripe dashboard will only show web/desktop subscribers. Cohort analytics inconsole.raxx.appmust aggregate from both sources — the Stripe-only view will undercount.
Rollback
If the live-mode swap turns out to be premature:
heroku config:set FLAG_STRIPE_FOUNDERS=0 --app raxx-api-prod >/dev/null 2>&1
This disables the Founders signup path application-side. Existing subscriptions in Stripe remain billable — the flag gates new signups, not existing customers. To pause billing for everyone, navigate each subscription to "Pause collection" in the Stripe dashboard (operator action; not scripted).
Refs
docs/architecture/stripe-customer-billing.md— billing data modeldocs/architecture/queue-stripe-webhook-design-2026-05-14.md— webhook handler designdocs/architecture/founders-grace-transition.md— month-6 transition mechanicsdocs/architecture/adr/0073-stripe-v1-home-decision.md— Raptor as v1 billing hostdocs/architecture/adr/0076-queue-phase1-cpp-billing-v1.md— Queue cutover planfeedback_heroku_config_set_echoes_secrets.md— everyheroku config:setmust be silencedfeedback_no_inline_secrets_in_repo.md— never paste Stripe keys into the repofeedback_secrets_in_vault_sop.md— vault is the only authoritative storefeedback_vault_folder_must_exist.md— create/raxx/stripe/before writing secretsproject_pricing_tiers_locked.md— Founders $29/mo lock, 6 months, all featuresproject_ios_billing_iap.md— iOS revenue via Apple IAP, not Stripeproject_referral_bonus_deferred.md— attribution-only at v1, no Stripe coupons-
204 — Founders Promo epic
-
2743 — PA SaaS sales-tax CPA determination