>_ Analyst Engineering

Webhooks Explained for Analysts: How to Specify, Test, and Debug Them

Written by Ahmed at Analyst Engineering, a Senior Technical Business Analyst with 10+ years in banking and payments delivery.

Cover for webhooks explained for analysts, showing a provider sending a signed POST to a receiving endpoint, with retry, duplicate, and ordering notes.

Key takeaways

  • A webhook is an HTTP request a provider sends to a URL your system exposes when an event happens, which reverses the usual API direction: the provider decides when to call you.
  • Webhook delivery guarantees differ per provider and must be written into requirements: Stripe retries failed deliveries for up to three days in live mode, while GitHub advises you to redeliver missed deliveries yourself.
  • Every webhook consumer must handle four realities: duplicate deliveries, out-of-order events, forged requests, and missed events, so requirements need idempotency by event ID, state checks, signature verification, and reconciliation.
  • Providers expect a fast 2xx acknowledgement: GitHub asks for a response within 10 seconds, and Stripe advises returning 2xx before any complex logic, which means processing belongs on a queue after the acknowledgement.
  • Webhooks can be tested without building a server: webhook.site shows raw deliveries, and the Stripe CLI forwards, triggers, and resends real signed sandbox events.

A webhook is an HTTP request a provider sends to your system when something happens: a payment succeeds, a refund completes, a pull request opens. It reverses the usual API direction, which is exactly what makes it fast and exactly what makes it tricky. Webhooks arrive late, twice, out of order, or not at all, and every one of those behaviors needs a requirement and a test.

Webhooks work like this: you register an HTTPS URL with the provider and choose which events you want; when an event occurs, the provider sends a POST to that URL with a JSON body describing it and a signature header proving it came from them; your endpoint verifies the signature, acknowledges with a 2xx quickly, and processes the event afterwards. If the provider gets no timely 2xx, it may retry. Analysts must specify how the consumer handles duplicates, ordering, forged requests, and missed events, because the provider’s guarantees do not cover them.

APIs for Analysts, intermediate track. Builds on Part 1, what is an API and Part 3, how to analyze an API. Full learning path: APIs for Analysts.

In payment integrations, the webhook is often where the business outcome actually arrives: the API response says “accepted”, and the webhook later says “settled” or “failed”. That makes webhook requirements some of the highest-stakes lines in a specification, and some of the most often missing. How to document them for consumers is covered in API Documentation from Scratch.

What is a webhook, and how is it different from polling?

A webhook is a push; polling is a pull. Both answer “has something changed?”, with very different trade-offs.

PollingWebhook
Who calls whomYour system calls the provider repeatedlyThe provider calls your system once per event
Speed of knowingAs slow as your polling intervalNear real time
API calls usedMany, mostly returning “no change”One per event
Your system must exposeNothingA public HTTPS endpoint
Failure modesRate limits, wasted callsDuplicates, out of order, missed deliveries, forged requests
Who controls timingYouThe provider

Mature integrations use both: webhooks for speed, and a periodic API check or reconciliation for the events that never arrived. The general pattern is laid out in integration patterns.

What does a webhook delivery look like?

A webhook is just an HTTP request, so it has the same anatomy as any API call. A simplified Stripe delivery:

POST /webhooks/stripe HTTP/1.1
Host: payments.example.com
Content-Type: application/json
Stripe-Signature: t=1789480000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e7...

{
  "id": "evt_1Q...",
  "type": "payment_intent.succeeded",
  "created": 1789480000,
  "data": {
    "object": {
      "id": "pi_3Q...",
      "object": "payment_intent",
      "amount": 2000,
      "currency": "eur",
      "status": "succeeded",
      "metadata": { "order_ref": "ORD-88213" }
    }
  }
}

The parts analysts care about:

  • Event ID (evt_...): the key for detecting duplicates.
  • Event type: which business event happened, and which handler runs.
  • The object: the state of the resource when the event was created, not necessarily its state now.
  • Your reference (metadata.order_ref): how you match the event to your own records. If you did not send it on the original API call, it is not in the webhook.
  • Signature header: proof of origin. Stripe’s Stripe-Signature carries a timestamp t and an HMAC SHA-256 signature v1.

GitHub’s deliveries follow the same shape with different names: the event type arrives in the X-GitHub-Event header, each delivery has a unique X-GitHub-Delivery identifier, and the signature is in X-Hub-Signature-256.

