The Complete Guide to Webhooks
What a webhook is (and when to use one)
A webhook is a reverse API call. Instead of your application repeatedly asking a service "has anything happened yet?", the service calls you - an HTTP POST to a URL you control - the moment an event occurs. The classic phrasing is that webhooks are "user-defined HTTP callbacks"; you register a URL once, and the provider pushes data to it whenever the relevant thing happens.
The alternative is polling, and the contrast is stark. A poller asking every minute wastes thousands of empty requests a day and still adds up to a minute of latency to every real event. A webhook fires within seconds of the event and sends zero requests when nothing is happening. For anything event-driven - a form submission arriving, a payment settling, a build finishing - webhooks are the correct primitive.
Use a webhook when another system needs to react to an event in near real time and you control code at the receiving end: create a CRM contact when a lead submits, open a support ticket, post to a team channel, kick off a background job, or sync a record to your data warehouse. Reach for a no-code integration instead when the destination is a common SaaS tool and you do not want to host an endpoint, and reach for the REST API when you need to ask the question on your own schedule (a nightly export, an on-demand lookup). The three are complementary; mature stacks use all of them. Formspring fires a signed webhook on submission events, exposes the same data over a REST API, and offers direct integrations for the common destinations.
The anatomy of a webhook delivery
Every webhook delivery is an ordinary HTTP request, and understanding its parts is what lets you handle it safely.
- Method and URL. Almost always
POSTto the endpoint URL you registered. Your endpoint must be publicly reachable over HTTPS - plain HTTP leaks the payload in transit and is rejected by most senders. - Headers. This is where the metadata lives. Expect a
Content-Type(usuallyapplication/json), a signature header carrying an HMAC and a timestamp, and frequently a unique delivery or event ID header. The signature header is the one that makes the request trustworthy; the ID header is the one that makes it safe to retry. - Body. A JSON payload describing the event: an event type, a timestamp, and the resource that changed (the submission, the payment, the record). Treat the body as untrusted until the signature checks out.
- The expected response. Your endpoint should return a
2xxstatus quickly to acknowledge receipt. Any non-2xx (or a timeout) tells the sender the delivery failed, which triggers a retry. The single most common production mistake is doing slow work before returning 2xx, so a healthy delivery times out and gets retried needlessly.
The mental model that prevents most bugs: a webhook is an at-least-once notification, not an exactly-once command. The network can duplicate it, reorder it, and replay it. Everything in the rest of this guide follows from designing for that reality. Formspring's webhook payload documents the exact envelope, fields, and headers you will receive.
Verifying HMAC signatures
Your webhook URL is a public POST endpoint. Anyone who learns it - from a log, a referrer header, a leaked config - can forge requests to it. Signature verification is what turns "any request that hits this URL" into "a request I can prove came from the real sender."
The standard mechanism is an HMAC (hash-based message authentication code). The sender shares a secret with you out of band, then for each delivery computes HMAC-SHA256(secret, signed_payload) and sends the result in a header. You recompute the same HMAC on your side using the raw request body and your copy of the secret; if the two match, the payload is authentic and untampered. RFC 2104 defines HMAC; the security property you are relying on is that without the secret, an attacker cannot produce a valid signature for a payload they chose.
Three details separate a correct implementation from a broken one:
- Sign the raw bytes, not the parsed object. Re-serializing JSON reorders keys and changes whitespace, which changes the hash. Capture the request body before any framework parses it, and HMAC exactly those bytes.
- Compare in constant time. Use a timing-safe comparison (
hash_equalsin PHP,crypto.timingSafeEqualin Node,hmac.compare_digestin Python) so an attacker cannot probe the secret byte-by-byte via response-time differences. - Check the timestamp. Signed payloads typically prefix a timestamp (
t=...,v1=...). Recompute the HMAC overtimestamp + payload, and reject deliveries whose timestamp is older than a few minutes. This defeats replay attacks where someone captures a valid delivery and re-sends it later.
Formspring signs every delivery with a timestamped SHA-256 HMAC, and the step-by-step verification post shows correct, copy-pasteable receivers in Node, PHP, and Python. If you implement only one thing from this guide, implement signature verification - an unsigned webhook handler is an unauthenticated write endpoint.
Idempotency: handling duplicate deliveries
Because webhooks are delivered at least once, your endpoint will receive the same event more than once - after a retry, after a network blip that ate your 2xx, after a sender-side replay. If processing a duplicate creates a second CRM contact, sends a second confirmation email, or charges a card twice, you have a correctness bug waiting for a busy day.
The fix is idempotency: make processing the same event twice indistinguishable from processing it once. The reliable pattern uses a stable identifier:
- Read the unique event/delivery ID from the payload or header (every well-designed webhook provides one).
- Before doing any work, check whether you have already processed that ID - a unique column in your database, a key in a fast store with a TTL, or a dedicated
processed_eventstable. - If it is new, record the ID and do the work in the same transaction so a crash between the two cannot leave you half-done. If it already exists, acknowledge with 2xx and stop.
A database unique constraint on the event ID is the most robust version: an attempted duplicate insert fails atomically, and you treat that failure as "already handled." Avoid using payload contents or arrival time as the dedup key - only the provider's stable event ID survives reordering and partial failures.
Idempotency and retries are two halves of one design: retries guarantee you eventually get the event, idempotency guarantees getting it twice is harmless. Formspring includes a stable delivery ID on every webhook for exactly this, and the idempotency deep-dive walks through the dedup table and transaction pattern in full.
Retries and exponential backoff
Endpoints go down. They deploy, they hit a slow query, they get rate-limited, they 500. A webhook sender that gave up on the first failure would silently lose events; a sender that retried immediately and forever would hammer a struggling endpoint into staying down. The answer both sides should expect is retries with exponential backoff.
The sender retries failed deliveries (non-2xx or timeout) on a schedule that doubles the wait each time - say 1 minute, then 2, 4, 8, 16, and so on, often with a little random jitter added so a fleet of failed deliveries does not retry in a synchronized thundering herd. Google's SRE guidance on handling overload and AWS's writeup on backoff and jitter both make the same case: backoff gives the dependency room to recover, and jitter spreads the load. Retries usually continue up to a maximum window (commonly 24-72 hours) before the delivery is marked permanently failed.
What this means for you, the receiver:
- Return 2xx fast. Acknowledge first, process asynchronously. If you do the slow work before responding, a retry may arrive before the first attempt finishes, and now you are processing the same event in parallel.
- Distinguish retryable from fatal failures. Return 5xx (or a timeout) when you want the sender to try again - a transient DB outage. Return 4xx for a payload you will never accept, so the sender stops wasting attempts. Never return 2xx for an event you failed to process; that tells the sender to forget it forever.
- Make retries safe. This is where idempotency from the previous section pays off - every retry hits the same dedup guard.
Formspring retries failed deliveries with exponential backoff automatically, and the retry-strategies explainer covers the backoff math and the receiver-side rules in detail.
Replaying and debugging failed deliveries
Even with retries, deliveries permanently fail - your endpoint was down longer than the retry window, or a bug in your handler 500'd every attempt. When you fix the problem, you need to recover the events you missed, and you need a record to debug why they failed. That is what a delivery log and replay are for.
A good webhook system keeps a delivery log: for each attempt, the timestamp, the response status and body your endpoint returned, the latency, and the attempt number. This turns debugging from guesswork into reading. "Every delivery since 14:00 got a 502" points straight at a deploy; "401 Unauthorized" points at your signature check rejecting valid payloads (usually because you hashed the parsed body instead of the raw bytes).
Replay lets you re-send a past delivery on demand once your endpoint is healthy again - recovering missed events without asking the sender to manually re-trigger anything. Because you built idempotency, replaying a delivery you actually did process is harmless: it hits the dedup guard and no-ops.
When you are developing locally, you do not want to deploy to test a handler. Capture real payloads from the delivery log and feed them to your endpoint with curl, or use a tunnel that exposes your local server to the sender. Verify the signature path works against a real signed payload early - it is the part most likely to be subtly wrong. Formspring keeps a full delivery log with one-click replay, and the post on why deliveries fail and how to replay them catalogues the failure signatures and their fixes.
Building a receiver that does not fall over
Pulling the pieces together, a production webhook receiver follows the same shape regardless of language or framework:
- Read the raw body before parsing. You need the exact bytes for signature verification.
- Verify the signature with a timing-safe comparison, and check the timestamp to reject replays. Reject with 401 if it fails - and log it, because a flood of 401s usually means your verification is broken, not an attack.
- Check idempotency. Look up the event ID; if seen, return 2xx and stop.
- Persist the event and return 2xx immediately. Write the raw event to a queue or table, acknowledge, and get out of the request cycle. Do not call your CRM, send email, or run reports inside the request.
- Process asynchronously. A background worker does the real work - and because the event is already stored idempotently, that worker can itself retry safely.
The two failure modes this shape prevents are the ones that bite everyone. Doing slow work inline causes timeouts and duplicate processing under retry. Trusting the payload before verifying lets a forged request write to your systems. Get the order right - raw body, verify, dedup, ack, process - and the endpoint stays boring under load, which is exactly what you want from it.
Keep the handler small and the worker idempotent, and a webhook receiver becomes one of the most reliable parts of your stack rather than the 3 a.m. page. The webhook configuration docs cover setting the endpoint, secret, and event types; the recipes show end-to-end receivers wired to common destinations.
Security hardening checklist
Beyond signatures, a handful of measures close the remaining gaps. Treat this as a pre-launch checklist for any endpoint that accepts webhooks:
- HTTPS only. Reject plain HTTP; the payload and headers are sensitive in transit.
- Verify the signature on every request, before any processing, with a constant-time compare. No exceptions for "internal" traffic.
- Enforce a timestamp tolerance (a few minutes) to defeat replay of captured deliveries.
- Store the signing secret as a secret - environment variable or secret manager, never in source control or client-side code. Rotate it if it may have leaked, and support two valid secrets briefly during rotation so in-flight deliveries do not fail.
- Validate and bound the payload. Cap the body size you will accept, validate the schema, and never
evalor interpolate payload fields into shell, SQL, or templates - a webhook body is attacker-influenceable input even when authenticated. - Do not leak in errors. Return generic failure messages; a verbose stack trace in the response body hands an attacker a map of your internals.
- Rate-limit and alert. A sudden spike in 401s, a flood from one source, or a delivery-failure alarm should reach a human. Watch the delivery log the way you watch error rates.
- Keep the URL unguessable but not secret. Treat the endpoint path as discoverable; your security comes from the signature, not from a hidden URL.
None of these is expensive, and together they turn a public POST endpoint into a trustworthy one. The signing documentation and the broader security overview cover how Formspring implements the sender side of this checklist.
Webhooks vs polling vs integrations
Three ways to move an event from one system to another, each right in different situations.
Polling has your code ask the source on a schedule. It is simple and needs no public endpoint, which makes it the fallback when you cannot receive inbound requests (behind a corporate firewall, a CLI script, a notebook). The cost is latency and waste: you trade real-time for an interval, and you send mostly-empty requests forever. Poll when you genuinely cannot accept a callback, or when you need a periodic snapshot rather than per-event reactions.
Webhooks invert that: the source pushes to you in near real time, with no wasted requests. The cost is operational - you host an endpoint and you own signature verification, idempotency, and retry handling. Choose webhooks when reactions must be timely and you control code at the receiving end. This guide is the manual for doing that part well.
Direct integrations are a managed webhook you do not have to build. The provider runs the endpoint and the plumbing; you connect an account and pick a destination. Choose an integration when the target is a common tool - a team chat, a spreadsheet, a CRM, an automation platform - and you would rather not host or maintain a receiver. The trade is flexibility: you get what the integration offers, not arbitrary custom logic.
The right architecture is rarely one of these alone. A typical setup fires a direct integration to Slack for instant human visibility and a webhook to the system of record for durable processing, while reserving the REST API for scheduled exports. Formspring offers all three against the same submission data, so you can match each consumer to the mechanism that fits it.