Billing host for:
| Product | Amount | CTA |
|---|---|---|
| Charter Pro | $49/mo | /subscribe?product=charter |
| Hookkeep Pro | $9/mo | /subscribe?product=hookkeep |
No PayPal. Never commit sk_ / whsec_ secrets. Never paste secret keys into chat — use a secure channel into .env only.
If a test secret was ever pasted into chat, rotate it in the Stripe Dashboard and use the new key only in .env.
- Create a Stripe account (test mode first) under Kestrel Ops billing.
- Copy secret key from Developers → API keys (
sk_test_…) into.envvia secure input — not chat, not git. - Clone / deploy this repo, then:
cp .env.example .env # paste STRIPE_SECRET_KEY into .env securely npm install npm run setup-products # copy printed STRIPE_PRICE_CHARTER / STRIPE_PRICE_HOOKKEEP into .env
- Webhooks
- Local:
stripe listen --forward-to localhost:4242/api/webhook→ pastewhsec_…intoSTRIPE_WEBHOOK_SECRET - Production: Dashboard → Webhooks → endpoint
https://YOUR_HOST/api/webhook
Events:checkout.session.completed,customer.subscription.updated,customer.subscription.deleted
- Local:
- Set
PUBLIC_BASE_URLto the public origin of this host (no trailing slash), e.g.https://billing.example.com. 5b. Hookkeep fulfill (optional): setHOOKKEEP_FULFILL_URL+HOOKKEEP_FULFILL_SECRETsocheckout.session.completedauto-unlocks Pro on Hookkeep viaPOST /api/stripe/fulfill. - Optional: set
STRIPE_SUCCESS_URL/STRIPE_CANCEL_URL(or product-specific overrides) to Pages or this host’s/successand/cancel. - Deploy (
npm startbehind HTTPS). Point GitHub Pages CTAs at:{PUBLIC_BASE_URL}/subscribe?product=charter{PUBLIC_BASE_URL}/subscribe?product=hookkeep
- When going live: switch to
sk_live_…, re-runsetup-products(or create live prices), update webhook secret, redeploy.
cd /workspace/ship/stripe-billing-2026-09-16 # or your clone
cp .env.example .env
# edit .env — STRIPE_SECRET_KEY via secure channel only
npm install
npm run setup-products
npm start- Health:
GET /api/health - Subscribe:
GET /subscribe?product=charter|hookkeep - Checkout API:
POST /api/checkoutwith JSON{ "product": "charter" }or{ "priceId": "price_…" } - Webhook:
POST /api/webhook(raw body; requiresSTRIPE_WEBHOOK_SECRET)
Subscription state is written to data/subscribers.json (gitignored).
| Variable | Purpose |
|---|---|
STRIPE_SECRET_KEY |
Server secret (sk_test_ / sk_live_) |
STRIPE_WEBHOOK_SECRET |
Webhook signing secret (whsec_…) |
STRIPE_PRICE_CHARTER |
Recurring price id — Charter $49/mo |
STRIPE_PRICE_HOOKKEEP |
Recurring price id — Hookkeep $9/mo |
PUBLIC_BASE_URL |
Public origin of this billing host |
STRIPE_SUCCESS_URL |
Success redirect (default /success?session_id={CHECKOUT_SESSION_ID}) |
STRIPE_CANCEL_URL |
Cancel redirect (default /cancel) |
PORT |
Default 4242 |
HOOKKEEP_FULFILL_URL |
Hookkeep origin (e.g. http://127.0.0.1:8787) — webhook POSTs {URL}/api/stripe/fulfill |
HOOKKEEP_FULFILL_SECRET |
Shared secret; must match Hookkeep HOOKKEEP_FULFILL_SECRET (x-fulfill-secret) |
Pass workspaceId (and optional email) on /subscribe?product=hookkeep&workspaceId=ws_… or POST /api/checkout so fulfill can unlock the right workspace.
Until PUBLIC_BASE_URL is a real host, marketing pages keep a wiring note (“Stripe Checkout — connect key to go live”). When the host exists, set CTA hrefs to absolute URLs:
https://YOUR_HOST/subscribe?product=charter
https://YOUR_HOST/subscribe?product=hookkeep
Relative /subscribe?product=… works only if Pages are reverse-proxied to this app (usually they are not — use absolute PUBLIC_BASE_URL).
Public surfaces: Kestrel Ops. No personal names on public HTML. No Upwork.