How to Test Webhooks: Tools and a Step-by-Step Guide

A practical webhook testing guide: capture payloads, tunnel localhost, trigger Stripe/GitHub/Slack events, and assert your handler with Apidog.

INEZA Felin-Michel

INEZA Felin-Michel

7 July 2026

How to Test Webhooks: Tools and a Step-by-Step Guide

Apidog for Enterprise

On-Premises Deploy

SSO & RBAC

SOC 2 Compliant

Explore Apidog Enterprise

To test a webhook, you give the provider a reachable URL, trigger a real event, and then confirm your handler accepts the payload and reacts correctly. The hard part is that your app is the receiver, not the caller, so you can’t just hit “Send” and read the response. This guide walks through the tools you need (inspection services, local tunnels, provider triggers) and a repeatable process to assert your handler end to end.

button

Why webhooks are harder to test than normal APIs

With a normal API call, you control the request. You pick the method, set the body, fire it, and read the response. Webhooks flip that. The provider sends the request to you, on its own schedule, with a payload it defines. That role reversal creates four testing problems.

First, you can’t trigger the event yourself by default. A payment_intent.succeeded event only fires when Stripe decides it did, so you need provider tooling to force one.

Second, delivery is asynchronous. The event arrives whenever the upstream action completes, so your test has to capture an inbound request rather than block on a return value.

Third, the payload shape is provider-defined. Your handler has to parse exactly what GitHub, Stripe, or Slack sends.

Fourth, most providers sign their requests. Your endpoint must verify a signature header before trusting the body, and a test that skips this hides real bugs. For a deeper look, see our guide on webhook signature verification.

If you’re still deciding whether webhooks are even the right pattern for your integration, webhooks vs polling and webhook vs WebSocket compare the trade-offs.

The webhook testing toolkit

You’ll reach for four kinds of tools, often in the same session: a capture service to see raw payloads, a tunnel to reach your laptop, provider triggers to fire real events, and an assertion tool to verify your handler.

1. Inspection and capture services

Before you write a single line of handler code, point the provider at a throwaway URL and look at what it actually sends. These services give you a public endpoint and show every request in real time.

webhook.site gives you a unique random URL and email address the moment the page loads. Everything sent to it appears instantly: full body, headers, and method. Free URLs expire after 7 days and cap at 100 requests, with a 10 MB max request size. Paid plans add permanent URLs, unlimited requests, and a retained history of the latest 10,000 requests.

Beeceptor offers a free HTTPS endpoint you can use as a webhook receiver and inspect incoming payloads in real time. It also provides mock-server endpoints, so you can capture and simulate from the same tool.

Pipedream RequestBin is another widely used request-capture tool. Check its current docs for tier specifics, since capture services change their free limits often.

Use these to answer one question: what does the real payload look like? Copy a sample for later, when you build repeatable tests.

2. Local development: expose localhost with a tunnel

Providers can’t reach localhost:3000 on your machine. A tunnel creates a public HTTPS URL that forwards traffic to your local port, so you can run your handler in your editor and debug it live.

ngrok is the common choice. Install it, authenticate once, then forward a port.

brew install ngrok                          # macOS
ngrok config add-authtoken $YOUR_TOKEN
ngrok http 3000                             # forwards a public HTTPS URL to localhost:3000

Free ngrok accounts get an automatically chosen dev domain. Custom domains require a paid plan.

cloudflared runs a quick tunnel with a single command if you prefer Cloudflare.

cloudflared tunnel --url http://localhost:3000

Paste the public URL the tunnel prints into the provider’s webhook settings, and inbound events land on your local server. For a fuller walkthrough of this pattern, see how to test localhost APIs with webhook services.

3. Triggering test events from the provider

A capture URL only helps once something is sent to it. Most major providers ship tooling to fire test events on demand, so you don’t have to manufacture real payments or pushes.

Stripe CLI is the cleanest example. The stripe listen command forwards live events from your Stripe sandbox to a local path, and it prints a webhook signing secret you put in your app config.