How do providers differ on retries, ordering, and duplicates?

This is the most important section for requirements, because “the provider handles delivery” means different things per provider. Two well-documented examples:

BehaviorStripeGitHub
Expected acknowledgementReturn 2xx quickly, before complex logicRespond with 2xx within 10 seconds
Automatic retriesUp to three days with exponential backoff in live mode; three times over a few hours in a sandboxDocumentation advises redelivering missed deliveries yourself once your server is back up
Manual redeliveryDashboard resend up to 15 days; CLI stripe events resend up to 30 daysRedeliver from the webhook’s recent deliveries
OrderingNot guaranteed; do not rely on event orderTreat as not guaranteed
DuplicatesEndpoints may occasionally receive the same event more than onceUse X-GitHub-Delivery to detect replays
SignatureStripe-Signature, HMAC SHA-256, timestamp includedX-Hub-Signature-256, HMAC SHA-256 with your secret

Read that table as a requirements finding. If your integration depends on a GitHub webhook and your service is down for an hour, nothing arrives later by itself; your requirements need a recovery process. With Stripe, three days of retries cover most outages, but your sandbox testing will not show that, because sandbox retries are much shorter. This is exactly the kind of difference API analysis should surface before the design is fixed.

What should an analyst specify for a webhook consumer?

Ten requirements. A webhook integration missing any of them has a known production failure waiting.

#RequirementExample wording
1Event catalogueConsume payment_intent.succeeded, payment_intent.payment_failed, charge.refunded. Ignore all others with 2xx.
2Payload mappingMap data.object.metadata.order_ref to the order; map amount from minor units.
3Endpoint per environmentOne registered URL and signing secret per environment; sandbox events never reach production.
4Signature verificationReject any delivery whose signature fails verification, or whose timestamp is older than 5 minutes, with no side effects.
5Acknowledgement contractReturn 2xx within 2 seconds after verification and persistence; process asynchronously.
6Idempotent processingRecord processed event IDs; a duplicate event produces no second business action.
7Out-of-order handlingNever move an order backwards; on doubt, retrieve the current object from the API before acting.
8Missed event recoveryHourly reconciliation compares open orders against the provider’s API and repairs gaps.
9MonitoringAlert when signature failures exceed a threshold or no events arrive for 30 minutes during business hours.
10Secret rotationSigning secrets rotated per security policy with an overlap window.

A few notes on the ones people get wrong:

  • Requirement 4’s 5-minute tolerance mirrors the default in Stripe’s official libraries, which reject signatures whose timestamp is more than 5 minutes old to limit replay attacks. The timestamp is inside the signed content, so it cannot be altered without breaking the signature.
  • Requirement 5 is why processing belongs on a queue. Stripe explicitly advises returning a 2xx before complex logic, such as updating an invoice in an accounting system, because slow handlers cause timeouts and timeouts cause retries and duplicates.
  • Requirement 7 matters because the payload is a snapshot. If a charge.refunded event arrives before payment_intent.succeeded, a naive handler marks the order refunded and then overwrites it back to paid.
  • Requirement 8 is the one nobody writes. Webhooks are an optimization, not a guarantee, and reconciliation design is what makes the combined system trustworthy.

If your own API sends webhooks to consumers, the same list applies in reverse: publish your event catalogue, retry policy, signature scheme, and ordering guarantees in the documentation, as Part 4 describes. The Standard Webhooks specification is a useful public reference for consistent header names and signing.

How do you test webhooks without building a server?

Start by seeing real deliveries, then test the consumer’s behavior against the ten requirements.

Capture real payloads

webhook.site gives you a unique public URL and displays every request sent to it: method, headers, and body. Register it as the endpoint in a provider’s sandbox, trigger an event, and you have a genuine payload for mapping and test data.

The Stripe CLI does more, because it works with signed sandbox events end to end:

stripe login

# Stream sandbox events to your terminal and forward them to a local endpoint
stripe listen --forward-to localhost:4242/webhook

# In a second terminal: fire a real sandbox event
stripe trigger payment_intent.succeeded

# Resend an existing event to an endpoint, to test duplicate handling
stripe events resend evt_1Q... --webhook-endpoint=we_1Q...

stripe listen prints the signing secret for the session, starting with whsec_, which the consumer under test uses to verify signatures.

