Structural / Integration · CTFL 4.0, CTAL-TA

Webhook Testing

Test asynchronous event-driven APIs where external services call your endpoints to notify you of events. Webhooks are unreliable by nature — messages can be delayed, retried, or lost entirely. You need to test accordingly.

Senior Test Lead ISTQB CTFL 4.0 · CTAL-TA

What it is

A webhook is a callback — when Event X happens in an external system (payment processed, file uploaded, subscription cancelled), that system makes an HTTP POST request to your endpoint to notify you. Unlike APIs where you pull data on demand, webhooks push data to you when something important happens.

Webhook testing verifies that your system correctly receives, processes, and acts upon webhook notifications. It’s an integration test: you’re checking that an external system and your system synchronise correctly even when messages are delivered out of order, delayed, or duplicated.

Webhooks are inherently unreliable. Networks fail. Your endpoint goes down. Retries can result in duplicate messages. The external system may not know whether the webhook was delivered, so it keeps retrying. Testing must account for all these failure modes.

Why webhooks are tricky to test

Asynchronous timing: You send a request to create a resource in an external system. The system returns 200 immediately. But the webhook is delivered 100ms later, or 5 seconds later, or after a retry following a timeout. Your test must wait for the event, not assume it happened instantly.

External control: You can’t trigger the webhook directly — the external system controls when it fires. You can trigger the action (charge a card, upload a file), but you must wait and observe the webhook arriving.

Duplicate delivery: If your endpoint is slow to respond, the external system may retry. Your system may now see the same webhook twice. Is your application idempotent? Does it create two records or one?

Order of delivery: Three webhooks may arrive out of order due to retries and network delays. Does your system handle them correctly?

Webhook flow: trigger → POST → handler → verification

A typical webhook flow has three stages:

Webhook anatomy
StageWhat happensTest focus
1. Trigger Event occurs in external system (payment gateway processes a charge) Can you trigger the webhook from the external system? Does it fire at all?
2. Delivery External system POSTs JSON payload to your endpoint (e.g. https://yourapp.co.nz/webhooks/payment) Does your endpoint receive it? Is the HTTP status code 200? Is the payload what you expected?
3. Processing Your system parses the payload, validates it, and takes action (update order status, send confirmation email) Did the right thing happen? Order status updated? Email sent? Database record created?
4. Acknowledgement Your endpoint returns 200 to signal success, or 5xx to signal failure (triggering retry) Does your endpoint return the right status? Too slow to respond?

Test scenarios every webhook system needs

Happy path

Webhook arrives, system processes it correctly, and acts accordingly. User sees the expected outcome (order confirmed, payment recorded, notification sent).

Retry and duplicate handling

Your endpoint times out or returns 5xx. The external system retries 3 times over the next hour. Only one action should happen, not three. This requires idempotency — usually via a unique webhook ID that your system tracks.

Test: Send the same webhook payload twice (with the same ID). Verify that the side effect (charge, update, send email) happens exactly once.

Invalid payload handling

Webhook arrives but is malformed: missing required fields, wrong data types, signature invalid. Your system should log the error and return 400 or 422, not crash or silently ignore it.

Test: Send a webhook with a missing field, a negative price, or a corrupted signature. Verify the error is logged and the system doesn’t process it.

Out-of-order delivery

Three webhooks arrive in the wrong order: webhook 3, webhook 1, webhook 2. Does your system handle them correctly? Usually requires a timestamp or sequence number.

Timeout and slow processing

Your endpoint takes 8 seconds to process the webhook. The external system has a 5-second timeout. The system retries. You now have concurrency: the first request is still processing while the retry arrives.

Test: Add artificial delays to your webhook handler and verify that retries don’t cause race conditions.

Tools and setup

Webhook testing platforms

webhook.site (free) — creates a unique URL that captures all HTTP requests sent to it. Inspect the request method, headers, body, and timing. No code required. Good for quick exploration and seeing what data your payment gateway actually sends.

RequestBin (free) — similar to webhook.site; older but reliable. Captures and displays webhook payloads.

ngrok (free tier available) — tunnels your local development server to a public HTTPS URL. Allows external systems to POST to your local machine without deploying. Essential for testing during development.

ngrok setup example

Suppose your local webhook endpoint is http://localhost:8080/webhooks/payment. To make it accessible to a payment gateway:

  • Install ngrok: brew install ngrok (or download from ngrok.com)
  • Run: ngrok http 8080
  • ngrok outputs a public URL like https://abc123.ngrok.io
  • Register https://abc123.ngrok.io/webhooks/payment with the payment gateway
  • When the gateway sends a webhook, ngrok tunnels it to your local http://localhost:8080/webhooks/payment
  • You see every request in the ngrok dashboard and in your local logs

This is invaluable during development — you can trigger events in the external system and watch the webhook arrive in your local app in real time, without pushing to staging.

NZ worked example: payment processor webhook

A NZ e-commerce site uses Stripe (or a similar payment processor) to handle card payments. When a charge succeeds, Stripe sends a webhook to your system:

Stripe payment.success webhook test scenario
TestSetupExpected outcome
Happy path: payment succeeds Charge a test card in Stripe dashboard. Webhook fires: event.type: "charge.succeeded" Order status changes to "paid". Confirmation email sent. Inventory decremented. Payment ID stored in database.
Duplicate webhook Intercept the webhook (with ngrok logs). Manually send it again with the same webhook ID. Order status is already "paid". Email not sent again. Inventory not decremented again. Idempotency key prevents double-processing.
Webhook arrives before payment API call Webhook sent. Your code queries the Stripe API for payment details. Timeout. System retries the query or falls back gracefully. Order is not left in a limbo state.
Invalid signature Send a webhook with a tampered body or corrupted HMAC signature. Webhook rejected with 401/403. Error logged. Order not updated.

Security testing: HMAC, replay attacks, TLS

HMAC signature verification

Webhooks must be verified to ensure they came from the external system, not an attacker. Most webhook systems (Stripe, PayPal, etc.) include an HMAC signature in a header (e.g. X-Stripe-Signature). Your endpoint must verify this signature before processing.

Test: Send a webhook with a valid payload but an invalid signature. Your endpoint should reject it.

Test: Send a webhook with a valid signature but a tampered body (e.g. changed the amount). Your endpoint should reject it.

Replay attack prevention

An attacker captures a webhook (e.g. payment received) and resends it repeatedly. Without replay protection, your system charges the customer multiple times.

Test: Capture a webhook with a network sniffer or your logs. Replay it 10 times. Verify that idempotency (via webhook ID) prevents duplicate processing.

TLS/SSL validation

Ensure your webhook endpoint is HTTPS-only. If the external system can reach your endpoint via HTTP, an attacker can intercept the webhook.

Test: Register your webhook endpoint as http:// (not https://). Some systems will reject it. Others might not. Check the documentation.

Common bugs in webhook systems

  • No signature verification — endpoint accepts any POST, allowing attackers to forge notifications
  • No idempotency — duplicate webhooks cause duplicate orders, charges, or emails
  • Slow endpoint timeout — webhook handler takes 10 seconds to complete, but external system times out at 5 seconds, causing unnecessary retries
  • No logging — webhook arrives, is processed incorrectly, and there’s no audit trail of what happened
  • Wrong HTTP status — endpoint returns 200 even when processing failed, so the external system doesn’t retry
  • Race condition on first webhook — webhook arrives before the order is fully created in your database, causing a foreign key error

Tips

Use webhook.site first. Before writing any test code, paste a webhook.site URL into the external system and trigger an event. See what data actually arrives. Often the payload structure is different from what you expected, or fields are missing.

  • Log every webhook — even if processing succeeds. Log the webhook ID, timestamp, event type, and outcome. This is invaluable for debugging.
  • Test with ngrok during dev — don’t wait until staging. Catch webhook issues early when you can see them in your local logs.
  • Implement idempotency via webhook ID — track webhook IDs in your database and skip processing if you’ve seen this ID before
  • Return 200 quickly — do lightweight validation and return 200 to the webhook immediately. Put heavy processing (email sending, API calls) in a background queue.
  • Test with intentional delays — wrap your webhook handler in a sleep(5000) and trigger the webhook. Does the external system retry? Does your app handle it?

Related: See API Testing for testing synchronous endpoints, and Security Testing for HMAC and TLS verification details.