# Forward all events to your local handler:
stripe listen --forward-to localhost:3000/webhooks

# Filter to specific events only:
stripe listen --events payment_intent.succeeded,checkout.session.completed \
  --forward-to localhost:3000/webhooks

With the listener running, stripe trigger fires a test event. Note that triggering creates properly backed API objects and has side effects: payment_intent.succeeded also emits payment_intent.created.

stripe trigger payment_intent.succeeded
stripe trigger checkout.session.completed
stripe trigger --help                       # list every supported event

GitHub keeps a short delivery history you can replay. Go to your repo, then Settings, then Webhooks under “Code and automation.” Click the webhook URL, open the “Recent deliveries” tab, click a delivery GUID, and click “Redeliver.” Two constraints matter: you can only redeliver deliveries from the past 3 days, and you need admin access to the repo. GitHub does not auto-redeliver failed deliveries, so replay is manual.

Slack incoming webhooks are the simplest to test. You send a message by POSTing JSON to the webhook URL. The minimal payload is just a text field.

curl -X POST https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX \
  -H 'Content-type: application/json' \
  -d '{"text":"Hello, world."}'

The Slack webhook URL is a secret. Slack revokes URLs that leak, so keep it out of client-side code and public repos.

4. Asserting your handler

Capturing and triggering prove the plumbing works. They don’t prove your handler is correct. The final tool sends a crafted payload and checks the response plus side effects. At its most basic, that’s a curl command with a representative body.

curl -X POST http://localhost:3000/webhooks \
  -H 'Content-Type: application/json' \
  -H 'Stripe-Signature: t=...,v1=...' \
  -d '{"id":"evt_test","type":"payment_intent.succeeded","data":{"object":{"id":"pi_123","status":"succeeded"}}}'

A single curl is fine for a smoke test. It falls short when you want repeatable, asserted tests that run in CI. That’s where a dedicated API tool earns its place.

Testing webhooks with Apidog

Apidog is an all-in-one API platform for designing, debugging, testing, mocking, and documenting APIs. It has no dedicated “webhook inbox,” so this section is honest about what it actually does well: crafting payloads, saving them, asserting responses, and standing in for a provider with a mock server.

Craft and save a sample payload as a reusable request

Take the payload you captured from webhook.site (or copied from a provider’s docs) and build it into an Apidog request. Set the method to POST, paste the JSON body, and add the headers the provider sends, including the signature header your handler checks.

Save that request against your endpoint. Now you have a repeatable, version-controlled way to POST a known-good (or deliberately malformed) payload to your handler, instead of retyping a curl command every time.

Assert the response with the visual assertion builder

Sending the payload is half the test. The other half is checking what your handler returns. In the request or scenario step, open Post Processors, click “+ Add,” and choose “Assertion.” Apidog adds a validation rule you configure without code.

Point the assertion at the response body and use $ for the JSON root. For example, target $.data.status, set the condition to Equals, and compare to succeeded. Run the request, and the result shows in the Assertion tab. Visual assertions compare values as strings. When you need type-precise checks (a real number, a boolean), switch to a custom script using the Postman-compatible pm.test syntax.

This turns “it returned a 200” into “it returned a 200, the status field is succeeded, and the order id matches.” Build several assertions into one scenario to cover the success path, a missing-field case, and a bad-signature rejection.

Use a mock server to stand in for the provider

Sometimes you want to test the other direction: a service in your stack that calls out to a provider. During local testing you may not want to hit the real provider at all. Apidog’s mock server lets you stand in for it.

Apidog offers three mock types. Local Mock runs with the desktop client and works only while the client is open. Cloud Mock is hosted on Apidog servers, runs 24/7, and toggles on or off (it’s off by default). Runner Mock runs on self-hosted runner infrastructure and is shared across your team. Each HTTP endpoint has a Mock module; copy the mock URL from the API tab in Design mode or the Mock tab in Debug mode. Only paths starting with / route to the mock environment. Point your code at that mock URL during tests, and your integration gets predictable provider responses without real API calls or side effects.

