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

  1. Click Generate free webhook URL on the homepage. You are redirected to your private inspector at /app/{slug}.
  2. Copy the target URL from the top bar. It looks like https://snaphook.site/w/{slug}.
  3. 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

URLWhat 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.

MethodPathBehaviour
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

LimitValueBehaviour at the edge
Captured body256 KBStored truncated and flagged; the request still succeeds
Retained captures30 per sessionOldest are dropped first
Retained bytes2 MB per sessionOldest are dropped first, even below 30 captures
Request headers32 KB totalRejected by the server before capture
Session lifetime2 h idlePurged from memory; the URL then returns 404
Session creation10 / min / IP429 with Retry-After
Ingestion20 / s / IP429 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 /stripe to 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, noarchive and are disallowed in robots.txt.
  • Captures live in process memory only. There is no database and no payload logging.
  • X-Content-Type-Options, X-Frame-Options: DENY, a strict Content-Security-Policy and Referrer-Policy: strict-origin-when-cross-origin are 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.