Webhooks are the hardest part of an API to debug. They're asynchronous, they fire from someone else's server, and the only signal you get when something goes wrong is a customer complaint days later. The usual loop — sender → your handler → silent failure — leaves you reverse-engineering a black box.
Outworx Docs ships a webhooks playground that turns that loop into something you can see, replay, and verify. A unique capture URL catches incoming requests in real-time. Send-test fires signed payloads at any handler. Replay re-runs a captured request against a different URL. And the HMAC verifier matches signatures from ten different webhook providers — Stripe, GitHub, Shopify, Square, Linear, HubSpot, Zoom, Zendesk, Slack — entirely in your browser.
This guide walks the whole flow: capturing a real webhook, replaying it against your local handler, sending test events, verifying signatures, locking inboxes down for production traffic, and the FAQ that always comes up about tunnels, retention, and rate limits.
What you can debug with this
Before the steps, the use cases. People reach for a webhook playground when:
- Stripe / Polar / Lemon Squeezy aren't firing the event you expected and you need to confirm whether it's the sender or the handler
- GitHub Actions sends a workflow webhook and you want to see the full payload schema before writing your matcher
- Shopify orders are dropping a field intermittently and you need to capture the raw JSON to file a bug
- You're building a webhook receiver from scratch and want to confirm your HMAC verifier works against real traffic before going live
- A flaky integration is sending duplicates and you want to see the timing + bodies side by side
- Your CI is testing webhook handlers and you want a deterministic replay endpoint instead of asking Stripe to fire test events on a schedule
If any of those rings a bell, this is the guide.
Prerequisites
- An Outworx Docs account (free signup includes 5 webhook inboxes)
- Something that emits webhooks — Stripe, GitHub, your own service in test mode
You don't need a project, a spec, or any prior setup. Webhooks is a standalone workspace at /webhooks in the dashboard.
Step 1 — Create an inbox
Go to /webhooks in the dashboard and click + New inbox. Give it a label like "Stripe staging" — only you see it. Linking to a project is optional and we'll skip it for now.
You'll land on a page with a prominent capture URL:
https://docs.outworx.io/api/inbox/2NX_7mCiC46d.......Click Copy. This is the only auth on the inbox by default — anyone with the URL can post to it, so treat it like a webhook signing secret. (We'll lock it down in Step 6.)
Step 2 — Point a sender at it
Paste the URL into Stripe's webhook configuration (Dashboard → Developers → Webhooks → Add endpoint), or GitHub's repo webhooks settings, or whatever sender you're debugging. Send a test event from the sender's UI.
The capture appears in the dashboard's live stream within ~200ms — no refresh needed. We use Postgres logical replication via Supabase Realtime, so every authenticated browser tab subscribed to that inbox sees inserts the moment they happen.
Click any captured row to open the inspector. You get the full method + path, the source IP and ASN, every header (cookies stripped for safety), and the body pretty-printed if it's JSON / XML / form-encoded. Binary uploads (multipart) get a hex-dump preview with a download button.
Step 3 — Verify the signature
Most webhook senders sign their payloads. The Outworx HMAC verifier handles ten common schemes plus raw HMAC variants:
- Stripe (v1, hex)
- GitHub (sha256 + legacy sha1, hex)
- Slack (v0, hex, with timestamp basestring)
- Shopify (sha256, base64)
- Square (sha256 over URL + body, base64)
- Linear (sha256, hex)
- HubSpot (v3, sha256 over method + URI + body + timestamp, base64)
- Zoom (v0, sha256, hex, with timestamp basestring)
- Zendesk (sha256 over timestamp + body, base64)
- Outworx (our own Send-test signatures)
- Raw HMAC-SHA1 / 256 / 512 in hex or base64
The verifier auto-detects the right scheme from the captured headers. Open a capture, expand "Verify signature (HMAC)", paste your signing secret, click Verify. Math runs in WebCrypto in your browser — your secret never crosses our network.
If the math passes, you also get a staleness check: schemes with timestamps (Stripe, Slack, HubSpot, Zoom, Zendesk) flag signatures older than 5 minutes, since most senders reject those as a replay defence. Useful when you're debugging "why does Stripe say my signature is valid but my handler rejects it" — odds are your timestamp tolerance is too tight.
Step 4 — Replay against your handler
Found a malformed payload that broke production? Reproducing it normally means asking the sender to fire another one — Stripe doesn't make that easy. Outworx solves this with one click.
Hover the captured row, click Replay. The Send-test modal opens with the original body and headers pre-filled. Paste your handler URL (or a tunnel URL like https://abc.ngrok.io/webhooks for a local handler), optionally add your signing secret, and fire. We sign with HMAC-SHA256 in Stripe-v1 format under X-Outworx-Signature: t=<unix>,v1=<hex> so your handler can verify with the standard library it already uses.
Replays drop the original sender's signature header — the math wouldn't verify against your secret anyway, and we add X-Outworx-Replay: true so a paranoid handler can refuse replays if it wants.
The response from your handler is captured back into the same inspector view: status code, headers, body, timing. So a replay loop reads top-to-bottom: original capture → modified replay → your handler's response. That's usually enough to nail down whether the handler is the bug.
Step 5 — Send test events without waiting for the sender
The Send-test modal isn't just for replays. From /webhooks, click Send test to fire any payload at any URL:
- Method: POST, GET, PUT, PATCH, or DELETE
- Custom headers: Authorization, X-Webhook-Source, etc.
- Signing secret: optional HMAC-SHA256 in your choice of scheme
- Body: paste raw JSON, or pre-fill from a spec event
If your project's OpenAPI spec includes the OpenAPI 3.1 webhooks: block, those events show up under /webhooks?section=events with per-event Send buttons that pre-fill payloads from the schema. One click and you've fired a sample invoice.paid at your handler — useful when you're building a new integration and don't want to wait on Stripe to actually issue an invoice.
Step 6 — Lock the inbox down for production traffic
The default inbox is open — anyone with the URL can post. That's the point during exploration. When you're ready to point real traffic at it, open Security in the inbox toolbar and turn on any combination of three layers:
- HMAC signature verification — pick a scheme (Outworx, Stripe, GitHub sha256/sha1, Slack, Shopify, Linear, Zendesk, or Custom HMAC where you set the header name, algorithm, encoding, and prefix), paste the shared secret, and the receiver will reject anything missing or mismatched with
401. Schemes that sign a timestamp also get replay protection — configurable tolerance, default 5 minutes. - HTTP Basic auth — username + password gate; missing or wrong credentials get
401 + WWW-Authenticateso well-behaved senders prompt for the right ones. - Source IP allowlist — paste IPv4 / IPv6 CIDR ranges, one per line. Off-network requests get
403. Useful for providers that publish their egress IPs (Stripe, GitHub).
Each layer is independent. Replays auto-sign with the inbox's stored secret using the configured scheme, so a locked-down inbox accepts its own replays without you having to re-paste the secret. Secrets and passwords never round-trip to the browser — the dialog shows a "leave blank to keep" affordance so saving other settings doesn't expose the stored value.
The combination "HMAC + IP allowlist" is what most production teams ship. Stripe publishes their egress IPs; lock to those, verify the signature, done.
Localhost and tunnels
Outworx runs in the cloud and can't reach localhost on your machine. The Send-test modal shows a tunnel hint when it detects a localhost URL:
npx ngrok http 3000
# or
cloudflared tunnel --url http://localhost:3000
# or
npx localtunnel --port 3000Paste the resulting public URL into Send-test. The captured response shows up in the inspector even when your local handler is the destination, so you can debug console.log output by reading the response body in the dashboard.
When you're running the Outworx app itself on localhost in dev mode, we auto-allow loopback addresses — no tunnel needed. Production is HTTPS-only with the SSRF guard fully active so a captured webhook can't be replayed against 169.254.169.254 (instance metadata) or any internal RFC1918 range.
Plan limits
Free includes:
- 5 inboxes
- 500 captures received in any rolling 7-day window
- 100 sends/day
- 7-day retention
Pro: 25 inboxes, 10,000 captures / 30 days, 5,000 sends/day, 30-day retention.
Business: unlimited everything, 90-day retention.
The capture cap is enforced via an append-only ledger that survives "Clear all" — so a malicious user can't reset their quota by clearing inboxes. Senders that hit the cap receive HTTP 429 with Retry-After until ledger entries age out.
Frequently asked questions
Does the inbox URL change if I rotate it? Yes — that's the point. Rotation is on the inbox page; it generates a new token, the old URL stops accepting captures, and you'll need to update the sender to the new URL. Senders will get 404 on the old URL, which is intentional (they don't quietly fall back to a stale endpoint).
Can I have multiple inboxes pointing at the same handler? Yes. Common pattern: one inbox per environment (staging / prod / per-customer). Each inbox has its own capture URL, its own retention window, its own security config, and its own replay history.
What happens during the 7-day / 30-day / 90-day retention window? Captured requests are stored with their full headers + body for the retention period. After expiry they're hard-deleted. The capture count quota uses an append-only ledger that tracks count separately from row presence — so deleted captures still count toward your monthly cap. This stops the "clear inbox to reset cap" exploit.
Can I replay an old capture after retention expires? No — once a capture is hard-deleted, the body is gone and replay isn't possible. Pro / Business retention windows exist precisely so production teams have enough time to investigate intermittent issues without losing the body.
Is my webhook signing secret stored anywhere? When you use the verifier in the dashboard, no — the secret is computed in WebCrypto in your browser and never leaves it. When you turn on inbox-level Security, the secret is stored encrypted at rest on our servers (server-side HMAC verification needs it). The "leave blank to keep" affordance means you can update other security settings without re-pasting the secret.
How do I make my CI tests deterministic? Pre-record a capture during exploration, then drive replays from the dashboard — open the capture and click Replay, or save it as a Sender template and trigger it from there. The body is byte-identical every time, the signature is reproducible, and the test runs in milliseconds instead of waiting on a real Stripe event. (The inbox token is capture-only by design — there's no anonymous-token replay endpoint, so a leaked token can't be used to re-fire arbitrary payloads at your handler.)
Can I forward all captures to multiple handlers? Not natively. The pattern is: one Outworx inbox → your "fan-out" handler → multiple downstream services. Outworx is the inspection point, not the routing layer. (If a fan-out feature is something you actually need, drop us a line — we'll prioritize it if there's demand.)
What about webhook order? Will captures arrive in send order? In practice yes (sub-millisecond), but webhooks are inherently parallel and HTTP doesn't guarantee delivery order. If your handler relies on order, fix that bug — webhook senders explicitly disclaim order. Use a sequence ID in the payload itself if you need it.
Do I need to whitelist Outworx IPs in my firewall? Only for the replay direction — your handler receives requests from Outworx's egress. We publish the current egress IP range in the dashboard's Security panel. The capture direction is inbound to us; senders don't see Outworx IPs.
What about WebSocket or SSE webhooks? The capture URL is HTTP only — it accepts any method, any body, but doesn't upgrade connections. WebSocket-style "webhooks" (the Slack RTM API, Discord gateway) need a different tool. Most "webhooks" in modern usage are plain HTTP POSTs, which Outworx handles.
What this replaces
If you were using webhook.site, ngrok inspect, RequestBin, or a tab full of console.log statements in your handler — Outworx Webhooks is the integrated version. Capture + replay + signature verification all in one workspace, with plan limits sized for real production debugging instead of a forced upgrade after the third capture.
The capture URL is yours forever (or until you rotate it). The signing secret is yours and never crosses our network. Everything else — the schemas, the docs, the AI chat, the MCP server — is right next door if you need it.
Open /webhooks in your dashboard and create your first inbox. The whole flow is under thirty seconds.
Related guides
Outworx REST API — Manage Projects from CI
Drive every Outworx Docs action from a script: create projects, push spec versions, promote a default. Personal access tokens, copy-paste curl, and the full OpenAPI spec.
Outworx CLI — push specs from anywhere
Publish OpenAPI / Swagger / GraphQL specs to Outworx Docs from your laptop or any CI provider. One command, zero install graph, scriptable exit codes.
PR preview deploys — every pull request gets its own docs URL
Push specs from CI with `--preview` and Outworx spins up a throwaway version with a banner showing PR / branch / commit. 14-day rolling TTL, hidden from public version listings, three lines of GitHub Action.
Ship your API docs in under a minute.
Upload your OpenAPI, Swagger, or GraphQL spec and get beautiful hosted docs with AI chat and a per-project MCP server — free forever for 2 projects.