Provision your payment wiring. Never paste it.
An afternoon lost to copying secrets between two dashboards, when the API returns the value and the CLI accepts it on stdin.
26 August 2026 · 5 min read
If you searched forStripe webhook secret keeps not working after saving it in Vercel
The afternoon
Copy the signing secret from Stripe. Paste it into the hosting dashboard. Redeploy. Still 400. Check you pasted the right one. Redeploy. Still 400. Check whether it saved. It says it saved. Redeploy.
Every minute of that was avoidable, and the reason it was avoidable had been sitting in the API docs the entire time.
The thing nobody mentions
Creating a webhook endpoint through Stripe's API returns the signing secret in the response. Your host's CLI accepts a value on standard input. One pipes into the other.
The secret exists only in the memory of a running process. It never touches a clipboard, a file, a screen, or a command argument — and that last one matters, because command arguments are visible to every process on the machine.
The whole thing
const created = await stripe.webhookEndpoints.create({
url: `${deploymentUrl}/api/stripe/webhook`,
enabled_events: ["checkout.session.completed"],
})
// Straight into the host. Never printed, never written, never an argument.
execFileSync("vercel", ["env", "add", "STRIPE_WEBHOOK_SECRET", env], {
input: created.secret,
stdio: ["pipe", "ignore", "inherit"],
})One paste is the floor
You still have to supply the API key once per environment, because there is no API for handing you your own API key. That is the floor. Anything beyond it is a defect in your runbook, not a fact of life.
Two traps that cost hours
Your host's variable list probably shows creation time, not edit time. Ours does, and there is no flag to change that. A row reading "1d ago" tells you nothing about whether you just saved a new value. We read it as an edit time and concluded, twice, that a correctly-saved secret had not saved — sending somebody back to redo work they had already done right.
Never say "redeploy the newest deployment." That list mixes preview and production, and the newest row is frequently the wrong environment. We hit production twice aiming for staging. Name the environment explicitly, or trigger the build yourself with an empty commit on the branch.
Why we did it the slow way
Worth being honest about, because the reason generalises.
Two days earlier a file of pulled credentials had reached a git remote. The rule that came out of that was: never handle secrets, send the human to the dashboard. Sensible rule — about files. Pulling secrets into a file is what leaked.
Passing a value between two APIs in memory is a completely different act, and the rule was applied to it anyway. A safety rule enforced outside the situation it was written for stops being safety and becomes pure cost.
Every one of these is running on the board. Components worth copying are in the kit.