Webhook API Example: Build a Secure Receiver Step by Step

Looking for a Shorter Overview?

Key Moments

Verification Before Parsing

Always verify the raw request body signature before JSON parsing to ensure message integrity.

Durable Event Storage

Store events with unique IDs before acknowledging to avoid data loss and duplicate processing.

Asynchronous Processing

Acknowledge events quickly and process them asynchronously to prevent handler timeouts.

Handling Retries and Duplicates

Use proper HTTP response codes and unique event IDs to manage retries, duplicates, and dead-letter queues effectively.

You've wired up a simple POST endpoint, it returned 200 in testing, and now it's live. Then a 1-star review arrives twice, posts two alerts, creates two CRM notes, and a third event disappears while your server was deploying. That gap between a demo and production is where webhooks break. This Webhook API example treats the receiver as a reliability contract, not just a route. You'll verify the raw body, store the event before you respond, and deduplicate by event ID.

Webhook API Example Payloads and Headers

Say a customer leaves a 1-star Google review. You don't want to poll an API every few minutes to find it. Instead, the provider sends an HTTP POST to a URL you control as soon as the event happens.

The safe order is verify, store, acknowledge, then work.

That push model is the core of the design. As the webhook receiving best practices describe it, a provider sends an HTTP POST request to an endpoint controlled by the receiving application when a specific event occurs, and you expose an HTTPS route, register its URL, validate the signature, and return a successful 2xx response. Delivery is generally at-least-once, so the same review can arrive more than once and you need a stable event ID as an idempotency key.

A review.created Request

The request carries routing, security, and business data in different places. Headers tell you whether you can trust it and whether you've seen it before. The JSON body tells you what happened and which handler should run.

Part Example value What your endpoint does
Method and path POST /webhooks/reviews Accept only POST on a dedicated HTTPS route
Content-Type application/json Read the raw body as bytes before parsing
X-Webhook-Id evt_9f3k2m7x1a Store as the idempotency key
X-Webhook-Timestamp 1719849600 Reject if outside the tolerance window
X-Webhook-Signature v1,hex HMAC-SHA256 Recompute over the ID, timestamp and raw body, then compare
body.event review.created Route to the matching handler
body.data.review.rating 1 Trigger a team alert for ratings of 2 or lower
body.data.review.source Google Tag and group reviews by platform

Practical rule: Don't parse first and verify later. The signature covers the exact bytes you received, so capture them untouched before any JSON library reformats them.

Two conditions define success for the sender. Your route accepts POST over HTTPS, and it returns a 2xx response after the event is safely recorded. Everything else in the receiver exists to protect that contract when networks fail, senders retry, or someone forges a request.

Build and Verify the Receiver in Six Steps

A minimal handler that parses JSON and sends a Slack message inline will pass a demo. It won't survive a slow downstream call or a duplicate delivery. Build the six steps in order, and keep business work out of the request path.

A six-step infographic illustrating the process for building and verifying a secure webhook receiver API.

Steps 1 to 3 Expose, Capture, Verify

  1. Expose a POST route on HTTPS. Create a dedicated path such as /webhooks/reviews, register its full URL with the sender, and subscribe only to events you handle. Don't reuse a general API route with browser auth, redirects, or CSRF middleware. The sender isn't a browser session.

  2. Read the raw request body as bytes. Configure your framework to preserve the original payload before JSON parsing. In Express style setups, that usually means a raw body parser for this route or a verify hook that stores the buffer. In other frameworks, read the stream directly and parse a copy afterward.

  3. Verify the signature and timestamp before anything else. Check that the signature header exists, recompute an HMAC-SHA256 digest with your per-endpoint secret over the signed content, and compare it in constant time. Return 401 if the header is missing or the digest doesn't match. Reject timestamps outside a short tolerance window, about five minutes in this example, to block replays of captured requests.

The verification step is where most first implementations fail. GitHub's webhook troubleshooting guidance recommends using a webhook secret together with the X-Hub-Signature-256 header, and it stresses that verification must use the raw request body before JSON parsing or reserialization because changing whitespace, field ordering, or encoding can produce a different cryptographic digest. If your framework parses and stringifies the JSON first, every legitimate signature can fail.

Apply the same discipline you'd apply to any public API surface. DevArmor's REST API protection guide is useful background here because a webhook route is still an internet-facing endpoint, even though authentication runs in the opposite direction. You aren't sending a key. You're proving the caller holds the secret.

