AI Global Academy Join the waitlist

Courses / Bonus: what else you can build

Lesson 10.2 · 60 minTaking payments with Stripe (test mode)

Duration~60 min in the lesson + ~30 min homework
PrerequisitesCheckpoint lesson-8.6 (lesson-5.7 is enough: lead capture, notifications and the CRM); a Stripe account, which is free to open and needs no business verification for testing.
Checkpointlesson-10.2

What you will have

The site has a /pay page that sells one product through Stripe's hosted checkout page in a Stripe sandbox. Each test payment is recorded once in a payments table, shown at /admin/payments and on the matching lead's card, and announced to the owner. The code refuses live keys unless the owner deliberately allows them.

Video

The video for this lesson is not recorded yet.

Prompts used in this lesson

Part 5 — Building it

Prompt to Claude
Purpose: sell one product on my site through Stripe's hosted Checkout page, in
a Stripe sandbox only, and record each payment against the lead in my CRM.

Context: read docs/architecture.md, docs/crm-spec.md, the lead endpoint and
the notification code from 4.5. STRIPE_API_KEY (a restricted sandbox key),
STRIPE_WEBHOOK_SECRET and STRIPE_ALLOW_LIVE are in .env.local; never print,
log or ask for their values. The Stripe CLI forwards sandbox events to
localhost:3000/api/stripe/webhook.

Before writing code: read Stripe's current documentation at docs.stripe.com
for Checkout Sessions with the hosted page, "Fulfill orders", webhook
signature verification in a Next.js route handler, and the Node library. Do
not rely on memory. Tell me the pages you read and the calls you will make,
then wait for approval.

Build:
1. content/payments.ts: one product (name, description, amount, currency);
   ask me for the values. A public page /pay showing it with a "Pay" button.
2. POST /api/checkout: creates a payment-mode Checkout Session with the
   amount from the server file, never from the request; success URL
   /pay/success with Stripe's session id placeholder, cancel URL
   /pay/cancelled; redirects to the session's URL.
3. Table payments (migration): id, created_at, lead_id (nullable),
   stripe_session_id (unique), amount, currency, status, product, email,
   livemode. No anonymous access, like the other CRM tables.
4. One server function that records a payment from a session id: retrieve
   the session, check its payment status as the fulfilment guide says,
   insert one row, and notify me once through the existing Telegram or email
   code. Safe to call twice or at the same moment: rely on the unique
   column. Set lead_id to the most recent lead with the payer's email, or
   leave it empty.
5. POST /api/stripe/webhook: verify the Stripe-Signature header with
   STRIPE_WEBHOOK_SECRET on the raw body; on failure return 400 and do
   nothing. For checkout.session.completed and
   checkout.session.async_payment_succeeded call the function from step 4.
   Answer quickly; ignore other events.
6. /pay/success calls the same function, then shows "Payment received" or
   "We are confirming your payment". /pay/cancelled says nothing was charged.
7. /admin/payments: date, product, amount, status, lead link or "Unmatched"
   with "Attach to lead", a "TEST" badge when livemode is false. A
   "Payments" section on the lead card.
8. Safety catch: if STRIPE_API_KEY does not contain "_test_" and
   STRIPE_ALLOW_LIVE is not exactly "true", /api/checkout refuses and /pay
   shows "Payments are not available".

Constraints: no card data on my site; no Stripe key in client code; no new
analytics events; nothing existing renamed; no subscriptions or saved cards.

Verify before reporting: run the build; post to the local webhook with no
signature and with a wrong one and show both are rejected with no row
created; search the built client files for the key. Then tell me you are
ready for my test payment. Say what you could not verify.

Do along

Work on your own project. Pause the video where a step says so.

  1. Pause after Part 3. Create a sandbox and a restricted key. Add STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET (empty for now) and STRIPE_ALLOW_LIVE=false to .env.local. Check that the key contains _test_.
  2. Pause after Part 4. Install the Stripe CLI, switch on CLI access in the Dashboard (Settings, Team and security, MCP and CLI access), run the two commands, and put the printed secret in .env.local yourself. Leave the window open.
  3. Pause after Part 5. Run the build prompt in plan mode with your own product; a placeholder will do until the homework. Apply the migration and restart the dev server.
  4. Pause after Part 6. Pay with 4242 4242 4242 4242 using a sample lead's email. Then try 4000 0000 0000 0002.
  5. Commit and push. Add the three variables in Vercel, create the event destination for your live domain, put its signing secret in Vercel, redeploy.
  6. Do the "Check your work" steps, then tag with the commands under "Recap and next".

Check your work

  1. On your live domain, pay at /pay with the 4242 test card and a sample lead's email. Expected: Stripe's page, then "Payment received".
  2. Open /admin/payments. Expected: exactly one new row with a TEST badge, linked to that lead; one notification.
  3. Pay with the decline card. Expected: no new row.
  4. Ask Claude: "Send a POST to my live /api/stripe/webhook with a fake completed event and no valid signature, and show me the response." Expected: 400, no new row.

Common problems

  • Webhooks rejected with a signature error → locally: the secret is not the one printed by the running stripe listen, or the dev server was not restarted; live: Vercel has the CLI's secret instead of the event destination's → fix the secret; never disable verification. If the secret is right, tell Claude: "Verification needs the raw request body. Re-read Stripe's webhook documentation and fix the handler."
  • Stripe returns a permission error → the restricted key lacks a permission → the error names it; edit the key.
  • /pay says "Payments are not available" → the safety catch: the key is missing in this environment or is not a test key → check STRIPE_API_KEY there.

Homework

About 30 minutes, on your own. No other lesson depends on it. Everything stays in the sandbox; no task spends money.

  1. Your real product. Put the real name, description, amount and currency of what you would sell in content/payments.ts, rewrite the text on /pay, and pay once with the test card. Done when: a stranger can say what the payment is for after reading /pay. Commit without a tag.
  2. The questions before real money. Create docs/payments-go-live.md: the mechanics from Part 7 as a checklist, and your open questions on tax, receipts, refunds and terms, each with the name of the person you will ask. Done when: no question is answered by a guess. Commit without a tag.
  3. A refund, in the sandbox. Refund one test payment in the sandbox Dashboard (Payments page, the ⋯ menu beside the payment, Refund payment), then look at /admin/payments. Done when: the same file has three lines under "Refunds": what Stripe showed, what your CRM showed, and whether your first live version needs the CRM to know about refunds.

Save your work

git add -A
git commit -m "Lesson 10.2: Stripe hosted checkout in a sandbox, payments in the CRM"
git tag lesson-10.2
git push
git push --tags