Run webhook tests in CI with the Apidog CLI

Once your scenario passes locally, run it on every push with the Apidog CLI. It requires Node.js v16 or later.

npm install -g apidog-cli
node -v && apidog -v && which node && which npm && which apidog   # verify install

Run a saved scenario online, passing the access token and IDs from the scenario’s CI/CD “Command line” tab.

apidog run --access-token $APIDOG_ACCESS_TOKEN -t 637132 -e 358171 -d 3497013 -r html,cli

Here -t is the test scenario, -e is the environment (required), -d is the test data (a CSV or JSON file path, or a stored data-set ID), and -r sets the reporters, which accept cli, html, json, and junit. For a full command-line walkthrough, see the Apidog CLI tutorial.

Want to build and assert your own webhook tests visually? Try Apidog free, no credit card required.

A step-by-step webhook testing workflow

Putting the tools together, here’s a repeatable sequence.

  1. Inspect the real payload. Point the provider at a webhook.site URL and capture one real event. Note the body shape, headers, and signature format.
  2. Reach your code. Start your handler locally, open a tunnel with ngrok http 3000 or cloudflared, and put the public URL into the provider’s webhook config.
  3. Trigger a real event. Use stripe trigger, GitHub’s Redeliver button, or a Slack POST to send a genuine event through the tunnel.
  4. Verify signature handling. Send the same payload with a valid signature and again with a tampered one. Your handler must accept the first and reject the second.
  5. Assert the response and side effects. Save the payload as an Apidog request, add assertions on the response body and status, and confirm the database row or downstream call happened.
  6. Automate it. Move the scenario into the Apidog CLI so it runs in CI on every change.

If you’re designing the webhook system itself rather than just consuming one, how to design reliable webhooks and payment webhook best practices cover retries, idempotency, and security. For the broader picture, the webhooks API guide and webhooks and event-driven architecture explain where webhooks fit in a larger system.

Frequently Asked Questions

How do you test a webhook?

Give the provider a reachable URL, trigger a real or test event, then confirm your handler receives the payload, verifies the signature, returns the right status, and performs the expected side effect. Capture a real payload first, then build a repeatable request with assertions so the test runs the same way every time.

How do you test webhooks locally?

Run your handler on a local port, then expose it with a tunnel so the provider can reach your machine. Use ngrok http 3000 or cloudflared tunnel --url http://localhost:3000, paste the public HTTPS URL into the provider’s webhook settings, and trigger an event. Inbound requests land on your local server, where you can set breakpoints and inspect them live.

How do you test a webhook with Postman?

Postman can POST a crafted payload to your endpoint and assert the response, but it can’t receive a real inbound webhook on its own. The same applies to Apidog: you build a request with the provider’s payload and headers, add assertions on the response body and status, and save it as a reusable test. To capture a genuine inbound event, pair the tool with a capture service or a tunnel.

How do you test Stripe webhooks?

Use the Stripe CLI. Run stripe listen --forward-to localhost:3000/webhooks to forward sandbox events to your handler and get a signing secret. Then run stripe trigger payment_intent.succeeded (or any event from stripe trigger --help) to fire a real test event. Triggering creates backed API objects and can cascade, so payment_intent.succeeded also emits payment_intent.created.

How do you test a Slack webhook?

POST JSON to the incoming webhook URL. The minimal valid payload is {"text":"Hello, world."}, sent with a Content-type: application/json header. A successful test posts the message into the configured channel. Keep the URL secret, since Slack revokes webhook URLs that leak.

How do you test a webhook URL?

Send a sample POST to the URL and confirm it returns a success status and behaves correctly. The fastest check is a single curl with a representative body. For a real test, capture an actual payload first, send it with both valid and invalid signatures, and assert the response and side effects rather than just checking for a 200.

Practice API Design-first in Apidog

Discover an easier way to build and use APIs

How to Test Webhooks: Tools and a Step-by-Step Guide