Why
The card flow on the hosted checkout is architecturally complete — `createCardSession` auto-fills source fields, creates a Stripe `paymentIntent`, returns a `clientSecret` — but the current `apps/api/.env` has the placeholder `STRIPE_SECRET_KEY="sk_test_..."`. Hitting `/payments/:id/card-session` returns a clean Stripe error (we wrap it in PR 7.7) but no actual card payment has happened.
This is the lowest-effort way to close the card flow into "actually works" status.
Scope
- Provision a real Stripe test key — `sk_test_…` (the full one, not the placeholder)
- Wire it into local dev + document for prod
- Run a full card payment through the hosted checkout
- Confirm the webhook fires `payment.completed` on success
What to do
1. Get the key
2. Wire it
```bash
In apps/api/.env
STRIPE_SECRET_KEY="sk_test_<real_key>"
STRIPE_WEBHOOK_SECRET="whsec_<real_secret>"
```
Restart the API. Now `createCardSession` actually creates a real Stripe PaymentIntent and returns a usable `clientSecret`.
3. Run the flow
- Register a fresh merchant
- Create a payment link (`amount: 25, currency: USD`)
- Open `http://localhost:3003/l/` in incognito
- Click Pay → Card tile
- Stripe Elements form should render with the test card prompt
- Use Stripe's test card: `4242 4242 4242 4242`, any future expiry, any CVC, any ZIP
- Submit → payment should succeed
- Page should redirect to `/:paymentId/success`
4. Verify the webhook
- In the API log, look for `Stripe webhook received: payment_intent.succeeded`
- Payment row's `status` should flip to `COMPLETED` with `completedAt` set
- `WebhookEvent` row should land with `eventType: 'payment.completed'` queued for delivery to the merchant's webhookUrl
5. Document
Add a section to `apps/api/docs/operations/stripe-setup.md`:
- How to get the test key + webhook secret
- The webhook endpoint URL and the events to subscribe
- The tunnel command (`cloudflared tunnel` or `ngrok http 3333`) for local webhook delivery
- Test card numbers for common scenarios (success, decline, 3DS, insufficient funds)
- How to flip to prod (use live keys, set up the prod webhook endpoint, run KYB on the Stripe account)
Acceptance criteria
Estimated effort
~1 hour, assuming you have or can get a Stripe account.
Notes
- Keep the key in a secrets manager (1Password, Doppler, etc.) once you have it. Not in Slack DMs.
- The webhook secret matters: without it, `PaymentsService.handleStripeWebhook` will reject the signature and return 400.
- The test card `4242 4242 4242 4242` always succeeds. For 3DS testing use `4000 0027 6000 3184`. Full list: https://docs.stripe.com/testing.
Why
The card flow on the hosted checkout is architecturally complete — `createCardSession` auto-fills source fields, creates a Stripe `paymentIntent`, returns a `clientSecret` — but the current `apps/api/.env` has the placeholder `STRIPE_SECRET_KEY="sk_test_..."`. Hitting `/payments/:id/card-session` returns a clean Stripe error (we wrap it in PR 7.7) but no actual card payment has happened.
This is the lowest-effort way to close the card flow into "actually works" status.
Scope
What to do
1. Get the key
2. Wire it
```bash
In apps/api/.env
STRIPE_SECRET_KEY="sk_test_<real_key>"
STRIPE_WEBHOOK_SECRET="whsec_<real_secret>"
```
Restart the API. Now `createCardSession` actually creates a real Stripe PaymentIntent and returns a usable `clientSecret`.
3. Run the flow
4. Verify the webhook
5. Document
Add a section to `apps/api/docs/operations/stripe-setup.md`:
Acceptance criteria
Estimated effort
~1 hour, assuming you have or can get a Stripe account.
Notes