September 12, 2026
VERIFY THE DOORBELL BEFORE YOU OPEN THE DOOR


Your webhook endpoint is a public URL. That is the whole point — the outside service must be able to reach it — and it is also the whole danger. Anyone on the internet can send an HTTP POST to a public URL. Attackers probe them constantly. Curious scripts poke them. A mistyped URL from some stranger's system can land in your logs.
So the rule is blunt: never trust a knock because it arrived. Trust it because it verifies.
Signatures: the shared-secret handshake
Signature verification works like a wax seal on a letter. The sender and you share a secret — a long random string only the two of you know. When the sender prepares an event, it computes a verification value (a hash) from the request content plus that secret, and attaches it as a signature header. When your endpoint receives the request, it recomputes the same value using its own copy of the secret and compares. Match: the letter is really from the sender and arrived unaltered. Mismatch: reject it before doing anything else.
Two details make this work in practice:
1. The secret travels once, through a secure channel — you copy it from the provider's dashboard into your hosting platform's environment settings. It never appears in the payload, the URL, the repository, or the logs. 2. The signature covers the raw request body, exactly as the provider's docs specify. Parse-then-recompute can fail because formatting changes the bytes; follow the provider's verification instructions for how to read the raw body.
A forged request fails the check because the forger does not know the secret and cannot produce a matching seal. A corrupted request fails because the bytes no longer match. Either way, your app answers with a rejection and records nothing.
First principles for every provider
Vendor docs differ in header names and hash algorithms, but the principles are identical everywhere. Teach yourself these five, and any provider's page becomes a fill-in-the-blank exercise:
1. Keep the signing secret in environment settings. It lives in your deployment platform's secret store — Vercel environment variables, Cloudflare secrets, Railway variables — never in code, never in Git, never in a chat transcript. This is the Tools Class 14 keys lesson applied directly. 2. Verify the raw request as the docs require. Read the provider's verification page for the exact header, algorithm, and body handling. Durable idea first (shared-secret check), provider page second (exact field names). 3. Reject invalid signatures before any other work. No database write, no queue message, no fulfillment. Log the rejection for your own inspection, then stop. 4. Record the event ID. Every serious sender includes a unique event ID. Store it with the record — it is your defense against the next problem. 5. Acknowledge fast. Answer the sender with success as soon as the event is verified and recorded or enqueued. The sender is waiting; long work happens later.
Replays and duplicates: the legitimate double-knock
Here is the subtlety that breaks naive endpoints: a legitimate provider may deliver the same event more than once. The sender's network timed out before your acknowledgement arrived, so it retried. Its system redelivered after an outage. An operator replayed yesterday's events during an incident. None of this is an attack — but processing the same order.paid event twice could mean fulfilling the order twice.
The defense is two parts, both building on Class 26's idempotency lesson:
- Event IDs make duplicates visible. Because you recorded the event ID on first receipt, the second arrival matches an existing row. Your endpoint can acknowledge it ("yes, received") without doing anything new.
- Idempotent processing makes repeats safe. Design the follow-up so repeating it changes nothing: fulfillment checks "was this order ID already fulfilled?" before acting; a data refresh writes to the row identified by source-plus-date instead of appending a second copy.
Short version: IDs detect the double-knock; idempotent handlers survive it.
The safe shape: verify fast, work later
Separate the response from the work, always:
POST arrives
→ check signature against env secret → invalid? reject + log, stop
→ valid? check event ID → already seen? ack, stop
→ new? write event record + enqueue job → ack fast (200 OK)
→ worker picks up job → does slow work → updates record + logs
The endpoint's job finishes in milliseconds: verify, deduplicate, record, enqueue, acknowledge. The worker's job takes as long as it takes: fetching data, calling a model, generating a file, sending a notification. If the worker fails, the queue retries the *job* — the sender already got its acknowledgement and moved on. This is the Class 26 pattern returning: the trigger starts the story, the queue carries the load.
The boundary matters for money-shaped events most of all. An endpoint that charges, publishes, or alters production data *synchronously* during verification turns every retry into a potential double-charge or double-publish. Verify and enqueue quickly; let reviewed, idempotent worker logic perform anything with consequences.
Backlinks: this is Tools 14–16 again
Nothing here is new philosophy — it is earlier access-control judgment applied to an inbound surface:
- Keys and secrets (Class 14): the signing secret is a credential. Environment settings, rotation when exposed, never in the repo.
- Least privilege (Class 15): the endpoint has the smallest authority possible — write one shaped event record, enqueue one job type. It cannot read other tables or change settings.
- MCP/API boundaries and human approval (Class 16): the webhook is an API boundary with a non-human caller. Actions above a consequence threshold — refunds, public posts, production data changes — need the same approval gate you would put on an agent's tool call, not automatic execution on receipt.
A mock pipeline, honestly labeled
Picture a mock order.paid event used purely to learn the pipeline — not to build a store, not to touch real money:
{
"type": "order.paid",
"id": "evt_mock_001",
"created": "2026-09-12T10:00:00Z",
"signature": "v1=<computed-with-shared-secret>",
"data": { "order_id": "order_mock_42", "amount": "4900", "currency": "usd" }
}
The educational pipeline: endpoint checks the signature against the mock secret in env → rejects on mismatch → records evt_mock_001 once → enqueues fulfill(order_mock_42) → acknowledges → worker marks the mock order fulfilled, idempotently. No payment provider account, no real charge, no financial-operations advice. The mechanics — seal, ID, fast ack, queued work — are the entire point.
Check your understanding
1. Why must the signing secret live in environment settings rather than in the endpoint code? 2. What are the two legitimate reasons the same event might arrive twice, and what makes the second arrival harmless? 3. Why should money- or publication-shaped work happen in a worker rather than in the endpoint?
Exercise: the security checklist
Extend the WEBHOOK-PLAN.md you started in 27.1 with a security section. Fill every line — "n/a" is not an answer:
## Security checklist
- Signature scheme (per provider docs):
- Secret location (env var name only — never the value):
- Raw-body handling (how docs require reading it):
- Invalid-signature behavior:
- Event ID field + storage:
- Duplicate rule (what happens on second arrival):
- Acknowledgement (what returns, and how fast):
- Queued work (what the endpoint enqueues vs. does):
- Logs (what is recorded for valid, duplicate, invalid):
- Test event (how you will send a signed test before going live):
Finish line: a WEBHOOK-PLAN.md whose security checklist a reviewer could audit line by line.
Verification: send (or simulate) three deliveries — a valid event, the same event again, and one with a tampered byte. Confirm: first records + enqueues, second acknowledges without new effects, third is rejected and logged. If any of the three behaves otherwise, the endpoint is not done.
Next, Lesson 27.4 assembles everything — requests, schedules, queues, events — into one event-to-action map, and shows the floor the coming agents will stand on.
ARTICLE DISCUSSION
JOIN THE
CONVERSATION.
Got a question, a take, or a better way to do this? Log in and leave a comment.