Steps 4 to 6 Store, Acknowledge, Process

  1. Write the raw event to durable storage. Insert the payload, headers you need for debugging, received time, and event ID into a queue or table guarded by a unique constraint on that ID. If the insert conflicts, you've already accepted this event. Return success without repeating side effects, which prevents duplicate testimonials and repeated alerts.

  2. Return 2xx quickly, before enrichment. Don't tag the review, call AI analysis, update widgets, post to Slack, or sync a CRM in the handler. GitHub's guidance on handling webhook deliveries requires the receiving server to return a 2xx response within 10 seconds, and if the response takes longer, GitHub terminates the connection and treats the delivery as failed. A handler that waits on a CRM call is the usual cause of that timeout.

  3. Process asynchronously and record status. A worker reads the queued event, routes on body.event, performs the business action, and marks the row processed, failed, or dead-lettered with an error message. That status trail is what lets you replay one event instead of guessing from application logs.

Acknowledging before durable storage creates silent data loss. Processing everything synchronously creates timeouts. The safe order is verify, store, acknowledge, then work.

Rotate secrets without dropping events by accepting both the old and new secret during a short overlap. Try the new secret first, then the old one, log which one matched, and remove the old value once the sender confirms rotation. A hard cutover turns a routine credential change into a stream of 401 responses.

A healthy log line is specific. It should show the event ID, event type, received time, response status 200, and acknowledgment time well under your internal target. If you can't tell from one line whether an event was verified, stored, and queued, add those fields before launch.

Finish with a local signature test. Sign a fixture payload with your test secret, send it once, and confirm 200. Change one character in the body without changing the signature, send it again, and confirm 401. I wouldn't ship a receiver until both results are boringly repeatable.

Handle Retries, Duplicates and Replays

Retries are the delivery model, not an edge case. A sender that gets a 500 at 09:00 will send the identical payload again, and your status code is the only instruction it gets about whether another attempt can succeed. Pick the code on that basis.

Response or event Provider behavior Your action
2xx success No retry Log the success and stop
3xx redirect Treated as failed and retried Point the URL at the final route
408 Request Timeout Retried Acknowledge sooner
429 Too Many Requests Retried with backoff Reduce load and confirm rate limits
5xx server error Retried with exponential backoff and jitter Fix the internal failure before the retry window closes
Timeout or connection failure Retried Confirm the response leaves within the target
Other 4xx Generally not retried Fix the signature, route or payload
Final failed attempt Sent to dead-letter queue once the sender's retry schedule is exhausted Replay manually after the fix

Most of those rows are transient. Extend's webhook best practices state that 408, 429, 5xx, timeouts, and connection failures are commonly retriable, whereas other 4xx responses generally indicate permanent rejection, and recommend a five-second internal acknowledgment target. Treat a bad signature as permanent: return 401 and stop. Return 5xx only when a transient internal failure prevented durable storage, because a 5xx on a malformed payload buys you nothing except a longer retry tail.

Duplicates, Dead Letters and Replay

The unique event ID does the work. The first delivery inserts the row and queues processing. A repeat delivery hits the constraint, so you return 2xx without a second Slack alert, testimonial import, or CRM note. The same guard is what keeps integrations like auto-connecting review sources from multiplying one review across downstream tools. Our webhooks send events to Zapier, Make, or any endpoint when a new review arrives, so each destination still needs its own deduplication check.

Don't assume every sender retries the same way. GitHub's documentation on failed webhook deliveries states that failed deliveries are not automatically redelivered, although developers can retrieve delivery records through the REST API and redeliver failed events by hand. Plan for that gap. After an outage, run a reconciliation job that polls the authenticated source API for reviews created during the window and inserts any missing events through the same deduplication path.

Repeated replay of old or failing events isn't only a technical nuisance. It can duplicate customer-facing actions and distort reporting, which is the angle covered in the research on the business impact of replay abuse. Keep event IDs at least as long as the sender's maximum retry period, or a late retry will look new.

When attempts run out, the dead-letter queue should retain the payload, destination, attempt count, last status, and error history. Test the full loop before launch:

  • Duplicate test: Send one signed payload twice with the same event ID, then confirm one stored review and one alert.
  • Failure test: Force a 500 in a test environment, watch retriable attempts, and confirm the event lands in the dead-letter queue.
  • Replay test: Fix the cause first, replay the dead-lettered event, and confirm it processes once.

Connect Events to Slack, a CRM and Zapier

Keep inbound and outbound work separate. Your receiver verifies and queues the review event. A worker then fans it out, so a slow Slack or CRM call never holds the sender's request open and one failing destination doesn't block the others.

