Take test payments with a hosted checkout and webhook
Connect your payment gateway in test mode so customers pay on its hosted page and a signed webhook marks the order paid, without your site ever touching card data.
You will have: Test payments that move a pending order to paid, failed or refunded safely, with duplicate webhook calls ignored and no card data stored anywhere.
Before you start
- Episode 12 is done: a checkout that saves pending orders works locally
- Create a free account with your payment gateway and stay in test mode (no business verification is needed for test mode). Check that it supports businesses in your country
- If you use Stripe, install the Stripe CLI (the agent will guide you). It forwards webhook messages to your computer while you test
- Have your test secret key ready. Paste it only into the file .env.local when the agent asks, never into the chat
Make it yours
Change the example (fashion store) to your own. Your answers are saved in this browser and used in every episode.
Other series keywords (4)
Your prompt
Paste into: Your AI agent opened in your project folder. Give secret keys only through the .env.local file, never in the chat.Context: this project is the online store "Thread & Needle", a fashion e-commerce store selling women's and men's clothing, shoes and accessories in United States and charging in USD. Checkout already saves pending orders (Episode 12). The stack is Next.js, TypeScript, PostgreSQL with Drizzle (local database shop_local) and Zod.
Goal: connect Stripe in TEST MODE using this pattern: the customer is sent to the gateway's own hosted checkout page, the gateway tells our server the result through a signed webhook, and only that webhook can mark an order paid. The default gateway is Stripe, using Checkout Sessions. If Stripe is a different gateway, follow the same pattern with its official documentation and tell me where it differs. Before writing code, read the official docs for the gateway and the Next.js version in this project, because details change.
Plan first: show me your plan (files, new tables, environment variable names, events you will handle) and wait for my OK before changing anything. Explain webhooks and idempotency in a few plain words.
Tasks, after I approve:
1. Ask me to add the test secret key and the webhook signing secret to .env.local. Add only the variable names to .env.example. Never print, log or commit their values.
2. Server action or route that creates a hosted checkout session for one pending order. Build the line items, currency and amounts from the order saved in the database, never from the browser. Put the order id in the session metadata and set success and cancel URLs. Redirect the customer to the gateway's page.
3. Success page: it only shows "We are confirming your payment" and reads the order status from our database. It must never mark an order paid.
4. Webhook route at /api/webhooks/stripe (or the matching name for the gateway): read the raw request body, verify the signature, reject anything unverified with an error status, and return quickly. Handle: payment completed (check the paid status, and that amount and currency match the order), delayed payment succeeded or failed if the gateway supports them, session expired, and refund events.
5. Idempotency: add a table that stores every processed event id with a unique constraint. If the same event arrives twice, do nothing the second time. Apply order changes and the event record in one database transaction.
6. Order status rules in one place: pending can become paid, failed or expired; paid can become refunded or partly refunded; paid orders never go back to pending. Reject impossible changes and log them without personal data.
7. On payment, reduce stock for each item inside the same transaction and empty that customer's cart.
Security rules: never see, store or log card numbers, expiry dates or security codes; keep keys only in .env.local and the host's settings; keep test and live keys clearly separate; return generic errors to callers.
Verify yourself and show results: write Vitest tests for the status rules and for duplicate events. Then run the gateway's CLI to forward test webhooks to localhost and complete one successful test payment, one declined test payment and one refund in the gateway's dashboard. Show the order status after each step. Run the type check and linter.
Commit to a branch named feature/payments-test-mode with a clear message.
Report back in this format: 1) What was done. 2) What I should check in the gateway dashboard and in the browser. 3) Anything that needs my decision.Review the prompt, then copy it. Never paste real passwords or API keys into a chat.
How to check it worked
- Clicking pay sends you to the gateway's own page, not a card form on your site
- Paying with the test card 4242 4242 4242 4242 (any future expiry, any 3-digit code) turns the order from pending to paid
- A declined test card leaves the order unpaid and lets you try again
- Replaying the same webhook event in the gateway tool does not change the order twice or reduce stock twice
- Refunding a test payment in the dashboard moves the order to refunded
- Searching your project for a card number or your secret key finds nothing, and .env.local is not tracked by git
Sample result
Example: fashion storeExample agent report for the fashion store, with the order moving through its states during test payments.
If something goes wrong
- The webhook returns a signature error (400).
- The signing secret in .env.local is usually from a different listener. Restart the CLI, copy the new secret it prints into .env.local, restart the site, and make sure the route reads the raw body and not parsed JSON.
- The customer paid but the order stays pending.
- Check that the CLI is still running and forwarding to the right port, then ask the agent: "Show the last webhook events received and why the order was not updated." A missing order id in the metadata is a common cause.
- The same order was processed twice, or stock dropped twice.
- Ask the agent to confirm the unique constraint on the event id and that stock changes happen in the same transaction as the status change. Then add a test that sends one event twice.
- Your payment gateway does not accept businesses in your country.
- Ask the agent to compare gateways that do, then change the payment gateway keyword at the top of the series. The hosted page plus signed webhook pattern stays the same.
Test mode costs nothing. In live mode gateways usually charge a fee on each sale; check the gateway's pricing page for your country before launch. Moving to live keys happens in Episode 23, with separate keys and a separate live webhook.
More in Payments and email
This is part of the series Build a secure, no-cost e-commerce website with AI agents. See all 24 episodes, the tools used and answers to common questions on the series page.