Documentation
Docs.
Everything SnapHook does, in one page. Generate an endpoint, send it traffic, read the captures in the browser or over the API.
01 — Quickstart
Three steps, no account
- Click Generate free webhook URL on the homepage. You are redirected to your private inspector at
/app/{slug}. - Copy the target URL from the top bar. It looks like
https://snaphook.site/w/{slug}. - Point any HTTP client or webhook provider at it. Captures appear in the inspector the instant they land — no refresh.
Nothing else is required. There is no account to create, no key to configure, and no SDK to install.
02 — URL anatomy
Two URLs per session
| URL | What it is |
|---|---|
/w/{slug} |
Your ingestion endpoint. Give this to the system you are debugging. Every method is accepted, and anything you append — /w/{slug}/orders/created — is captured as the request path. |
/app/{slug} |
Your inspector. Open it in a browser to watch captures live. Anyone with the link can read the session, so treat it as a secret. |
The slug is 12 characters drawn from a 32-symbol alphabet by a cryptographic random source — roughly 60 bits of entropy. It is the only thing protecting your session, which is why it is never logged, never indexed, and never shown to anyone you do not share it with.
03 — Code examples
Send a capture from anywhere
curl
# POST some JSON curl -X POST https://snaphook.site/w/YOUR_SESSION_ID/orders/created \ -H "Content-Type: application/json" \ -d '{"order_id": 4021, "status": "paid"}' # GET with a query string — both are captured verbatim curl "https://snaphook.site/w/YOUR_SESSION_ID/callback?state=xyz&code=123" # form encoded, custom headers, any method curl -X PUT https://snaphook.site/w/YOUR_SESSION_ID \ -H "X-Signature: t=1700000000,v1=deadbeef" \ --data-urlencode "event=subscription.updated"
JavaScript
// ingestion allows cross-origin requests, so this works from a browser too await fetch("https://snaphook.site/w/YOUR_SESSION_ID/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ hello: "world" }), });
Python
import urllib.request, json
body = json.dumps({"hello": "world"}).encode()
req = urllib.request.Request(
"https://snaphook.site/w/YOUR_SESSION_ID/test",
data=body,
headers={"Content-Type": "application/json"},
)
print(urllib.request.urlopen(req).read().decode())
Scripted end to end
# mint a session, send to it, then read the history back — no browser involved SLUG=$(curl -sX POST https://snaphook.site/api/new | sed 's/.*"id":"\([^"]*\)".*/\1/') curl -sX POST "https://snaphook.site/w/$SLUG/ping" -d 'hello' > /dev/null curl -s "https://snaphook.site/api/$SLUG/requests"
04 — The inspector
Reading a capture
The dashboard is a two-pane master-detail view. The left pane lists captures newest first with the method, path, relative time and payload size. The right pane breaks the selected capture into four parts:
- Metadata — method, full path with query string, protocol, client IP, timestamp and captured size.
- Headers — every header exactly as received, including duplicates.
- Query parameters — the parsed query string, with repeated keys preserved as lists.
- Body — pretty-printed when the payload is valid JSON, raw otherwise, with a toggle between the two.
The status pill in the top bar reports the live connection: live when the stream is attached, retrying while reconnecting, and expired once the session is gone. Clear discards the history but keeps the URL working.
05 — API reference
HTTP API
Every endpoint returns JSON and needs no authentication beyond knowing the slug.
| Method | Path | Behaviour |
|---|---|---|
POST |
/api/new |
Creates a session. Returns 201 with id, url, webhook_url and expires_in_seconds. Limited to 10 per minute per IP; returns 503 with Retry-After when the service is at capacity. |
ANY |
/w/{slug}/* |
Captures the request and returns 200 with {"ok":true,"id":N,"size":N,"truncated":false}. The capture's sequence number is also returned in the X-SnapHook-Request-Id header. Unknown or expired slug returns 404. |
GET |
/api/{slug}/requests |
Returns the retained history oldest first, with count, created_at and last_active. |
DELETE |
/api/{slug}/requests |
Discards the history immediately and returns 204. The session and its URL survive. |
GET |
/api/{slug}/sse |
Server-Sent Events stream of captures. See below. |
GET |
/healthz |
Liveness probe: {"ok":true,"sessions":N,"reaped":N}. Counters only, never session contents. |
Capture shape
{
"id": 3,
"timestamp": "2026-01-14T09:31:07.482Z",
"method": "POST",
"path": "/orders/created?source=stripe",
"proto": "HTTP/1.1",
"remote_ip": "203.0.113.7",
"headers": { "Content-Type": ["application/json"] },
"query_params": { "source": ["stripe"] },
"body": "{\"order_id\":4021}",
"size": 18,
"truncated": false
}
06 — Live stream
Server-Sent Events
Connecting to /api/{slug}/sse yields three event types:
init— sent immediately, carrying the whole retained history. Because the snapshot ships with the connection, there is no window in which a capture can be missed between fetching history and subscribing, and every reconnect re-syncs from scratch.request— one capture, in the shape above.expired— the session was purged; the stream then closes.
A : ping comment is sent every 25 seconds to keep intermediate proxies from dropping an idle connection.
Subscribing
const stream = new EventSource(`/api/${slug}/sse`);
stream.addEventListener("init", (e) => {
const { requests } = JSON.parse(e.data);
console.log(`replaying ${requests.length} captures`);
});
stream.addEventListener("request", (e) => {
const capture = JSON.parse(e.data);
console.log(capture.method, capture.path, capture.size);
});
From the command line, curl -N https://snaphook.site/api/YOUR_SESSION_ID/sse prints the raw frames.
07 — Limits
What the service will and will not hold
| Limit | Value | Behaviour at the edge |
|---|---|---|
| Captured body | 256 KB | Stored truncated and flagged; the request still succeeds |
| Retained captures | 30 per session | Oldest are dropped first |
| Retained bytes | 2 MB per session | Oldest are dropped first, even below 30 captures |
| Request headers | 32 KB total | Rejected by the server before capture |
| Session lifetime | 2 h idle | Purged from memory; the URL then returns 404 |
| Session creation | 10 / min / IP | 429 with Retry-After |
| Ingestion | 20 / s / IP | 429 with Retry-After |
These are memory-safety bounds, not upsell gates — SnapHook runs on a small VM and holds everything in RAM, so the ceilings are what keep it responsive for everyone.
08 — Provider setup
Wiring up a real sender
The pattern is identical everywhere: paste https://snaphook.site/w/{slug} where the provider asks for a destination URL, then trigger a test event.
- Stripe — Developers → Webhooks → Add endpoint, then Send test webhook. Append a sub-path such as
/stripeto keep senders distinguishable. - GitHub — Repository or organisation Settings → Webhooks → Add webhook. Set content type to
application/json; the Recent Deliveries tab and SnapHook should agree exactly. - Shopify, Twilio, Slack, Zapier, n8n — any field labelled callback, webhook or destination URL accepts a SnapHook endpoint as-is.
- Your own code — point a cron job, CI step or retry queue at it to confirm what it actually sends, headers included.
SnapHook always answers 200 quickly, so a provider's delivery log should show success. If it does not, the failure is between the provider and this service — check the sub-path and that the session has not expired.
09 — Security
Treat the session URL as a secret
SnapHook has no accounts, so the slug is the credential. Anyone who obtains it can read every capture in that session until it expires. That leads to a short list of rules:
- Use test-mode credentials only. Never send live API keys, bearer tokens, session cookies or signing secrets.
- Never send personal data. Real customer records do not belong in a public inspection endpoint, regardless of retention policy.
- Do not paste session URLs into public issues, gists or screenshots. Anything indexed is anything readable.
- Clear the history when you are done, or simply walk away — the session is purged after 2 hours of inactivity either way.
What the service does on its side
- Session surfaces send
X-Robots-Tag: noindex, nofollow, noarchiveand are disallowed inrobots.txt. - Captures live in process memory only. There is no database and no payload logging.
X-Content-Type-Options,X-Frame-Options: DENY, a strictContent-Security-PolicyandReferrer-Policy: strict-origin-when-cross-originare set on every response.- Per-IP token buckets cap session creation and ingestion so one sender cannot starve the rest.
See the privacy policy for the retention guarantee and the terms of use for what the endpoint may not be used for.
10 — Self-hosting
Run your own
SnapHook is a single static Go binary with no dependencies, no database and no configuration file. If you would rather not send even test payloads to someone else's host, run it yourself:
CGO_ENABLED=0 go build -o snaphook ./cmd/server
GOMEMLIMIT=1600MiB ./snaphook -addr 127.0.0.1:8080 -base-url https://hooks.example.com -trust-proxy
Put any TLS-terminating reverse proxy in front of it. Enable -trust-proxy only when that proxy overwrites X-Forwarded-For; otherwise clients can forge an address and evade the rate limiter.
11 — FAQ
Frequently asked.
How do I get a webhook testing URL?
Click Generate free webhook URL on the homepage, or POST /api/new. Both return a session slug; your ingestion endpoint is /w/{slug} and your inspector is /app/{slug}.
How long does a SnapHook session last?
A session is purged 2 hours after the last request it received and the last dashboard disconnecting from it. Keeping the inspector tab open keeps the session alive.
What are the payload limits?
Bodies are captured up to 256 KB and flagged as truncated beyond that. Each session retains its 30 most recent requests, and ingestion is limited to 20 requests per second per IP.
Is it safe to send real credentials to a webhook testing URL?
No. Anyone who knows a session URL can read what was sent to it. Use test-mode credentials and synthetic data, and never point a production payment, authentication or personal-data webhook at a public inspection endpoint.
Can I reply to a webhook with a custom status or body?
Not today. Ingestion always answers 200 with a small JSON acknowledgement, which is what provider delivery logs want to see. Configurable responses would make the endpoint useful as an open relay, which the terms prohibit.
Why did my session disappear?
Either it was idle for 2 hours, or the service restarted. Nothing is persisted across restarts by design — there is no disk state to restore.
Try it now.
A working endpoint is one click away.