Destination How the receiver connects Best first use
Slack Post a JSON message to an incoming webhook URL Alert the team when a 1-star review arrives
CRM such as HubSpot or Salesforce REST API call that upserts a contact or note by email Log customer feedback on the contact record
Zapier Send the event to a Catch Hook trigger URL No-code routing to a sheet or email
Make Send the event to a custom webhook module Branching scenarios with conditional steps
Your own database Insert the raw event with a unique event ID Analytics and reconciliation after an outage

Mapping a 1-Star Review

For Slack, build a short message from the rating, platform, reviewer name, and review text, then POST it to the incoming webhook URL with a short client timeout. Record the Slack delivery status against the event ID. If Slack returns an error, retry that outbound delivery from the worker. Don't mark the original review event as lost.

For a CRM, upsert rather than create. Match on the reviewer's email when you have it, attach the review as a note, and store the external event ID on the record or in an idempotency field. A repeat delivery then updates the existing activity instead of creating a duplicate contact or a second note.

Zapier and Make fit when a non-engineering team owns the next step, such as adding a row to a sheet or sending an email. Send the same normalized event to the catch-hook or custom webhook URL as a separate destination, not as a replacement for your database record. You can review the available Bragly integrations to decide which destinations your team needs.

I would start with one destination, usually your database plus a Slack alert for low ratings. Add the CRM, Zapier, or Make only after the duplicate and replay tests pass. Each extra destination multiplies the places where a retry can create visible noise.

Measure Health and Go Live

A receiver can appear healthy because it's returning 200 while dropping work in the worker. Track the request path and the processing path separately, and alert on age, not just error counts. These numbers also make it easier to sync reviews reliably once real traffic starts.

Metric What it tells you Action when it worsens
Delivery success rate Share of events accepted and stored Check signature, route and storage errors
Retry rate How often senders must redeliver Find timeouts and 5xx causes
p95 acknowledgment latency How slowly you respond at the tail Move work out of the handler
Permanent-failure rate Events rejected for non-retriable reasons Fix payload, auth or routing
Duplicate-event rate How often event IDs repeat Check timeout-induced retries
Oldest undelivered event age How long work sits unprocessed Scale workers or fix a stuck queue

Set a five-second internal acknowledgment target so slow networks still leave margin before a sender timeout. Add alerts for endpoint disablement, a growing dead-letter queue, and an oldest-event age that keeps rising. Put a rate limit on the route, but don't use it as a substitute for signature verification.

Go-live gate: If you can't prove what happens on a duplicate, a forced failure, and a replay, you're not ready to receive live reviews.

For local testing, expose your development server through a tunnel so the sender can reach it. Run the signed-payload test for 200, the altered-body test for 401, the duplicate test, the forced-failure test, and the dead-letter replay against that public test address. Confirm logs show event IDs and processing status for every case.

Go live only when all five checks pass. Then watch the six metrics during the first real deliveries, especially acknowledgment latency and duplicate rate. Those two numbers usually reveal the first production problem before customers notice a missing or repeated review.


Connect your review sources once and let Bragly send each new review event to Zapier, Make, or the secure endpoint you just built, so your team can alert, sync, and display fresh social proof without polling. Start with Bragly today and put this receiver pattern to work on live review events.

Related Posts

Asking for Reviews That Actually Get Answered

Getting reviews starts with asking at the right time and place—when the experience is clear and fresh. Use tailored channels and authentic, respectful messages to…

Auto Connect Reviews That Actually Stay Fresh

Auto connect reviews have evolved beyond convenience into essential infrastructure, demanding precise governance, frequent syncing, and clear source management to keep social proof fresh and…

Sync Reviews Across Sources and Keep Testimonials Fresh

Keeping testimonials up-to-date demands more than auto-sync—it requires clear rules for source prioritization, review conflict handling, and governance to build trust and prevent outdated or…

Questions Answered

How should a webhook receiver verify the integrity of incoming events?

By checking the signature header using HMAC-SHA256 on the raw payload before parsing.

What is the recommended order of operations for safe webhook processing?

Verify signature, store event, send acknowledgment, then process asynchronously.

How should retries, duplicates, and dead-lettered events be handled?

Return appropriate status codes and use unique event IDs to deduplicate and replay.

Why separate inbound reception from outbound processing like Slack or CRM updates?

To avoid blocking the webhook handler and ensure reliable, independent downstream flows.
Previous Article

What Is a Detractor in NPS? a Practical Guide

Next Article

8 Templates for Reviews That Collect Stronger Testimonials

Write a Comment

Leave a Comment

Your email address will not be published. Required fields are marked *