WebhookTool HTTP API
WebhookTool gives you a URL that records every HTTP request sent to it, and every DNS lookup of a name under it, and every email sent to an address of its own. Everything the dashboard does is also available over this API, for scripts, test suites and AI agents. The base URL is https://webhooktool.com, and request and response bodies are JSON unless noted.
Quick start
Create an inbox. The response holds its id, its webhook URL in urls.ingest and its secret, which is returned only this once.
curl -s -X POST https://webhooktool.com/api/v1/inboxesSend it anything.
curl -X POST 'https://whks.dev/<id>/orders?source=test' \
-H 'Content-Type: application/json' \
-d '{"hello": "world"}'Read what arrived.
curl -s -H 'Authorization: Bearer <secret>' https://webhooktool.com/api/v1/inbox/capturesOpen https://webhooktool.com/#<secret> in a browser to see the same inbox in the dashboard.
MCP for AI agents
AI agents can use WebhookTool through its MCP server at https://webhooktool.com/mcp. It needs no account or sign-in, because the agent creates its own inboxes and passes each inbox's secret to the tools that read or change it. To let an agent use an inbox you already have, give it the inbox's private link.
Add it to Claude Code.
claude mcp add --transport http webhooktool https://webhooktool.com/mcpOr to Codex.
codex mcp add webhooktool --url https://webhooktool.com/mcpIn VS Code, add "webhooktool": {"type": "http", "url": "https://webhooktool.com/mcp"} under servers in mcp.json. In Cursor, add "webhooktool": {"url": "https://webhooktool.com/mcp"} under mcpServers in mcp.json. Other agents take it as a remote MCP server with this URL, over Streamable HTTP.
| Tool | Does |
|---|---|
create_inbox | Create an inbox, with any settings. Returns its URLs, dashboard link and secret. |
get_inbox | Get the inbox. |
update_inbox | Change its settings. |
delete_inbox | Destroy it and everything in it. |
wait_for_capture | Wait for the next capture, or the next one matching a search, and return it. |
list_captures | List or search captures, without their headers and bodies. |
get_capture | Get one capture with its headers and body. Long bodies come in parts. |
get_capture_as | Get a request as code that sends it again. |
check_signature | Check an Amazon SNS or PayPal signature with the sender's certificate, a Google Pub/Sub, Microsoft Graph or Azure Event Grid token with its issuer's published keys, an App Store notification with Apple's root certificate, or a decoded JWT from Google, Microsoft or Apple. |
set_capture_note | Set or remove a note. |
delete_captures | Delete captures by ID, by search, or all of them. |
Anyone who knows an inbox's URL can send to it, so the server tells agents to treat captures as data and not to follow instructions in them.
Inboxes and secrets
- An inbox has an
id, a UUID that is part of its webhook URLhttps://whks.dev/<id>. Anyone who knows the webhook URL can send requests to it, but reading them needs the secret. To operate and maintain the service, the operators may also access inbox data, as the privacy page explains. - The
secret, 26 lowercase letters and digits, reads, changes and deletes the inbox. Send it as a bearer token:Authorization: Bearer <secret>. It is returned only when the inbox is created and cannot be recovered. The linkhttps://webhooktool.com/#<secret>opens the inbox in the dashboard, andhttps://webhooktool.com/#<secret>/<capture-id>opens one capture. - An inbox and everything in it are deleted
retention_secondsafter the inbox was created: 1 day unless you choose between 5 minutes and 3 days.expires_atsays when. You can change the time later, but not to less than the inbox's current age. - An inbox keeps its newest
capture_limitcaptures: 50 unless you choose 1 to 100, and at most 10.0 MiB in total. Older captures are deleted to make room. - How many inboxes you can have at a time is limited. Destroying an inbox makes room right away.
- How many inboxes you can create within 1 day is limited too, and destroyed and expired inboxes count toward it. Once you reach it, you can create the next inbox 1 day after your latest one.
Sending requests
Any method, path, query string, headers and body sent to https://whks.dev/<id> or a path below it are recorded. Bodies are recorded up to 1.0 MiB. Anything beyond that is cut off and the capture is marked "truncated": true.
The inbox answers with the response in its settings, plus an X-Request-Id header holding the capture's id and X-Robots-Tag: noindex, nofollow, which keeps inbox URLs out of search engines. To answer one request with another status, start the path with a status code from 200 to 599, as in https://whks.dev/<id>/404/anything.
Browsers that load the webhook URL themselves, as a page, a frame, a script, an image or the like, get the body as text/plain; charset=utf-8 whatever content_type says, so an inbox can't show visitors a web page, start a download or run as a script. Calls from a page's scripts with fetch() or XMLHttpRequest, and requests from anything that isn't a browser, such as webhook senders and curl, get the response exactly as set.
With cors on, responses allow every origin, and CORS preflight requests (OPTIONS with Origin and Access-Control-Request-Method headers) are answered with 204 and allow the requested method and headers.
Other answers from the webhook URL are plain text.
| Status | Meaning | Recorded |
|---|---|---|
404 | There is no inbox with that id, or it expired. | No |
403 | The request asked for a service worker script, or one of WebhookTool's own pages asked for a script, worker or style sheet. Inboxes do not serve those. | No |
429 | Requests are reaching the inbox too fast. Retry after Retry-After seconds. | No |
429 | Too many responses with a delay are waiting to be sent for the inbox. | Yes |
503 | The server is busy. Retry after Retry-After seconds. | No |
507 | The server's storage is full, and no inbox had older requests to give up. When storage is full, the inboxes of the network that stores the most lose their oldest captures first to make room. | No |
DNS lookups
Lookups of any name under <id>.1.whks.dev are recorded in the inbox and answered with no records. The inbox's urls.dns holds that pattern. Lookups count toward the same rate limit as requests, and lookups above it are answered but not recorded.
dig hello.<id>.1.whks.devA lookup is a capture with "kind": "dns", "method": "DNS" and a dns object. Its remote_ip is the address of the resolver that sent the lookup, usually not that of the system that made it. When the resolver's address lies in a network's published ranges, insights.network names that network, such as google-public-dns for Google Public DNS.
Emails
Emails to any address at <id>.email.whks.dev, such as hello@<id>.email.whks.dev, are recorded in the inbox. The inbox's urls.email holds that pattern. Messages up to 1.0 MiB are recorded, and longer ones are cut off. Mail for an unknown inbox is refused. Emails count toward the same rate limit as requests, and those above it are refused for the time being, so mail servers send them again later.
An email is a capture with "kind": "email", "method": "SMTP" and an email object. Its proto is SMTP, ESMTP, or ESMTPS after STARTTLS, or UTF8SMTP and UTF8SMTPS when the sender used SMTPUTF8, and its remote_ip is the address of the sending mail server. headers lists the message's header fields in the order they came, files its attachments and other parts by the part's index, and body is the raw message, downloaded as message/rfc822. Getting one capture also returns text and html, the message's plain text and HTML, decoded.
Each email is checked with SPF, DKIM and DMARC when it arrives, and email.auth holds the results: whether the sending server may send mail for the envelope sender's domain, whether each DKIM signature verifies, and whether the message passes the DMARC policy of its From address's domain. The checks use the DNS records the sender's domains had then. Only DMARC ties the From address to its domain's owner.
Endpoints
Every endpoint except POST /api/v1/inboxes needs the inbox's secret as a bearer token, and acts on that inbox. Browsers can only call endpoints that change something from WebhookTool's own pages. Such requests from other sites are refused with 403. Scripts and other programs that are not browsers are not affected.
| Endpoint | Does |
|---|---|
POST /api/v1/inboxes | Create an inbox. |
GET /api/v1/inbox | Get the inbox. |
PATCH /api/v1/inbox | Change its settings. |
DELETE /api/v1/inbox | Destroy it and everything in it. |
GET /api/v1/inbox/captures | List or search captures. |
DELETE /api/v1/inbox/captures | Delete all captures, or those matching a search. |
GET /api/v1/inbox/captures/{id} | Get one capture with its body. |
DELETE /api/v1/inbox/captures/{id} | Delete one capture. |
GET /api/v1/inbox/captures/{id}/body | Download the raw body. |
GET /api/v1/inbox/captures/{id}/files/{n} | Download an uploaded file. |
GET /api/v1/inbox/captures/{id}/as/{format} | Get the request as code that sends it again. |
PUT /api/v1/inbox/captures/{id}/note | Set or remove a note. |
POST /api/v1/inbox/captures/{id}/signature | Check a signature with the sender's certificate or its issuer's published keys. |
GET /api/v1/inbox/export | Download captures as JSON, CSV or HAR. |
POST /api/v1/inboxes
Creates an inbox and answers 201 Created with the inbox, including its secret. The body is optional and may hold any settings to start with.
curl -s -X POST https://webhooktool.com/api/v1/inboxes \
-H 'Content-Type: application/json' \
-d '{"retention_seconds": 86400, "response": {"status": 202, "body": "accepted"}}'GET /api/v1/inbox
Returns the inbox.
PATCH /api/v1/inbox
Changes any of the inbox's settings and returns the inbox. Fields you leave out keep their values, also inside response.
curl -s -X PATCH https://webhooktool.com/api/v1/inbox \
-H 'Authorization: Bearer <secret>' \
-H 'Content-Type: application/json' \
-d '{"capture_limit": 100, "response": {"cors": true}}'DELETE /api/v1/inbox
Destroys the inbox, so it and its captures are deleted right away and its secret stops working. Answers 204 No Content.
GET /api/v1/inbox/captures
Lists captures, without their bodies.
| Parameter | Default | Meaning |
|---|---|---|
page | 1 | Page number, from 1. |
per_page | 50 | Captures per page, from 1 to 100. |
sort | newest | newest or oldest first. |
q | Only captures matching this search. |
{
"data": [
{"id": "01a10371-afd5-7ba1-8019-b929b9af644a", "received_at": "2026-10-03T20:31:02.517Z", "kind": "http", "method": "POST", "url": "https://whks.dev/6ba141bf-2c4f-4601-a998-b0b878661de7/orders?source=test", "status": 200}
],
"total": 1,
"page": 1,
"per_page": 50,
"sort": "newest"
}The captures in data have every field described under Capture except body. A search stops reading bodies after a set amount of data, and its result then has "partial": true.
DELETE /api/v1/inbox/captures
Deletes every capture, or with q only those matching a search. Answers {"deleted": 3, "partial": false}, where partial is true when the search stopped early, as when listing.
GET /api/v1/inbox/captures/{id}
Returns one capture, including its body.
DELETE /api/v1/inbox/captures/{id}
Deletes one capture. Answers 204 No Content.
GET /api/v1/inbox/captures/{id}/body
Downloads the raw body as application/octet-stream, or an email's raw message as message/rfc822 in an .eml file.
GET /api/v1/inbox/captures/{id}/files/{n}
Downloads the uploaded file whose index is n in the capture's files, or an email's part with that index, decoded.
GET /api/v1/inbox/captures/{id}/as/{format}
Returns the request as text that sends it again. format is curl, httpie, fetch (JavaScript), python (requests), powershell, or har for a HAR 1.2 document. The copy is addressed to the inbox's webhook URL with the same path and query, not to the host named in the original request, which the sender chose. Not available for DNS lookups or emails.
PUT /api/v1/inbox/captures/{id}/note
Sets the capture's note, up to 10000 characters, and returns the capture without its body. An empty note removes it.
curl -s -X PUT https://webhooktool.com/api/v1/inbox/captures/<capture-id>/note \
-H 'Authorization: Bearer <secret>' \
-H 'Content-Type: application/json' \
-d '{"note": "retried by the provider"}'POST /api/v1/inbox/captures/{id}/signature
Checks the signature of an Amazon SNS or PayPal capture with the signing certificate it names. The server downloads that certificate from Amazon's or PayPal's own servers, never from anywhere else, and keeps it for 12 hours. A PayPal signature also needs webhook_id, the ID PayPal gave the webhook. Signatures made with a shared secret, such as GitHub's or Stripe's, are checked in the dashboard instead, which keeps the secret in the browser. So are SendGrid's Signed Event Webhook (sendgrid, ECDSA P-256 over the timestamp and the body) and Discord's interactions (discord, Ed25519 over the timestamp and the body), with the public key SendGrid or Discord shows you. Discord's needs a browser that supports Ed25519 in WebCrypto.
Some senders sign without naming a certificate, and these are checked with keys their issuer publishes.
- Google Pub/Sub push requests (
google-oidc) carry a token from Google inAuthorization, checked with Google's keys fromwww.googleapis.com. Its issuer must beaccounts.google.com. - Microsoft Graph change notifications with resource data (
microsoft-graph) carry tokens invalidationTokens, and Azure Event Grid with Microsoft Entra authentication (azure-entra) carries one inAuthorization. Both are checked with Microsoft identity's keys fromlogin.microsoftonline.com, and their issuer must be Microsoft identity for a tenant. Each Graph token must also be issued to Microsoft Graph Change Tracking, the app0bf30f3b-4a52-48df-9a82-234910c4a086. - App Store Server Notifications V2 (
apple) carry the certificates that signedsignedPayload. They must lead to Apple Root CA - G3, which WebhookTool carries, and carry the marks Apple puts on its signing certificates, so nothing is downloaded.
Tokens and certificates must have been valid when the request arrived, allowing 5 minutes of clock difference for tokens. A token shows who sent the request, but doesn't cover its body. The result's token holds what the token says, such as its issuer, subject, audience, the service account's email and whether Google marked it email_verified, the app that requested it, its tenant, issued_at, expires_at, and the key_id and keys_url of the key that checked it. For Apple, certificate is the certificate that signed the payload, and token holds the app bundle ID and the environment.
To check a JWT that decoded lists instead, send its from as layer, as in -d '{"layer": "body.id_token"}'. Only tokens that Google (accounts.google.com), Microsoft identity or Sign in with Apple (https://appleid.apple.com) issued with RS256 can be checked, with keys downloaded only from those issuers' fixed addresses. Any other token gets 400 with not_checkable.
curl -s -X POST https://webhooktool.com/api/v1/inbox/captures/<capture-id>/signature \
-H 'Authorization: Bearer <secret>'{"scheme": "aws-sns", "valid": true, "certificate": {"url": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-<id>.pem", "subject": "CN=sns.amazonaws.com", "issuer": "CN=Amazon RSA 2048 M01,O=Amazon,C=US", "not_after": "2027-06-21T23:59:59Z"}}For PayPal, send the webhook ID as JSON, as in -H 'Content-Type: application/json' -d '{"webhook_id": "1JE4291016473214C"}'.
{"scheme": "google-oidc", "valid": true, "token": {"issuer": "https://accounts.google.com", "subject": "112233445566778899000", "audience": ["https://example.com/push"], "email": "pusher@my-project.iam.gserviceaccount.com", "email_verified": true, "key_id": "<key-id>", "keys_url": "https://www.googleapis.com/oauth2/v3/certs", "issued_at": "2026-10-12T10:00:00Z", "expires_at": "2026-10-12T11:00:00Z"}}valid is false with a reason when the signature fails. mismatch means it doesn't match the certificate or key, certificate_url that the certificate it names isn't on the provider's own servers, so the provider didn't send it, certificate_unknown that it is on the provider's own domain but not where WebhookTool expects certificates, so it wasn't checked, certificate_expired that the certificate had expired when the request arrived, certificate_chain that Apple's certificates don't lead to Apple's root or lack Apple's marks, and unsupported that it uses a version that can't be checked. For tokens, token_expired means the token had expired or wasn't valid yet when the request arrived, key_unknown that its issuer doesn't publish the key that signed it, which also happens once an issuer drops an old key, issuer that the sender doesn't issue tokens like it, and token_app that a Microsoft Graph token wasn't issued to Microsoft Graph Change Tracking. A capture without such a signature, or whose body was cut off, gets 400 with not_checkable.
GET /api/v1/inbox/export
Downloads captures with their bodies, oldest first, as a file. format is json (an array of captures, the default), csv (one row per capture, with lists and objects as JSON, and a leading ' on text starting with =, +, - or @ so spreadsheets do not run it as a formula) or har (HAR 1.2, without DNS lookups and emails). With q, only captures matching a search are included.
curl -s -OJ -H 'Authorization: Bearer <secret>' 'https://webhooktool.com/api/v1/inbox/export?format=har'Objects
Inbox
{
"id": "6ba141bf-2c4f-4601-a998-b0b878661de7",
"secret": "k7n2xq4vb6mz3wd5rf2hp7ty4c",
"urls": {
"ingest": "https://whks.dev/6ba141bf-2c4f-4601-a998-b0b878661de7",
"dns": "*.6ba141bf-2c4f-4601-a998-b0b878661de7.1.whks.dev",
"email": "*@6ba141bf-2c4f-4601-a998-b0b878661de7.email.whks.dev"
},
"created_at": "2026-10-03T20:25:36.123456Z",
"expires_at": "2026-10-04T00:25:36.123456Z",
"captures": 0,
"settings": {
"response": {
"status": 200,
"content_type": "text/plain; charset=utf-8",
"body": "",
"headers": [],
"delay_ms": 0,
"cors": false
},
"retention_seconds": 86400,
"capture_limit": 50,
"ip_mode": "full"
}
}| Field | Meaning |
|---|---|
id | The inbox ID, part of its webhook URL. |
secret | The bearer token. Only in the answer that creates the inbox. |
urls.ingest | The webhook URL. |
urls.dns | The names whose lookups are recorded. |
urls.email | The addresses whose emails are recorded. |
created_at | When the inbox was created, in UTC. |
expires_at | When the inbox and its captures will be deleted, in UTC. |
captures | How many captures the inbox holds. |
settings | The inbox's settings. |
restriction | Only while the operator restricts the inbox, which happens to inboxes that break the terms of use. http is true when new requests are refused with 403, dns when lookups are not recorded, email when new emails are refused, and response when requests get the default answer instead of the inbox's. notice says why, and expires_at when the restriction ends, if it does. |
Settings
| Field | Default | Allowed values |
|---|---|---|
response.status | 200 | 200 to 599. |
response.content_type | text/plain; charset=utf-8 | A header value of up to 256 bytes. Empty sends no Content-Type. Browsers that open the URL directly get text/plain instead. |
response.body | empty | Up to 16.0 KiB of text. A refresh <meta> tag is refused, since redirects are disabled to prevent abuse. |
response.headers | [] | Up to 32 extra headers as {"name": "X-Signature", "value": "abc123"}, each value up to 4096 bytes, and names and values together up to 8.0 KiB. Headers such as Content-Type, Content-Length, Content-Disposition, Set-Cookie, Content-Security-Policy, WWW-Authenticate and X-Robots-Tag cannot be set, and neither can CORS headers such as Access-Control-Allow-Origin, which cors sends. Location and Refresh are refused too, since redirects are disabled to prevent abuse. |
response.delay_ms | 0 | 0 to 30000. Waits this long before answering. |
response.cors | false | true lets web pages on other sites call the webhook URL. |
retention_seconds | 86400 | 300 to 259200, counted from the inbox's creation. Not less than its current age. |
capture_limit | 50 | 1 to 100. |
ip_mode | full | How much of each sender's IP address the inbox shows, either full, truncate (the /24 or /48 network) or none, which also leaves out the network the address lies in. The server keeps the whole address and its network either way, so a change applies to earlier captures too. |
Capture
{
"id": "01a10371-afd5-7ba1-8019-b929b9af644a",
"received_at": "2026-10-03T20:31:02.517Z",
"kind": "http",
"method": "POST",
"url": "https://whks.dev/6ba141bf-2c4f-4601-a998-b0b878661de7/orders?source=test",
"host": "whks.dev",
"path": "/orders",
"query": [{"name": "source", "value": "test"}],
"headers": [
{"name": "Accept", "value": "*/*"},
{"name": "Content-Length", "value": "18"},
{"name": "Content-Type", "value": "application/json"},
{"name": "Host", "value": "whks.dev"},
{"name": "User-Agent", "value": "curl/8.7.1"}
],
"proto": "HTTP/1.1",
"remote_ip": "203.0.113.7",
"content_type": "application/json",
"content_length": 18,
"body_size": 18,
"status": 200,
"body": "{\"hello\": \"world\"}",
"body_encoding": "utf-8"
}| Field | Meaning |
|---|---|
id | The capture ID, a UUIDv7. |
received_at | When the request arrived, in UTC. |
kind | http, dns or email. |
method, url, host, proto | As received. |
path | The path after the inbox ID, as sent. |
query, form | Query string and form fields as lists of name and value, in the order sent. |
headers | Request headers as a list of name and value, sorted by name. |
remote_ip | The sender's IP address, or the resolver's for a DNS lookup, or the sending mail server's for an email, as far as ip_mode shows it. Left out with none. |
tls | version, cipher_suite, server_name and alpn of an HTTPS connection. |
timing | How long the request took to arrive, in milliseconds to the microsecond. tls_handshake_ms is the TLS handshake of an HTTPS connection, given for its first request only, and reused_connection is true for the requests after it. headers_ms runs from the request's first byte to the end of its headers, over HTTP/1 only. body_ms is how long reading the body took. Behind a reverse proxy they time the proxy's connection. Left out when nothing is known. |
content_type, content_length | The request's Content-Type and Content-Length, with -1 when the length was not given. |
body_size | Bytes of body recorded. |
truncated | true when the body was longer than 1.0 MiB and cut off. |
form_truncated | true when only the first fields or files of a large form are listed. The full form is in the body. |
files | Uploaded files as index, field, filename, content_type, size and detected_type, the type the start of the file's content shows, when it shows one. |
status | The status the webhook URL answered with. |
note | Your note, if any. |
insights | What a request names about itself, worked out when it arrived. source is the sender, such as github or stripe, event the event type, delivery the ID that retries of one delivery share, attempt the attempt number when the sender counts them and occurred_at when the event happened. signature names the scheme, header, algorithm and encoding of the request's signature, the content it signs such as {timestamp}.{body}, and signed_at. handshake marks a request sent to verify the URL, and for DNS lookups decoded lists the parts of the name that decode to text, each with from, encoding and text. ip_list tells whether the sender's address is on a list of addresses a provider publishes, with that list and listed. For a sender that has one, it is that sender's list, such as github, stripe or atlassian for their webhooks, aws for Amazon SNS and Data Firehose, google for Google Pub/Sub, or azure for Azure Event Grid and Microsoft Graph. Otherwise it is a webhook list that holds the address. network names the cloud, hosting provider, service, crawler, uptime monitor, VPN or anonymizer whose published ranges, or for providers such as ovhcloud or leaseweb the blocks it registered at the internet registries, hold the address, with its list, such as aws, google-cloud, azure, github-actions, hetzner, digitalocean, google-crawlers, bingbot, applebot, openai, uptimerobot, pingdom, nordvpn, proton-vpn, mullvad, pia, ivpn, ovpn, google-vpn, cloudflare-warp, tor or icloud-private-relay. A VPN's list names the addresses its servers take connections on, which is where most VPNs send their users' traffic from, or the ranges it sends from. A list of the addresses a provider uses for one service, such as its crawlers, names an address before the provider's whole network. It has the region when the provider names one, either a region such as eu-north-1 or a place such as Falkenstein, DE. For DNS lookups it names the resolver's network, such as google-public-dns. It is left out when ip_mode is none, and so are the network's mark and matches for network: and has:network. Both need the server to have the lists. The sender chose everything else these come from, so a matching signature is what shows who sent it. |
insights.agent | The crawler, link preview, AI fetcher, uptime monitor, scanner or HTTP client the User-Agent names, with its id, such as googlebot, slack or curl, its kind, one of crawler, preview, ai, monitor, scanner or client, and the version of an HTTP client when it gives one. The dashboard shows an agent as verified when the sender's address is in the ranges its owner publishes for it, which insights.network names, such as google-crawlers, bingbot, applebot, openai, uptimerobot or pingdom. |
body, body_encoding | The body, with body_encoding utf-8, or base64 when it is not valid UTF-8. Only when getting one capture and in JSON exports. |
decoded | What the body, form fields, query and headers hold encoded, decoded, such as a gzip body, JSON inside strings, base64 data, JWTs and Basic credentials. Each has from, encoding, format (json, text, jwt or basic), text, and user and password for Basic credentials. JWT signatures are not checked here, but a JWT that Google, Microsoft or Apple issued can be checked on request. Only when getting one capture. |
dns | For DNS lookups, the name, type, class, transport (udp or tcp), recursion_desired, and edns with udp_size and dnssec_ok when the query used EDNS. |
email | For emails, the envelope's helo, mail_from and rcpt_to, the header fields from, to, cc, reply_to, subject, date and message_id with encoded words decoded, and text and html with the index, content_type and size of the parts holding the plain text and HTML. auth holds the SPF, DKIM and DMARC results, with their names from RFC 8601. auth.spf has the result (none, neutral, pass, fail, softfail, temperror or permerror), the domain checked, the identity it came from (mailfrom, or helo when the envelope sender is empty), the record term that matched, the record holding it and a reason. With an ip_mode other than full, matched names only the term's mechanism, such as ip4. auth.dkim lists one object per signature, with the result (pass, fail, neutral, policy, temperror or permerror), the signing domain (d=), selector (s=), algorithm, identity (i=), length when the signature covers only that many bytes of the body, the RSA key_bits, testing when the key is marked as in testing, and a reason. It is left out for an unsigned message. auth.dmarc has the result (none, pass, fail, temperror or permerror), the From address's domain, the policy_domain whose record applies, its policy (none, quarantine or reject), testing, spf_aligned and dkim_aligned when SPF or a DKIM signature passed for a domain aligned with the From domain, and a reason. When the checks did not run, auth holds only a reason. |
text, html | An email's plain text and HTML, decoded. Only when getting one capture. |
query, form, form_truncated, files, tls, remote_ip, truncated, note, insights, decoded, dns, email, text and html are left out when they are empty or do not apply.
Search
Use q to search, for example method:post status:4xx -path:/health.
curl -s -G -H 'Authorization: Bearer <secret>' https://webhooktool.com/api/v1/inbox/captures \
--data-urlencode 'q=method:post status:4xx -path:/health'Plain words match anywhere in a capture's method, URL, sender address, note, header, query and form names and values, file names, named sender and event, and body. All matching ignores case. Terms are combined with AND. Use OR, NOT or a leading -, parentheses, and double quotes around values with spaces. A search can be up to 1000 characters long with up to 32 terms. It stops reading bodies after a set amount of data and then reports "partial": true.
| Term | Matches |
|---|---|
method:post | The HTTP method. |
path:/orders url:example | Text in the path after the inbox ID, or in the full URL. |
ip:203.0.113 | Text in the sender's address. from: works too, and also matches an email's sender address. |
body:"order.created" | Text in the body. |
header:stripe header.user-agent:stripe | Text in any header, or in one header by name. query, query.name, form and form.name work the same way. |
file:invoice | An uploaded file name or field. |
note:refund | Text in your note. |
status:404 status:4xx status:>=500 | The status the webhook URL answered with. |
size:>10kb | Body size, with <, <=, >, >= and b, kb or mb. |
after:now-2h before:2026-10-01 | Arrival time, as a date, a date and time such as 2026-10-01T14:30 (UTC), or relative to now with s, m, h, d or w. |
source:stripe event:invoice.paid | The sender and event a request names, such as GitHub's push or Stripe's invoice.paid. |
delivery:evt_123 | The delivery ID that retries of one delivery share. |
network:aws network:falkenstein network:vpn | Requests from the published or registered addresses of a cloud, hosting provider, service, crawler, uptime monitor, VPN or anonymizer, by its name, such as aws, azure, hetzner, ovhcloud, github-actions, google-public-dns, google-crawlers, openai, uptimerobot, nordvpn or tor, or by the region or place. network:vpn finds every VPN. Inboxes whose ip_mode is none match none. |
agent:googlebot agent:preview has:agent has:client | The agent the User-Agent names, by its id, name or kind. has:agent finds crawlers, link previews, AI fetchers, monitors and scanners, and has:client HTTP clients such as curl or Python Requests. |
json.data.object.status:paid json.amount:>1000 | A field of a JSON body by its path, with numbers compared like size. A number in the path picks an array item, such as json.items.0.sku, and without one any item matches. |
has:body has:files has:header.x-signature | Captures that have a body, files, a form, a query, a note, a given header, query or form field, or a JSON field such as has:json.data.id. has:signature, has:retry, has:handshake, has:listed and has:network find signed requests, retries their sender marks, requests that verify the URL, requests from an address on a provider's published list and requests from a cloud's or network's published addresses. has:secret finds requests whose headers, query, form fields or body seem to hold a secret, such as a GitHub token, a live Stripe key, an AWS access key ID, a Slack token, a Google API key, a private key or a payment card number. |
method:dns type:txt name:hello | DNS lookups, by record type or by the name looked up. |
method:smtp subject:invoice from:alice@ to:hello@ | Emails, by text in the subject, the sender's address or IP, or a recipient in To, Cc or the envelope. body: and plain words also search an email's decoded text and HTML. |
spf:fail dkim:pass dmarc:reject | Emails, by the result of their SPF, DKIM or DMARC check. A result word (none, neutral, pass, fail, softfail, temperror, permerror, policy) matches exactly, so spf:fail leaves out softfail, and dkim:none finds unsigned emails. Any other value matches text in the domain checked, such as dkim:example.com. dmarc: also matches the From domain's policy: none, quarantine or reject. |
Errors
Errors from the API have a JSON body with a machine-readable code and a message.
{"error": {"code": "invalid", "message": "capture_limit: must be between 1 and 100"}}| Status | code | Meaning |
|---|---|---|
400 | invalid | A setting is not allowed, and message names the field. |
400 | invalid_json, invalid_body | The body is not JSON of the expected shape, has unknown fields, or could not be read. |
400 | invalid_parameter, invalid_query | A query parameter or the search is not valid. |
400 | not_checkable | The capture carries no signature that a provider's certificate or published keys check, or its body was cut off. |
401 | unauthorized | The secret is missing or wrong, or its inbox expired or was destroyed. |
403 | blocked | The operator blocked your network, and message says why. |
404 | not_found | No such capture, file, format or endpoint. |
410 | taken_down | The operator took the inbox down, and message says why. |
413 | too_large | The request body is larger than 1 MiB. |
429 | rate_limited | Too many requests. Retry after Retry-After seconds. |
429 | inbox_limit | You have the most inboxes allowed at a time, or created the most allowed recently. Retry-After counts the seconds until you can create one. |
503 | capacity | The server holds the most inboxes it allows. |
503 | busy | The server is busy. Retry after Retry-After seconds. |
500 | internal | Something went wrong on the server. |
502 | certificate_unavailable | The provider's certificate or keys could not be downloaded. |
Limits
| What | Limit |
|---|---|
| Inbox lifetime | 1 day, or up to 3 days |
| Captures per inbox | 50, or up to 100 |
| Data per inbox | 10.0 MiB |
| Recorded body size | 1.0 MiB |
| Response body size | 16.0 KiB |
| Extra response headers | 8.0 KiB of names and values |
| Redirects | Disabled to prevent abuse, so Location and Refresh headers and refresh <meta> tags are refused |