# create a bin (plain text, one command) curl https://hookden.pages.dev/new # or JSON curl -s -X POST https://hookden.pages.dev/api/bins # send anything to the capture URL curl -X POST https://hookden.pages.dev/h/BIN_ID/any/path?x=1 -d 'payload' # list captured requests (JSON) curl -s https://hookden.pages.dev/api/bins/BIN_ID/requests
A tiny shell client (curl + sed, ~50 lines, no account needed) lives at https://hookden.pages.dev/cli:
# install curl -s https://hookden.pages.dev/cli -o hookden && chmod +x hookden # create a bin ./hookden new # live-tail captures into your terminal (Ctrl-C to stop) ./hookden tail BIN_ID # also accepts the full /h/ or /b/ URL # re-deliver captures to a server on YOUR machine (Ctrl-C to stop) ./hookden relay BIN_ID http://localhost:3000 # new captures only ./hookden relay BIN_ID http://localhost:3000 --all # history too
Each capture is printed with method, path, signature badge (if signature verification is on), headers, and a pretty-printed body. Under the hood it polls GET https://hookden.pages.dev/api/bins/BIN_ID/tail?after=N, a plain-text endpoint you can also script against directly — the x-hookden-last response header carries the cursor for the next poll.
./hookden relay turns a bin into a bridge to your dev machine: point the webhook provider at https://hookden.pages.dev/h/BIN_ID, run the relay, and every capture is re-delivered to your local server with the original method, sub-path, query string, headers (host/proxy headers stripped) and byte-identical body — so HMAC signatures like GitHub's X-Hub-Signature-256 still verify against the relayed request. No tunnel daemon, no firewall holes, no ngrok account: it's outbound polling with plain curl, so it works behind NAT, corporate proxies, and CI runners. Captures also stay in the dashboard, so you get inspection + local delivery at the same time. See the step-by-step guide.
Your bin's capture URL is https://hookden.pages.dev/h/BIN_ID. Any method, any sub-path, any query string is captured: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. Bodies are stored up to 100 KB (larger bodies are truncated). JSON bodies are pretty-printed in the dashboard, and application/x-www-form-urlencoded bodies (Slack, Twilio, GitHub's form mode…) are decoded into readable key = value lines. Binary bodies (protobuf, gzip, file uploads…) are kept byte-exact — the dashboard shows a hex dump with a "download raw bytes" link, and replay, CLI relay, and exports all resend the original bytes, so HMAC signatures still verify downstream. CORS is open on capture endpoints, so browser apps can hit them too.
Signed-in users can give any bin a memorable alias in its settings: set a slug like my-stripe-dev and the bin answers on https://hookden.pages.dev/h/my-stripe-dev (the dashboard moves to /b/my-stripe-dev too). The original random URL keeps working. Slugs are 3–40 chars of lowercase letters, digits, and hyphens, unique across Hookden — first come, first served. Handy when you re-configure the same provider often, or want a URL you can remember and type.
By default a bin answers 200 with {"ok":true}. In the bin dashboard you can set any status code (e.g. 500 to test your retry logic), content type, body, and a response delay of up to 10 seconds — handy for testing client timeouts and retry/backoff behaviour.
The response body supports {{variable}} placeholders that are filled in from each incoming request. This is how you pass provider URL-verification handshakes that require echoing a challenge — e.g. set the body to:
{"challenge":"{{body.challenge}}"}
and your bin passes Slack's (and Zoom's, Dropbox's…) endpoint verification while still capturing every event. Available variables:
| Variable | Value |
|---|---|
{{body}} | the raw request body (full echo) |
{{body.a.b}} | field from a JSON body (dot-path into nested objects) or a form-urlencoded body |
{{query.name}} | a query-string parameter ({{query}} alone = the whole query string) |
{{header.x-foo}} | a request header (case-insensitive) |
{{method}} / {{path}} | HTTP method / sub-path under the bin URL |
{{ip}} / {{timestamp}} / {{bin}} | caller IP / current Unix time (seconds) / bin ID |
{{hmac_sha256 var secret}} | hex HMAC-SHA256 of any variable's value, keyed by secret — computes crypto challenges, not just echoes |
The hmac_sha256 helper is what lets a bin pass Zoom's endpoint.url_validation CRC — a handshake that requires hashing the challenge, not just echoing it:
{"plainToken":"{{body.payload.plainToken}}","encryptedToken":"{{hmac_sha256 body.payload.plainToken YOUR_ZOOM_SECRET_TOKEN}}"}
The secret lives in your bin's response settings (visible to anyone who has the bin's dashboard). Use it for development secrets, and rotate the provider secret when you move to production. See the Zoom validation guide for the full walkthrough.
Unknown or missing variables render as an empty string. Values are substituted literally (no quoting added) — challenge tokens are alphanumeric so this is safe for the handshake use case. A body without {{ is returned exactly as-is, byte for byte.
Custom response headers (bin settings → Response headers, one Name: value per line, max 10) are templated with the same variables. That's what lets a bin pass Asana's webhook handshake, which requires echoing the X-Hook-Secret request header back as a response header:
X-Hook-Secret: {{header.x-hook-secret}}
Hop-by-hop and security-sensitive headers (Set-Cookie, Location, Content-Length, Strict-Transport-Security…) can't be overridden. Set a Content-Type line to override the configured content type per-request.
Debugging "invalid signature" errors is the worst part of webhook work. Set a signing secret in the bin's settings and Hookden verifies every incoming request's HMAC, showing a ✓ or ✗ badge per request. Supported schemes:
| Scheme | Header checked | How it's verified |
|---|---|---|
| GitHub | X-Hub-Signature-256 | sha256= + hex HMAC-SHA256 of the raw body |
| Notion | X-Notion-Signature | sha256= + hex HMAC-SHA256 of the raw body — GitHub's exact wire format, but the key is the one-time verification_token Notion POSTs to your endpoint when the subscription is created (you don't choose it). See the Notion guide for capturing that token with a bin. |
| Stripe | Stripe-Signature | v1 = hex HMAC-SHA256 of {t}.{body} (timestamp tolerance not enforced — this is a debugging tool) |
| Calendly | Calendly-Webhook-Signature | Stripe's exact wire format under a different header: t=<unix>,v1=<hex>; v1 = hex HMAC-SHA256 of {t}.{body}, key = the subscription's signing key (you choose it with personal access tokens; OAuth apps get one auto-generated). Timestamp tolerance (docs suggest ~3 min) not enforced — this is a debugging tool. |
| Mux | Mux-Signature | Stripe's exact wire format under a different header: t=<unix>,v1=<hex>; v1 = hex HMAC-SHA256 of {t}.{body}, key = the webhook's signing secret (per-endpoint — dashboard webhooks settings page, or returned once in the create response for API-created webhooks). Timestamp tolerance (SDK default 5 min) not enforced — this is a debugging tool. |
| Mailchimp | X-Mailchimp-Signature | Stripe's exact wire format under a different header: t=<unix>,v1=<hex>; v1 = hex HMAC-SHA256 of {t}.{body} — the body is the raw urlencoded form bytes, key = the signing secret Mailchimp shows exactly once at webhook creation (signing_secret in the API create response). Timestamp tolerance (docs suggest 5 min) not enforced — this is a debugging tool. |
| WorkOS | WorkOS-Signature | Stripe's exact wire format under a different header — with one twist: t=<ms>,v1=<hex> where t is milliseconds since epoch (Stripe/Calendly/Mux/Mailchimp use seconds); v1 = hex HMAC-SHA256 of {t}.{body}, key = the endpoint's webhook secret from the WorkOS dashboard. Timestamp tolerance (SDK defaults 3–5 min) not enforced — this is a debugging tool. |
| Paddle Billing | Paddle-Signature | header is ts=<unix>;h1=<hex>; h1 = hex HMAC-SHA256 of {ts}:{body} (colon-joined), key = the destination's pdl_ntfset_… secret key used as-is (never decoded). Multiple h1 values during key rotation all checked. Timestamp tolerance (SDK default 5s) not enforced — this is a debugging tool. |
| Svix / Standard Webhooks | Svix-Signature (or Webhook-Signature) | v1, + base64 HMAC-SHA256 of {id}.{timestamp}.{body}, key = base64-decoded part of the whsec_… secret. Used by Clerk, Resend, Railway, OpenAI platform webhooks and every Standard-Webhooks sender. Both svix-* and plain webhook-* header names (white-labeled Svix, OpenAI) are accepted. |
| Twitch EventSub | Twitch-Eventsub-Message-Signature | sha256= + hex HMAC-SHA256 of {message-id}{timestamp}{body} (concatenated, no separators — the exact scheme from Twitch's docs) |
| Square | x-square-hmacsha256-signature | base64 HMAC-SHA256 of {notification_url}{body} — Square signs the URL itself, so the bin verifies against the exact URL the request hit. Point the subscription straight at the bin and paste the subscription's signature key. Generic HMAC testers can't verify this scheme because they don't know the signed URL. |
| HubSpot | X-HubSpot-Signature-v3 | base64 HMAC-SHA256 of {method}{uri}{body}{timestamp} (timestamp from X-HubSpot-Request-Timestamp), key = the app's client secret. Like Square, the URI is inside the signed content, so the bin verifies against the URL the request actually hit. Legacy v1 signatures (plain SHA-256 of secret+body in X-HubSpot-Signature) verify too. |
| Mailgun | (in the body — no header) | hex HMAC-SHA256 of {timestamp}{token}, key = your Webhook Signing Key. The signature rides inside the payload itself: event webhooks POST {"signature":{timestamp,token,signature},"event-data":…}; legacy webhooks & inbound routes send the same three values as form fields. Both shapes verify. |
| SendGrid | X-Twilio-Email-Event-Webhook-Signature | ECDSA P-256 / SHA-256 over {timestamp}{body} (timestamp from X-Twilio-Email-Event-Webhook-Timestamp), signature base64 DER. Not HMAC — it's public-key crypto: paste the base64 Verification Key from Settings → Mail Settings → Signed Event Webhook Requests as the "secret" (it's a public key, so nothing sensitive is stored). HMAC testers structurally can't verify this scheme. |
| Intercom | X-Hub-Signature | sha1= + hex HMAC-SHA1 of the raw body, key = your app's client_secret (Basic Info page). Note the algorithm: SHA-1, not SHA-256 — GitHub-style sha256 verifiers fail on every Intercom delivery. |
| Airtable | X-Airtable-Content-MAC | hmac-sha256= + hex HMAC-SHA256 of the raw notification-ping body, key = the base64-decoded macSecretBase64 that the create-webhook call returns exactly once. Paste the base64 string as the secret — the bin decodes it before keying (keying with the undecoded string is the classic bug). |
| Kick | Kick-Event-Signature | RSA PKCS#1 v1.5 / SHA-256 over {messageId}.{timestamp}.{body} (ids from Kick-Event-Message-Id / Kick-Event-Message-Timestamp), base64 signature. Not Ed25519, not HMAC — and there's no per-app secret: paste Kick's published public key (PEM from the docs or api.kick.com/public/v1/public-key) as the "secret". |
| Buildkite | X-Buildkite-Signature | timestamp=<unix>,signature=<hex> — hex HMAC-SHA256 of {timestamp}.{body}, key = the webhook's Token value. (Buildkite sends either this header or clear-text X-Buildkite-Token, depending on the Notification Service setting — the signature mode is the one worth verifying.) |
| Frame.io | X-Frameio-Signature | v0= + hex HMAC-SHA256 of v0:{timestamp}:{body} (Slack's exact format under Frame.io headers), timestamp from X-Frameio-Request-Timestamp, key = the webhook's signing secret — returned only in the create-webhook response. Same scheme for V4 and Legacy; only the user-agent differs. |
| Zendesk | X-Zendesk-Webhook-Signature | base64 HMAC-SHA256 of {timestamp}{body} — the timestamp is the ISO-8601 value of X-Zendesk-Webhook-Signature-Timestamp (not unix epoch), and the signing secret is used verbatim (Zendesk's own sample never base64-decodes it). Watch out: test deliveries fired before the webhook is created sign with the documented static secret dGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ==, not your real one. |
| Trello | X-Trello-Webhook | base64 HMAC-SHA1 of {body}{callbackURL} — Trello signs the callbackURL exactly as provided at webhook creation, so the bin verifies against the URL the request actually hit (URL-signing family, like Square/HubSpot). Key = your application's secret (the OAuth1.0 secret from the Power-Up admin API Key tab). |
| Webflow | x-webflow-signature | hex HMAC-SHA256 of {timestamp}:{body} (colon-joined), timestamp from x-webflow-timestamp in milliseconds. Key = the OAuth app's client secret, or the per-webhook secret for site-token webhooks. Heads-up: webhooks created in the Webflow dashboard send no signature headers at all. |
| Adyen | none — signature travels in the body | base64 HMAC-SHA256 per item in notificationItems[].NotificationRequestItem.additionalData.hmacSignature, computed over 8 extracted fields joined with colons (pspReference:originalReference:merchantAccountCode:merchantReference:value:currency:eventCode:success) — not the raw body. Key = the hex HMAC key from the Customer Area, hex-decoded before keying (using the hex characters as the key is the classic Adyen bug). Batched items are each verified. Banking/Management webhooks instead send an hmacsignature header over the raw body — the bin falls back to that automatically. |
| Patreon | X-Patreon-Signature | plain hex HMAC-MD5 of the raw body (no prefix). Key = the webhook's secret (viewable any time on the webhooks page or in API webhook objects — not show-once). Patreon is the only major provider still signing with MD5; WebCrypto has no MD5 at all, so most crypto.subtle-based verifiers can't check it — the bin (and the signature debugger) uses a pure-JS HMAC-MD5. The trigger arrives separately in X-Patreon-Event (e.g. members:pledge:create) — one webhook can carry several triggers, so branch on that header, not on payload shape. |
| Generic HMAC | any header you name | hex or base64 HMAC-SHA256 of the raw body, optional sha256= or v1= prefix (the latter covers CircleCI's circleci-signature: v1=… versioned list) — with automatic HMAC-SHA1 fallback (bare hex or sha1=), which covers Segment's X-Signature and other legacy sha1 senders. The base64 compare also covers DocuSign Connect: set header x-docusign-signature-1, secret = the Connect HMAC key. |
Verification runs on the full raw body, even when the stored copy is truncated. Many providers (Shopify, Slack-adjacent tools, most "webhook secret" implementations) use one of these shapes.
Captured a webhook you wish had hit your real endpoint? Every request has a Replay ▸ button (bin owner only): give it a target URL and Hookden re-sends the exact method, headers, and body to it, then shows you the status code and latency your endpoint returned. The replayed request carries an X-Hookden-Replay: 1 header so you can tell it apart on your side.
Targets must be public http(s) hosts — for localhost, use a tunnel (e.g. cloudflared tunnel --url localhost:3000). Replays time out after 10s. Or use "Copy as" (curl / fetch / Python / HTTPie) and run it yourself.
Don't want to take your webhook offline while you debug? Set a Forward to URL in the bin's settings and every captured request is also mirrored to your real endpoint — same method, headers, and body, with the sub-path and query string appended to your URL. Point your provider at Hookden, keep production flowing, and watch everything in the live dashboard at the same time.
Forwarded requests carry X-Hookden-Forwarded: 1 and X-Hookden-Bin headers. Forwarding is fire-and-forget (your webhook sender always gets the bin's configured response) and times out after 10s. Same public-host rule as Replay.
Every capture has a Copy as menu: get the exact request as a ready-to-run curl command, a JavaScript fetch() call, a Python requests snippet, or an HTTPie command — headers and raw body included (hop-by-hop and CF headers stripped). Paste a real webhook straight into your test suite.
Why did yesterday's delivery work and today's fail? Hit Diff… on any two captures in the dashboard and get a colorized, line-by-line diff of the request line, headers, and (pretty-printed) body — long unchanged stretches are collapsed so the change jumps out. Works for everyone viewing the bin, no account needed.
| Anonymous | Free account | |
|---|---|---|
| Requests kept per bin | 100 | 1,000 |
| Retention | 24 hours | 30 days |
| Bins | unlimited | unlimited |
| Ingest rate per bin | 120/minute | 300/minute |
Over the ingest rate, the bin answers 429 with a Retry-After header until the minute window clears. New-bin creation is capped at 30/hour per IP.
When a bin is full, oldest requests are dropped first. Need more? Tell me — limits are soft while Hookden grows.
Heads-up: Cloudflare's edge (not Hookden) answers 403 error code: 1010 to Python's default Python-urllib user-agent. If you test with urllib, set any User-Agent header — requests, curl, Go, Java, Ruby and every real webhook sender work as-is.
GET https://hookden.pages.dev/new # create bin (plain-text answer)
POST https://hookden.pages.dev/api/bins # create bin → {bin,url,inspect}
GET https://hookden.pages.dev/api/bins/:id/requests # list requests (?after=ID for polling)
DELETE https://hookden.pages.dev/api/bins/:id/requests # clear captured requests
GET https://hookden.pages.dev/api/bins/:id/export # download all captures as JSON
GET https://hookden.pages.dev/api/bins/:id/har # download as HAR 1.2 (Chrome DevTools, Insomnia…)
DELETE https://hookden.pages.dev/api/bins/:id # owner; delete the bin entirely
POST https://hookden.pages.dev/api/bins/:id/requests/:rid/replay # owner; body {"target":"https://…"}
GET https://hookden.pages.dev/api/bins/:id/requests/:rid/share # mint read-only share link → {url}
Machine-readable contract: https://hookden.pages.dev/openapi.json — OpenAPI 3.1 spec for every endpoint (schemas, params, rate limits). Import it into Postman/Insomnia/Bruno, generate a client, or hand it to your AI agent.
Hookden is a native MCP server — AI agents (Claude Code, Cursor, or any MCP client) can create capture bins and read webhooks as first-class tools instead of scraping the API. Streamable HTTP transport, no auth, no sessions:
{ "mcpServers": { "hookden": { "type": "http", "url": "https://hookden.pages.dev/mcp" } } }
Claude Code one-liner: claude mcp add --transport http hookden https://hookden.pages.dev/mcp
Tools exposed: create_bin, list_captures, get_capture, wait_for_capture, send_test_webhook, set_response, and verify_signature. wait_for_capture blocks up to 45 s until the next webhook arrives, so an agent can trigger a webhook and read exactly what was sent (headers + raw body, binary-safe) in one step; send_test_webhook seeds a realistic labeled sample (any of the 67 provider payloads) so an agent with no HTTP client can still run the whole loop. set_response configures what the bin replies (status, content-type, body, headers, delay — with the same {{…}} templates as the settings page), so an agent can make its bin pass Slack/Zoom/Meta/Strava verification handshakes on its own. verify_signature checks a webhook signature against a secret with no bin at all — the same 25 provider-exact schemes as the bin badges (incl. SendGrid’s ECDSA, Kick’s RSA and Patreon’s HMAC-MD5, which generic HMAC tools structurally can’t verify); the secret is used in-memory for that one computation and never stored or logged. Same bins, same limits as the REST API; a human can watch the agent's bin live at its /b/… URL. Full walkthrough (Claude Code, Cursor, generic clients): Give your AI agent a webhook endpoint.
Every capture has a Share link button: it copies a read-only permalink (https://hookden.pages.dev/r/…) to just that one request — headers, body, signature badge. Paste it in Slack, a GitHub issue, or a bug report. The link shows only that capture: it never exposes your bin, its URL, or other requests, and it can't be guessed (HMAC-signed). It works as long as the capture is retained (24h anonymous / 30 days with an account).