- Stripe
Take Stripe payments in test mode without your agent seeing the secret key
Build Checkout in a Stripe sandbox, the new name for test mode, with a restricted key that can only create Checkout Sessions. Let your coding agent ask for it, and for the webhook signing secret, with offprompt: you paste both on a page in your browser, they are written to .env.local, and the agent writes code that reads STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET without ever seeing them.
Stripe's keys, and which ones are secret
- Publishable key,
pk_test_orpk_live_. Identifies your account to Stripe.js in the browser, and cannot create charges or read account data. The only Stripe key that is safe in front-end code. - Secret key,
sk_test_orsk_live_. Unrestricted access to every Stripe API. Stripe no longer recommends it for new uses. - Restricted key,
rk_test_orrk_live_. A secret key with a permission of None, Read or Write for each resource. The one to use here. - Webhook signing secret,
whsec_. Proves an event came from Stripe. Each endpoint has its own, and so doesstripe listen.
The restricted key and the signing secret both belong on the server only, and out of the agent's chat.
Steps
Work in a sandbox
Stripe now calls its test environments sandboxes, and the older test mode is one of them. Stripe suggests a new sandbox for a new integration, since some settings changed in test mode change live mode too. Switch to one in the Dashboard, or create one. Its keys start with
sk_test_,rk_test_andpk_test_, and card networks don't process its payments, so nothing the agent's code does there moves real money.Make a restricted key for Checkout
On the sandbox's API keys page, click Create restricted key and name it after the project. Set Checkout Sessions to Write, which includes Read, and leave every other resource at None, the default. Stripe recommends restricted keys over secret keys, especially for a key given to an AI agent; this one can create Checkout Sessions and little else.
If a request later fails with a permission error, add the one permission it names rather than switching to the secret key. A sandbox shows its keys again whenever you look; in live mode, a restricted key is shown once.
Start the webhook listener in your own terminal
Checkout tells your server about a payment with a
checkout.session.completedevent, which the server checks with a signing secret startingwhsec_. For local testing, the Stripe CLI forwards events and prints that secret. Run it in a terminal of your own, not through the agent: a command the agent runs puts its output, and the secret, in the transcript.stripe listen --forward-to localhost:4242/webhookLeave it running. The secret it prints stays the same between restarts. A deployed endpoint has a different secret, shown under Reveal secret on the endpoint’s page in the Dashboard; choose the Snapshot payload format for the code below.
zsh — ~/acme-shop~/acme-shop $ stripe listen --forward-to localhost:4242/webhook> Ready! Your webhook signing secret is whsec_•••••••••••••••• (^C to quit)Your own terminal, not the agent's. The secret is hidden here; yours prints in full, and you paste it on offprompt's page. Let the agent ask for both secrets at once
Ask for the task: “Add a Stripe Checkout button for a $20 T-shirt.” The agent calls offprompt's
collect_secretfor both values in one request, and a page opens in your browser on127.0.0.1. Paste the restricted key underSTRIPE_SECRET_KEY, the name Stripe's own examples use, and the CLI's secret underSTRIPE_WEBHOOK_SECRET.collect_secret { "secrets": [ { "name": "STRIPE_SECRET_KEY", "provider": "stripe" }, { "name": "STRIPE_WEBHOOK_SECRET", "provider": "stripe" } ], "reason": "Checkout needs a **Stripe** key, and the webhook its signing secret.", "sink": { "kind": "dotenv", "path": ".env.local" } }The page checks each value as you paste it: the key field takes
sk_andrk_keys, and says so when the publishablepk_key is pasted into it by mistake; the other takes onlywhsec_. offprompt writes both to.env.local; the agent gets their names and a four-emoji fingerprint.Create a Checkout Session
A server with two routes: one creates a Checkout Session and redirects to the page Stripe hosts, the other verifies and logs the webhook. Checkout hosted by Stripe needs no publishable key, so nothing secret goes near the browser.
npm install stripeserver.mjs import { createServer } from 'node:http' import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY) const base = 'http://localhost:4242' const checkout = async (req, res) => { const session = await stripe.checkout.sessions.create({ mode: 'payment', line_items: [ { price_data: { currency: 'usd', product_data: { name: 'T-shirt' }, unit_amount: 2000 }, quantity: 1, }, ], success_url: `${base}/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${base}/`, }) res.writeHead(303, { Location: session.url }).end() } // Stripe signs the body exactly as sent, so it is read raw, never parsed first. const webhook = async (req, res) => { const chunks = [] for await (const chunk of req) chunks.push(chunk) try { const event = stripe.webhooks.constructEvent( Buffer.concat(chunks), req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET, ) if (event.type === 'checkout.session.completed') { console.log(`Paid: ${event.data.object.id}`) } res.writeHead(200).end() } catch (error) { res.writeHead(400).end(`Webhook error: ${error.message}`) } } createServer((req, res) => { if (req.method === 'POST' && req.url === '/checkout') return checkout(req, res) if (req.method === 'POST' && req.url === '/webhook') return webhook(req, res) res.writeHead(200, { 'content-type': 'text/html' }) res.end('<form method="post" action="/checkout"><button>Buy a T-shirt, $20</button></form>') }).listen(4242, () => console.log(`Open ${base}`))node --env-file=.env.local server.mjsPay with a test card
Open
http://localhost:4242, press the button and pay with card number4242 4242 4242 4242, any future expiry date and any three-digit CVC. Stripe redirects to the success URL, the CLI forwardscheckout.session.completed, and the server logs the session ID.zsh — ~/acme-shop~/acme-shop $ stripe listen --forward-to localhost:4242/webhook> Ready! Your webhook signing secret is whsec_•••••••••••••••• (^C to quit)2026-10-31 16:24:03 --> checkout.session.completed [evt_1SNx…]2026-10-31 16:24:03 <-- [200] POST http://localhost:4242/webhook [evt_1SNx…]The same terminal after a test payment: the CLI forwards checkout.session.completed, among the other events the payment makes, and the webhook answers 200 once the signature checks out.
Going live
Make a live restricted key with the same permissions, and a webhook endpoint in live mode for your deployed URL, with its own signing secret. Both go in your host's environment variables for production, not in the .env.local on your machine, and neither should pass through the agent. A live restricted key is shown only once.
If a key may have been exposed, rotate it at once, as Stripe advises even when you are unsure anyone saw it. On the API keys page, Rotate key makes a replacement and lets you choose when the old key expires: both work for up to seven days, or choose Now to delete the old one immediately. Then have the agent ask for the new key with offprompt, which replaces the value in .env.local.
Why not paste the key into the chat
Stripe's own docs say not to share keys over chat. In an agent's chat, a key goes to the model provider with every later request and stays in the session's transcript. Claude asked for my API key. Is it safe to paste it? explains what happens to it, and what to do if one has already been pasted.
Sources
What the provider's own docs say, as of 31 October 2026.
- Stripe: API keys
- Stripe: Restricted API keys
- Stripe: API key permissions
- Stripe: Best practices for managing secret API keys
- Stripe: Sandboxes and test mode
- Stripe: Testing
- Stripe: Create a Checkout Session
- Stripe: Fulfill orders
- Stripe CLI: stripe listen
- stripe-node
Something here out of date? Open an issue on GitHub.
Try it
In a project's folder, run the command below, then ask the agent for something that needs a key.
npx offprompt init