Test cases for a webhook consumer

IDScenarioHow to produce itExpected
WH-01Valid signed eventstripe trigger payment_intent.succeeded2xx; order marked paid once
WH-02Invalid signatureReplay a captured payload from Bruno with one character of v1 changedRejected (400); no side effect; security log entry
WH-03Missing signature headerSame, header removedRejected; no side effect
WH-04Stale timestampReplay a genuine delivery captured more than 5 minutes agoRejected; no side effect
WH-05Duplicate deliverystripe events resend for an already processed event2xx; no second business action
WH-06Out of orderDeliver the refund event before the success event (resend in reverse order)Order ends in the correct final state
WH-07Unknown event typeTrigger an event not in the catalogue2xx; ignored; no error alert
WH-08Consumer downStop the consumer, trigger an event, restartEvent recovered by retry or reconciliation; alert raised
WH-09Slow processingDownstream dependency delayedAcknowledgement still inside the time limit; processing completes later
WH-10Wrong environmentA sandbox event sent to the production endpointRejected by signature (different secret)

WH-02 and WH-04 are where an API client earns its place: capture one genuine delivery, save it as a Bruno request pointing at the consumer’s test URL, and edit the signature or wait past the tolerance. Because you are sending the request, you control every header. The broader approach to proving duplicates are harmless is in idempotency testing.

How do you debug a webhook that did not arrive?

Work from the provider’s side inward.

  1. Check the provider’s delivery log. Stripe shows each endpoint’s event deliveries in Workbench with status codes and next retry time; GitHub lists recent deliveries per webhook with request, response, and a redeliver button.
  2. No delivery attempt at all? The event type is not subscribed, the endpoint is registered in a different environment or account, or the event never happened.
  3. Attempt with a connection error? The URL is not publicly reachable, DNS is wrong, or a firewall blocks the provider.
  4. Attempt with 4xx? Usually signature verification failing: the wrong signing secret for that environment, or a framework modifying the raw body before verification. Stripe’s documentation is explicit that the raw request body must be used.
  5. Attempt with timeout? The handler is doing the work before acknowledging. Move processing behind a queue.
  6. Delivered with 2xx but nothing happened? The problem is in your processing: check the queue, the logs by event ID, and whether the event was wrongly treated as a duplicate.

Finding the event ID in your own logs is the same skill as tracing any transaction, covered in reading production logs.

The APIs for Analysts learning path

Beginner: What is an API · API glossary · JSON for analysts · HTTP status codes · Your first collection · Why did my API request fail? · Reading an API contract

Intermediate: Analyze an API · Document an API · API test cases · Chaining and scripts · Webhooks (you are here) · GraphQL

Advanced: POCs and demos · API design review · Versioning and breaking changes · API security testing · API tests in CI

The takeaway

A webhook is an HTTP request the provider sends when an event happens, which makes integrations fast and introduces four realities: duplicates, out-of-order events, forged requests, and missed deliveries. Providers differ on retries, so write their actual behavior into requirements, then specify signature verification with a timestamp tolerance, a fast acknowledgement with processing on a queue, idempotency by event ID, state-aware handling of order, and reconciliation for anything that never arrives. Test all of it with webhook.site, the Stripe CLI, and replayed deliveries from your API client.

For documenting webhooks your own API sends, see API Documentation from Scratch, and for the test design behind the cases above, API Testing and QA Mastery for BAs. Specifying a webhook integration right now? Book a 1:1 Tech BA Coaching Call and we will pressure-test the requirements together.

Ahmed is a Senior Technical Business Analyst with 10+ years in banking and payments. He builds practical guides and tools for analysts at The Tech BA Toolkit.

Tags: Webhooks, Integration, API Testing, Stripe, Systems Analysis

About the author

Analyst Engineering is written by Ahmed, a Senior Technical Business Analyst with 10+ years of banking and payments delivery experience: ISO 20022 and SWIFT messaging, payments API integration, Kafka event validation, and production support. Every article comes from real delivery work, and each one is reviewed and updated as tools and standards change.

Free account

Practice on the Labs, keep your progress

A free account, no password: an email link signs you in. It saves your steps and self-assessments on the Labs, shows your missions on a dashboard, unlocks the solutions, and, if you tick the box, sends you new missions and articles when they ship.

Your email is used to sign you in. Nothing else, unless you ask. Privacy.