Documentation
Capture, inspect, forward and monitor webhooks. Put HookWatcher between your webhook provider and your application to see exactly what was sent, what your server answered, and resend anything that failed.
Getting Started
What HookWatcher does
HookWatcher gives you a URL that captures every HTTP request sent to it, exactly as it arrived. You can inspect headers and payloads, compare requests, replay them and generate documentation. With forwarding, HookWatcher also delivers each request to your own server and records the response, so it sits between your webhook provider and your application:
Webhook provider → HookWatcher (captures, inspects, retries, alerts) → Your application
You configure the provider once. You can then point forwarding at development, staging or production, see exactly what was sent and received, resend any request, and get an email when deliveries keep failing.
For anything that needs your local app's live answer, tunnels give a service on your machine a public HTTPS URL through hwcli.
Getting Started
Creating an endpoint
Open HookWatcher and you get an endpoint immediately; no account is needed to try it. Signed-in users create more from the sidebar (+) or with ⌘K → Create endpoint. Each endpoint URL looks like:
https://hookwatcher.com/<endpoint-id>
Anonymous endpoints keep history for 7 days. Sign up to keep endpoints, set retention and use forwarding.
Getting Started
Sending your first webhook
Paste the URL into your provider's webhook settings, press Send test in HookWatcher, or use cURL:
curl -X POST 'https://hookwatcher.com/<endpoint-id>' \
-H 'Content-Type: application/json' \
-d '{"event":"order.created","data":{"order_id":"ORD-1001","amount":1499}}'The request appears in Requests instantly. Test requests carry an X-HookWatcher-Test header and a "test" badge.
Getting Started
Inspecting a request
Select a request (or press j/k). The Overview shows its lifecycle (Received → Captured → Forwarded), forwarding result, key facts and actions. GraphQL requests also get a GraphQL tab (see GraphQL requests). Other tabs show the Body (a JSON tree — click a key to copy its path such as data.items[0].price), Headers, Query, Cookies, Raw bytes, the Response HookWatcher sent, Replay, Schema, Signature, Forwarding history and Compare.
Hooks
Endpoints (hooks), paths and query strings
An endpoint (also called a hook) is one capture URL with its own settings, history and forwarding. It accepts GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS, and any sub-path: /<endpoint-id>/orders/42?source=mobile is captured by the same endpoint, and the full path and query string are recorded with the request.
All endpoints lists every endpoint with its URL, last request, recent volume and forwarding state. Open goes to its requests; the ⋯ menu copies the URL, sends a test request, renames, configures or disables forwarding, clears captured requests or deletes the endpoint. The endpoint's Overview page has Overview, Forwarding, Security, Documentation and Settings tabs.
Request bodies up to 128 KiB are captured. HookWatcher replies immediately with the response configured in settings (default 200), including hw-request-id and hw-webhook-id headers.
Captured Requests
Captured requests
HookWatcher stores the exact bytes of every request with a SHA-256 fingerprint; formatting, masking and exports are derived views and never change what was captured. Header names are case-insensitive: Content-Type and content-type are the same header, and HookWatcher shows them as they arrived.
- Filter history by method, IP, provider, response status, signature, forwarding status, content type, protocol (GraphQL), GraphQL operation and date.
- Search event names and payload text, and sort newest or oldest first.
- Copy any request as cURL, Node.js or Python (sensitive headers masked by default), or export as JSON.
- Label, delete or bulk-export requests; history expires according to the endpoint's retention setting.
Forwarding
What forwarding does
Forwarding sends every request a hook receives to your own destinations after HookWatcher has captured it. One hook can forward to several forwarding endpoints at once — for example your production API, a staging API, an event archive and your laptop through hwcli. The sender gets HookWatcher's response straight away; delivery happens in the background, so a slow or failing destination never delays your provider or the other destinations.
Configure it
- Open the hook's Overview or Forwarding tab (or Endpoint settings) and choose Add forwarding endpoint.
- Pick the type — Remote URL or CLI endpoint — give it a name, the destination, the path behavior, and whether query parameters and credentials are forwarded.
- New requests are forwarded to every enabled endpoint. Use Test on an endpoint to send it a marked test event first.
- Disable pauses one endpoint without deleting it; requests are still captured. Remove deletes its configuration; past delivery records stay.
URL rules
A remote URL must be an https:// or http:// URL with a public hostname or address. Paths and query strings are kept, for example https://api.example.com/api/v1/orders/webhook?env=dev. Credentials in the URL, fragments and other schemes (file:, ftp:, data:, javascript:…) are rejected. See Security for network restrictions.
Forwarding
Multiple forwarding endpoints
Endpoint types
| Type | Delivered by | Destination |
|---|---|---|
| Remote URL | HookWatcher's servers | A public HTTP or HTTPS server (your API, staging, an archive). |
| CLI endpoint | hwcli on a computer you're signed in on | A server on that machine: localhost, 127.0.0.1 or [::1]. |
Plan limits
Your plan sets how many forwarding endpoints a hook can have, remote URLs and CLI endpoints combined:
| Plan | Forwarding endpoints per hook |
|---|---|
| Free | 2 |
| Developer | 3 |
| Pro | 5 |
| Team | 10 |
| Enterprise | Custom |
Disabled and offline endpoints still count — only removing an endpoint frees its place. There is no daily limit on forwarding: every request is delivered to every configured endpoint, however many you receive. Only technical protections apply (payload size, timeouts, abuse prevention and queue capacity).
Independent deliveries
Each endpoint gets its own delivery, retries, response and history. One endpoint failing never delays, retries or blocks the others. A request shows the result per endpoint — for example 2 of 3 delivered — and filters can find fully delivered, partially delivered and failed requests, or requests for one destination. Order doesn't matter: endpoints don't depend on each other.
CLI endpoints
Run hwcli listen <hook> --to http://localhost:3000/webhooks, or add a CLI endpoint here and choose one of your signed-in hwcli devices. HookWatcher queues the delivery and hwcli on that machine picks it up and delivers it locally. When the device is offline the endpoint stays configured and shows Offline; its deliveries fail as "CLI device offline" after the usual retries and are summarised in one line of the failure email, not one email per request.
Security
HookWatcher's servers never connect to localhost or private networks — remote URLs to those addresses are refused, including through DNS. CLI endpoints can target localhost only because the delivery is made by hwcli on your own machine, after it signed in to your account.
Signatures
Every endpoint receives the exact captured body, so provider signatures stay valid. Settings that change what is sent (for example leaving out credential headers) don't alter the body, but your server needs the signature header to verify it.
Forwarding
Wildcard paths
Choose Keep the incoming path and end the destination with /*. The * is replaced by whatever follows the endpoint ID in the incoming URL. Only a single trailing * is supported; it changes the path only, never the scheme, host or port.
| Incoming request | Destination | Forwarded to |
|---|---|---|
/abc123/orders/42 | https://example.com/webhooks/* | https://example.com/webhooks/orders/42 |
/abc123/customer/profile/update | https://dev.example.com/hooks/* | https://dev.example.com/hooks/customer/profile/update |
/abc123 | https://example.com/webhooks/* | https://example.com/webhooks/ |
/abc123/anything | https://example.com/hook (exact mode) | https://example.com/hook |
Repeated slashes are collapsed. Segments such as .., encoded slashes (%2F) or backslashes are refused and the delivery is marked failed, so an incoming path can never climb out of the configured base path.
Forwarding
Query, header and body behavior
Query parameters
With Forward query parameters on (the default for new destinations), incoming parameters are appended after the destination's own. When both define the same name, the destination's configured value wins and the incoming value is dropped. Repeated incoming names keep their order.
Destination: https://api.example.com/payment?env=dev Incoming: ?source=mobile&env=prod Forwarded: https://api.example.com/payment?env=dev&source=mobile
Body
The original method and the exact body bytes are sent unchanged, with the original Content-Type. HookWatcher never re-serializes JSON, so provider signatures computed over the raw body still verify on your server.
Headers
| Header | Forwarded? |
|---|---|
| Content-Type, Content-Encoding, Accept and other content headers | Yes |
| Provider signatures and events (Stripe-Signature, X-Hub-Signature-256, X-GitHub-Event, X-Shopify-*, webhook-id…) | Yes, unchanged |
| Custom X-* headers and User-Agent | Yes |
| Authorization, Cookie, X-Api-Key and other credentials | Only when Include credentials is on |
| Host, Content-Length, Connection, Keep-Alive, Transfer-Encoding, TE, Trailer, Upgrade, Proxy-* | No — the HTTP client sets its own |
| X-Forwarded-*, Forwarded, X-Real-IP, CF-* | No — they would misstate the source |
| Incoming X-HookWatcher-* | No — replaced by HookWatcher's own |
Headers HookWatcher adds
X-HookWatcher-Forwarded: true X-HookWatcher-Request-Id: <captured request ID> X-HookWatcher-Hook-Id: <endpoint ID> X-HookWatcher-Received-At: <ISO 8601 capture time> X-HookWatcher-Forward-Attempt: <attempt number>
No account or user information is added. Test deliveries also carry X-HookWatcher-Test: true.
Forwarding
How responses are evaluated
A delivery succeeds when your server returns any 2xx status. Everything else is a failure: 3xx redirects (HookWatcher does not follow redirects; update the destination to the final URL), 4xx and 5xx responses, timeouts, DNS failures, refused connections and TLS errors. For each attempt HookWatcher stores the status, response time, response headers (credentials masked) and the first 8 KiB of the response body, marked truncated when longer. Response bodies are shown as text and never rendered as HTML.
Timeouts depend on the endpoint owner's plan (5–30 seconds); connecting and DNS lookup must each finish within 3 seconds.
Automatic Retry
Automatic retry
Automatic delivery makes 3 attempts in total:
| Attempt | When |
|---|---|
| 1 | Immediately after the request is captured |
| 2 | About 10 seconds after attempt 1 fails |
| 3 | About 45 seconds after attempt 2 fails |
If any attempt succeeds, no further attempts are made. After attempt 3 fails, the request is marked Failed and becomes eligible for the hourly failure email. Retries are per endpoint: a failing endpoint retries on its own while the others are done. Destinations blocked by the security policy, a removed endpoint or an invalid incoming path fail immediately without retries. Retries are scheduled in the background and survive restarts.
Delivery is at-least-once: if your server processed a request but the response was lost (for example a timeout), a retry can deliver it again. Use X-HookWatcher-Request-Id to deduplicate.
Manual Retry
Send again
Send again appears next to each forwarding endpoint of a request (and Forward now for endpoints added after the request arrived). It makes exactly one new delivery attempt to that endpoint only using its current destination, records it as a Manual attempt in that endpoint's history, and shows the result immediately. Other endpoints are not sent the request again, and earlier attempts are never overwritten.
- Manual attempts are not retried automatically.
- Manual failures never trigger failure emails.
- Sending again replaces any automatic retry that was still waiting, so the destination does not receive the request twice.
- Each request allows up to 10 attempts per endpoint in total. Forwarding volume itself is not limited.
Failure Alerts
Failure alerts
When automatic delivery fails after all 3 attempts, HookWatcher emails the endpoint owner. To avoid spam you receive at most one alert email per hour: failures that happen after an email are collected and included in the next one.
Each email summarizes the failures since the previous email, grouped by endpoint: endpoint name, destination host and path, number of failures, and for each request the event type or method, the failure reason or status code, the number of attempts and the time, with a link to view them. Large digests show the first 20 and a count of the rest. Emails never include payloads, headers, query strings or secrets.
A failure that is later delivered by Send again before the next email is dropped from it. Turn alerts on or off under My Account → Notifications (on by default).
Forwarding Status
Forwarding status
| Status | Meaning |
|---|---|
| Not configured | The endpoint has no forwarding destination. |
| Not forwarded | Forwarding exists, but this request arrived before it was enabled. Use Forward now. |
| Disabled | Forwarding is paused for this endpoint; the request was captured but not delivered. |
| Queued | Waiting to be sent. |
| Forwarding | Being delivered now. |
| Retrying | An attempt failed and another automatic attempt (up to 3) is scheduled or running. |
| Sending | A manual Send again is in progress. |
| Delivered | The latest attempt got a 2xx response. |
| Failed | The latest attempt failed and no automatic attempt remains. |
Security
Security
Forwarding, replay and destination tests share one outbound policy:
- Only http and https. URLs with credentials or fragments are rejected.
- Localhost (
localhost,*.localhost,*.local,127.0.0.0/8,::1,0.0.0.0) cannot be used. - Private, shared, link-local, multicast and reserved ranges are blocked, including
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10,169.254.0.0/16(including the cloud metadata address169.254.169.254),fc00::/7,fe80::/10and IPv4-mapped IPv6 forms. - Hostnames are resolved before every delivery and every returned address must be public; the connection is pinned to the checked address, which prevents DNS rebinding.
- Redirects are never followed, so a public URL cannot redirect a delivery into a private network.
- Response size, header size and time are bounded.
To deliver to your laptop, add a CLI endpoint (hwcli listen delivers from your machine) or forward to a HookWatcher tunnel URL. Sensitive request headers are masked in the interface, exports and AI requests unless you explicitly reveal or include them. Signing secrets are encrypted at rest and never shown after saving.
Security
Signatures and MFA
Add a Stripe or GitHub signing secret in Endpoint settings to verify signatures against the captured bytes; results appear on each request. Verification describes a request and never blocks it. Because forwarding keeps the exact body and signature headers, your own server can verify the same signature.
Enable two-factor authentication under My Account → Security with an authenticator app. MFA is reset when you reset your password.
Troubleshooting
Troubleshooting deliveries
| What you see | What to check |
|---|---|
| 400 Bad Request / 422 | Your handler rejected the payload. Compare the forwarded body in the request with what your parser expects; parse the raw body before JSON-decoding when verifying signatures. |
| 401 / 403 | Your server requires authentication. Turn on Include credentials to forward the provider's Authorization header, or allow HookWatcher's requests (identified by X-HookWatcher-Forwarded). |
| 404 Not Found | The path is wrong. Check the destination URL and, in wildcard mode, the mapping shown in the forwarding dialog. |
| 3xx redirect | Redirects are not followed. Use the final URL (often https:// or a trailing slash). |
| 429 Too Many Requests | Your server is rate limiting. Automatic retries back off; raise the limit for HookWatcher or Send again later. |
| 500 / 502 / 503 / 504 | Your server or its proxy failed. Check its logs using X-HookWatcher-Request-Id, then Send again. |
| Timeout | Respond quickly (queue slow work) or ask for a longer plan timeout. |
| DNS error | The hostname does not resolve. Check for typos and that the record is public. |
| TLS error | The certificate is invalid, expired or for another name. Fix the certificate or use a hostname that matches it. |
| Blocked destination | The URL or one of its DNS answers is private or local. Use a public address, a HookWatcher tunnel URL (hwcli tunnel http), or a CLI endpoint (hwcli listen). |
Captured Requests
GraphQL requests
HookWatcher recognises GraphQL over HTTP and shows it as GraphQL — operation type, operation name, formatted document and variables — instead of escaped JSON. Detection never changes what was captured: the raw bytes are stored once, and the GraphQL view is derived when you open the request.
Supported request formats
| Format | Example |
|---|---|
| JSON body (most common) | {"query": "…", "variables": {…}, "operationName": "CreateOrder"} |
application/graphql | The body is the GraphQL document; operationName and variables may be query parameters. |
| GET | /graphql?query=…&variables=…&operationName=… |
| Batch | A JSON array of request objects; each operation is listed separately. |
| Persisted query | extensions.persistedQuery.sha256Hash with or without the query text. |
A JSON body counts as GraphQL only with reasonable confidence: its query must parse as a GraphQL document, or the object must carry GraphQL request fields (variables, operationName, extensions) and nothing else. {"query": "red shoes", "page": 2} stays an ordinary JSON request.
In the request inspector
The list shows the operation — for example GraphQL · mutation and CreateOrder — and the Overview adds Protocol and Operation. The GraphQL tab shows:
- Operation type and name. An unnamed operation shows as Anonymous operation; a document with several operations and no operationName shows Multiple operations detected — HookWatcher doesn't guess which one ran. When operationName selects one, it is marked.
- The formatted document, with syntax highlighting, and its fragments listed once.
- Declared variables with the types written in the operation, and the variables in the JSON viewer (search, copy value, copy path). Variables with sensitive names (password, token, secret…) are masked until you choose to include sensitive values.
- Extensions (persisted query hash, tracing, client data) as JSON. A persisted query without text shows its hash and says the text isn't known.
- Copy operation, Copy variables, Copy operation name and Copy as cURL. The cURL command writes the GraphQL body literally (--data-raw), byte for byte.
If the document doesn't parse, the tab says Unable to parse GraphQL operation, shows the parser's message and the query exactly as sent.
Example
mutation CreateOrder($input: OrderInput!) {
createOrder(input: $input) {
id
status
}
}{
"input": {
"amount": 1499,
"currency": "USD"
}
}This request appears as POST · GraphQL · mutation · CreateOrder; its GraphQL tab lists $input: OrderInput! under declared variables and the amount and currency under variables.
Search and filters
Use the Protocol, Operation type and Operation name filters, or type protocol:graphql, graphql.type:mutation or graphql.operation:CreateOrder in the search box (combine them with other words). Requests captured before GraphQL support are recognised when you open them.
Forwarding
GraphQL requests are forwarded like any other request: every forwarding endpoint receives the same method, raw body, Content-Type, headers and query parameters — never a reserialized body, so custom signatures still verify. CLI endpoints (hwcli forward) deliver them to localhost the same way.
GraphQL errors versus delivery
GraphQL servers often answer HTTP 200 with an errors array. HookWatcher shows both: the delivery result (for example 200 OK) and, separately, GraphQL errors: 2 with each error's message, path and location. Retries follow the HTTP result only — a GraphQL error never triggers an automatic retry. The endpoint analytics count forwarding responses with GraphQL errors.
Replay
Edit & Replay on a GraphQL request has separate GraphQL query, Variables (JSON) and Operation name fields, plus Format GraphQL (shows the parse error if the query is invalid). Unedited fields replay the exact captured bytes; edits rebuild the request in its own format — the JSON body with its other fields kept, the application/graphql body, or GET parameters. Use original raw body undoes the edits. The replay response shows GraphQL data, errors and extensions.
Compare and operation changes
Comparing two GraphQL requests shows the operation type and name, a line diff of the formatted documents and a structural diff of the variables. When a named operation's document changes between captures, the GraphQL tab says the operation changed and can show the diff. This is a GraphQL operation change, not schema drift: HookWatcher has no GraphQL schema.
What HookWatcher does not do
- It doesn't validate operations against a schema, execute them or run introspection against your destinations.
- Subscriptions captured as HTTP requests are shown as subscriptions; GraphQL subscriptions over WebSocket are not proxied.
- AI documentation uses the operation, declared variable types and redacted variables — it doesn't claim field meanings a schema would provide.
FAQ
GraphQL FAQ
| Question | Answer |
|---|---|
| Does HookWatcher support GraphQL? | Yes, GraphQL over HTTP: JSON bodies, application/graphql bodies, GET, batches and persisted queries. |
| Can I inspect GraphQL variables? | Yes, in the GraphQL tab's JSON viewer. Variables with sensitive names are masked until you include sensitive values. |
| Can I replay GraphQL mutations? | Yes. Replay sends the exact original bytes, or edit the query, variables and operation name first. |
| Are GraphQL errors detected? | Yes, in forwarding and replay responses — shown separately from the HTTP status, without changing retries. |
| Can GraphQL requests be forwarded to multiple endpoints? | Yes. Every forwarding endpoint receives the unchanged request. |
| Does the CLI support GraphQL requests? | Yes. hwcli requests get shows the operation, formatted query and variables; --raw prints the exact body; hwcli listen delivers GraphQL requests to localhost unchanged. |
FAQ
Forwarding FAQ
| Question | Answer |
|---|---|
| Does HookWatcher alter the payload? | No. The captured bytes are forwarded unchanged with their Content-Type. |
| Does it retry automatically? How many times? | Yes — 3 attempts in total, about 10 and 45 seconds apart. |
| Can I send a request again manually? | Yes, with Send again on the request. It makes one attempt and is recorded separately. |
| Does Send again trigger a failure email? | No. Only final automatic failures are emailed, at most once per hour. |
| Can I forward to localhost or a private IP? | Not as a remote URL. Add a CLI endpoint with hwcli listen, or forward to the URL of a HookWatcher tunnel (hwcli tunnel http). |
| Can I preserve request paths? | Yes — choose Keep the incoming path and end the destination with /*. |
| Are the original headers forwarded? | Content, signature, provider and custom headers are. Connection and proxy headers are not, and credentials only when enabled. |
| Will webhook signatures still verify? | Yes, as long as your server verifies the raw body; HookWatcher does not change the bytes or signature headers. |
Debugging
Replay, debug and test
Every captured event can be replayed exactly, modified, run against your local code and saved as a regression test. The original event never changes: replays, debug attempts and test runs are separate records linked to it.
- Replay Locally sends the event to an hwcli session that is listening on your machine. Replay to URL sends it from HookWatcher to a public URL. Modify & Replay changes fields and headers first, with a side-by-side view.
- Replay Lab sends duplicate, concurrent, delayed, burst, unsigned or malformed deliveries, or events in any order, marked
X-HookWatcher-Synthetic: replay-lab. - Debug Sessions record each attempt while you fix the handler. The successful attempt can be saved as a test.
- Regression tests store the exact request and the expected response. Run them from the Tests view or
hwcli test runin CI. - Failures lists failed deliveries, replays and tests across the workspace. Diff compares a failed event with the most similar successful one, hiding volatile fields.
- Share creates an expiring, read-only link with secrets redacted.
Viewers can inspect and compare. Developers can also replay, debug, and create and run tests. Admins also manage everyone's tests and Workspace settings → Debugging (production replay, ignored diff fields, redaction rules). Replays to hosts that look like production always ask for confirmation. Step-by-step instructions are in the guides.
CLI
Install hwcli
hwcli is the HookWatcher command line: watch requests as they arrive, deliver them to a server on your machine, give a local service a public HTTPS URL with a tunnel, replay and debug requests, and turn them into regression tests for CI. It is one small executable for macOS, Linux and Windows (x64 and ARM64) with nothing else to install.
| Platform | Install | Update |
|---|---|---|
| macOS (Apple silicon, Intel) | brew install hookwatcher/tap/hwcli | brew upgrade hwcli |
| Linux (x86-64, ARM64) | Homebrew, as on macOS, or the download | same way |
| Windows 10/11 (x64, ARM64) | scoop install hookwatcher/hwcli | scoop update hwcli |
macOS and Linux (Homebrew)
brew tap hookwatcher/tap https://gitlab.com/hookwatcher/hwcli-hb brew install hookwatcher/tap/hwcli hwcli version
Windows (Scoop)
scoop bucket add hookwatcher https://gitlab.com/hookwatcher/hwcli-wn scoop install hookwatcher/hwcli hwcli version
Direct download
The Developer page lists the downloads for your platform with copyable install commands. Each /downloads/hwcli/latest/<platform> link always serves the newest version:
curl -fL -o hwcli.tar.gz https://hookwatcher.com/downloads/hwcli/latest/darwin-arm64 tar -xzf hwcli.tar.gz mkdir -p ~/.local/bin && install -m 0755 hwcli ~/.local/bin/hwcli hwcli version
Replace darwin-arm64 with darwin-amd64, linux-amd64 or linux-arm64; on Windows use windows-amd64 or windows-arm64 (a zip). Linux builds are statically linked and run on any distribution, including Alpine. On Windows, unpack the zip and add its folder to your PATH; hwcli isn't code-signed, so SmartScreen may ask the first time.
On macOS, a copy downloaded with a browser is quarantined, and macOS says it "cannot be opened" because hwcli isn't notarized. Install with Homebrew, or download with curl as shown on the Developer page. Don't turn off Gatekeeper.
hwcli checks at most once a day whether a newer version exists and prints a notice; it never updates itself (hwcli config set update-check false turns the check off). hwcli version --check asks on demand.
CLI
Get started with hwcli
hwcli login # approve the code in your browser hwcli workspace use # pick a workspace (if you have several) hwcli hooks create --name payments --copy # new endpoint, URL on the clipboard hwcli listen payments --to http://localhost:3000/webhooks # deliver new webhooks to your app hwcli requests list # what arrived
hwcli login shows a one-time code and opens your browser; approve the matching code while signed in to HookWatcher (--no-browser prints the link for SSH sessions and containers). The session lasts 90 days and can be revoked on the Developer page, with hwcli sessions revoke or with hwcli logout. The token is kept in the macOS Keychain, the Linux Secret Service when available, or a file only you can read. hwcli whoami shows the account, plan, workspace and where the token is stored.
Run hwcli with no arguments for an interactive menu, and add --help to any command. An event can be given in full, as evt_ plus the first 8 characters (as hwcli prints it) or as any unique prefix of 8 or more characters. A hook is an endpoint ID or its name in the selected workspace; -w picks another workspace for one command. Your workspace role applies exactly as in the web app: Viewers can read, Developers can also replay, debug and run tests, and Admins and the Owner can change endpoints and forwarding.
CLI
hwcli commands
| Commands | What they do |
|---|---|
login · logout · whoami · sessions | Sign in and out, show the account, list or revoke CLI sessions |
workspace list · use · current | Choose the workspace other commands use |
hooks list · get · create · copy-url · delete | Endpoints; create --name N --copy, --template stripe |
requests list · watch · tail | Search history, stream new requests; filters --hook --method --event --provider --status --forwarding --graphql --since --until |
requests get | Metadata, forwarding results, headers (masked unless --reveal), body; --raw prints the exact bytes |
requests replay · resend | Send from HookWatcher to a public URL (--to), or again to one forwarding endpoint (--endpoint) |
listen | Deliver a hook's webhooks to a server on this machine |
forwarding list · add · edit · enable · disable · test · remove | Forwarding endpoints: --url for public URLs, --cli --to for this device |
replay · debug · diff | Replay exactly or modified, Replay Lab, debug sessions, compare two events |
test create · apply · list · show · run · delete | Regression tests from real events, as YAML in your repository, run locally or in CI |
monitor | Forwarding health per endpoint over 24 hours (--failing, --watch) |
config · version · completion · help | Settings, version check, shell completion for bash, zsh, fish and PowerShell |
events, endpoints and fwd are aliases of requests, hooks and forwarding. Every option is listed by hwcli <command> --help.
CLI
Deliver webhooks to your machine
hwcli listen <hook> --to http://localhost:3000/webhooks (:3000/webhooks works as shorthand) creates or reuses a CLI endpoint for this device and delivers each new request, byte for byte, to a server on this machine. Only localhost, 127.0.0.1 and [::1] are accepted, and HookWatcher never connects to your machine: hwcli asks for deliveries. hwcli listen without a hook serves every CLI endpoint bound to this device.
| Option | Effect |
|---|---|
--to URL | Local URL (also --forward, -t, -f) |
--name NAME | Endpoint name (default Local (<this device>)) |
--append-path | Append the path called after the hook ID (…/<id>/orders → <URL>/orders) |
--include-sensitive | Also deliver signature and credential headers, needed when your server verifies signatures |
A CLI endpoint is a forwarding endpoint like any other: it counts toward the hook's plan limit, keeps its place while hwcli isn't running (shown as Offline) and is removed with hwcli forwarding remove. Failed local deliveries are retried like any endpoint's. hwcli reconnects on its own after network interruptions and marks the device offline when you press Ctrl+C. Dashboard Replay Locally requests for this device are delivered by the same command.
CLI
Replay, debug and test from the terminal
hwcli replay <event> sends a capture again exactly (method, path, query, headers and raw body) from this machine to your listen target or --target :8080/webhooks, or from HookWatcher to a public URL with --to. Several events are replayed in the order given. The capture itself never changes.
| Options | Use |
|---|---|
--set a.b=5000 · --remove a.c · --header N:V · --remove-header N · --query k=v | Change the JSON body, headers or query |
-X PUT · --path /x · --content-type T · --body-file F · --edit | Change the method, path, type or whole body, or edit in $EDITOR |
--duplicate N · --concurrent N · --burst N --duration 5s · --delay 5s | Replay Lab: idempotency, races, load, timing (marked X-HookWatcher-Synthetic) |
--resign · --invalid-signature · --remove-signature · --malformed invalid_json | Signature and broken-body tests |
hwcli debug <event> --forward :8080/webhook opens a Debug Session: Enter replays after you change your code, e edits the request, s saves the passing attempt as a test, q quits. hwcli diff <failed> <successful> compares two events and hides volatile fields unless --all.
hwcli test create payment-ok --from evt_9da96b80 --expect-status 200 \ --assert 'order.status equals "paid"' --max-latency 500 hwcli test show payment-ok -o yaml > tests/payment-ok.yaml # tests as code hwcli test apply tests/*.yaml hwcli test run --target http://localhost:3000 [--ci --junit report.xml]
CLI
hwcli in scripts and CI
In CI, set HWCLI_TOKEN to an API key instead of signing in (tests need tests:read and tests:run), and use -o json or -o yaml for machine-readable output on stdout; messages go to stderr and --quiet prints only IDs or URLs. CLI calls count toward your plan's API requests per minute; listen deliveries don't count.
| Variable | Effect |
|---|---|
HWCLI_TOKEN | API key or session token; overrides the saved sign-in and is never written to disk |
HWCLI_SERVER | Server for every command |
HWCLI_WORKSPACE | Workspace ID for every command |
HWCLI_TEST_TARGET | Default --target for test run |
HWCLI_CA_BUNDLE | Extra CA certificates (PEM) for TLS-inspecting proxies; TLS is always verified |
HTTPS_PROXY · NO_PROXY · NO_COLOR · HWCLI_NO_UPDATE_CHECK | Proxy, no colors, no update check |
| Exit code | Meaning |
|---|---|
| 0 | Success |
| 1 | A replay, debug attempt or test didn't succeed, or diff found differences |
| 2 · 3 · 4 · 5 | Usage error · not signed in · not permitted · not found |
| 6 · 7 · 8 · 9 | Rejected by the server · API limit reached · network or server error · version no longer supported |
| 130 | Interrupted (Ctrl+C) |
hwcli test run uses its own codes: 0 all passed, 1 a test failed, 2 configuration error, 3 authentication error, 4 your app or HookWatcher unreachable.
CLI
hwcli troubleshooting
| Symptom | What to do |
|---|---|
| "You are not signed in" (exit 3) | hwcli login, or check HWCLI_TOKEN and hwcli config get server |
| macOS: "cannot be opened" | It was downloaded with a browser. Install with Homebrew, or download with curl |
| Windows SmartScreen warning | hwcli isn't code-signed yet. Install with Scoop to avoid the prompt |
| listen: "connection refused" | Your local server isn't running on that port |
| listen: endpoint limit reached | The hook already has your plan's number of endpoints: hwcli forwarding list <hook> |
| listen: signatures fail locally | Add --include-sensitive |
| "Secure connection … failed" | Behind a TLS-inspecting proxy, set HWCLI_CA_BUNDLE to its CA certificate |
| "API request limit reached" (exit 7) | Wait a minute; watch and listen resume on their own |
| "no longer supported" (exit 9) | Install the current version |
Tunnels
How tunnels work
A tunnel gives a service on your machine a public HTTPS hostname, https://<name>.hookwatcher.com. Requests to it go live to your local app and the app's response goes back to the caller. Unlike hwcli listen, which delivers webhooks HookWatcher already captured, a tunnel returns your app's real answer, which OAuth callbacks, URL verification challenges and demos need.
Caller → HookWatcher tunnel service ⇄ hwcli (your machine) → http://localhost:3000
↑ encrypted connection opened by hwcli, outbound- hwcli opens the connection from your machine. Nothing on your machine accepts connections from the internet, and no router or firewall change is needed.
- Each connection is authorized by HookWatcher with a one-time ticket tied to your hwcli sign-in. Only your hwcli can serve your hostname.
- Request history is kept only on your computer, in hwcli's local panel. HookWatcher keeps connection details and monthly totals.
Tunnels
Start, stop and manage tunnels
Install hwcli (CLI install) and sign in with hwcli login. Then:
| Command | Does |
|---|---|
hwcli tunnel http 3000 | Serve this device's tunnel (reserved on first use, so the URL stays the same) and show requests live. Ctrl+C stops it. |
hwcli tunnel http 3000 --name payment-test | The same with a name you choose: https://payment-test.hookwatcher.com (plans with custom names) |
hwcli tunnel create --name payment-test | Reserve a hostname without starting it |
hwcli tunnel start payment-test --port 3000 | Serve an existing tunnel from this machine |
hwcli tunnel list | Your tunnels, their state and plan usage |
hwcli tunnel status [name] | Connection, agent, gateway and request count |
hwcli tunnel stop <name> | Disconnect a running session, from any machine. The hostname stays reserved |
hwcli tunnel delete <name> | Delete the tunnel and release its hostname |
Useful options: --secret (callers must send the access secret as X-Tunnel-Secret or a Basic auth password), --preserve-host (send the public hostname as Host), --max-concurrency (parallel requests to your app; more wait their turn) and --capture (what the local history keeps). The dashboard's Tunnels view creates, renames, disables and deletes tunnels too, and shows whether each one is Connected.
Tunnels
Requests, limits and reconnection
- Forwarded as sent: method, path, query, headers and body. Bodies stream both ways. Your app also gets
X-Forwarded-For,X-Forwarded-Proto: httpsandX-Forwarded-Host; headers added by the network edge are removed first. - Cookies stay on the tunnel's hostname: the
Domainattribute is removed fromSet-Cookie. - Timing: your app must start answering within 60 seconds and a request may last 5 minutes (
503"Timed out" otherwise). - Plan limits: connected tunnels, requests in flight per tunnel and per account (
429), request body size (413) and the monthly request and traffic allowance (429once used up). Values per plan are on the pricing page. - Not supported: WebSocket and other protocol upgrades (
501). - Connection loss: hwcli detects a dead connection within about a minute and reconnects with a new session, at once after a HookWatcher restart and with backoff otherwise. Requests in flight at that moment fail for their callers and are not re-sent.
- One hwcli per tunnel: starting the same tunnel elsewhere takes it over; the earlier hwcli stops and says so.
Tunnels
Tunnel usage and monthly totals
| Counter | What it counts |
|---|---|
| Tunnels | Hostnames reserved on your account (connected or not) |
| Connected | Tunnels whose hwcli is connected and was seen by the tunnel service in the last minute |
| Requests this month | Requests handed to hwcli, including ones your app answered with an error or never answered. Requests refused before that (tunnel offline, wrong access secret, a limit reached) don't count. A caller's retry is a new request. |
| Traffic this month | Request and response body bytes through the tunnel service. Headers aren't counted. An interrupted transfer counts the bytes that were sent. |
Months are calendar months in UTC. Totals are reported by the tunnel service every 15 seconds and kept in the database, so they survive restarts. Tunnels belong to your account, not to a workspace: they count against your plan, whichever workspace you are viewing.
Tunnels
Tunnel troubleshooting
| Symptom | What to do |
|---|---|
| 404 "No tunnel here" | hwcli isn't running for that hostname. Run hwcli tunnel http <port> or hwcli tunnel start <name> --port <port> |
| 503 "Local service unreachable" | hwcli is connected, but nothing answered on the local port. Start your app, or check the port. hwcli warns about this when it starts |
| 503 "Timed out" | Your app didn't start answering within 60 seconds |
| 401 on every request | The tunnel requires its access secret. Send X-Tunnel-Secret, or turn the secret off in the dashboard |
| 429 | A plan limit was reached: requests in flight, or this month's allowance |
| "Another hwcli session took over" | The tunnel was started on another machine or terminal. Stop that one first |
| "This tunnel is disabled" | Enable it in the dashboard (a tunnel disabled by HookWatcher needs support) |
| "…connected tunnels at a time" | Your plan's limit of tunnels connected at once is reached. hwcli tunnel list shows which are connected |
| Stuck on "Reconnecting" | The connection to HookWatcher fails. Check your network; behind a TLS-inspecting proxy set HWCLI_CA_BUNDLE |
| "Tunnels are not set up on this server" | Tunnels aren't available on this HookWatcher installation |
API
API
The workspace uses these session-authenticated endpoints (owner or manage permission for changes; same-origin requests only):
GET /api/webhooks/:id/forward destination, 7-day stats, limits
POST /api/webhooks/:id/forward { forwardUrl, enabled, includeSensitive, pathMode: "exact"|"wildcard", forwardQuery }
DELETE /api/webhooks/:id/forward disable (keeps configuration); ?remove=true deletes it
POST /api/webhooks/:id/forward/test send a marked test event
GET /api/webhooks/:id/forward/attempts ?page=1&requestId=… (20 per page)
POST /api/webhooks/:id/forward/attempts/:attemptId/retry manual Send again for a failed attempt
GET /api/webhooks/:id/requests/:requestId/forwarding { status, destination?, attempts? }
POST /api/webhooks/:id/requests/:requestId/forwarding manual Send again / Forward now
GET /api/webhooks/:id/history?forwarding=failed filter by forwarding status
GET|PUT /api/profile/notifications { forwardingFailureAlerts: boolean }Replay, debug and tests
With an API key (Authorization: Bearer) or an hwcli session. Pick the workspace with X-HookWatcher-Workspace. Test endpoints need the tests:read, tests:write or tests:run scope.
POST /api/v1/requests/:id/replays { target: { type: "url"|"cli"|"local", url?, cliSessionId? },
modifications?, scenario?, sequence?, signatureMode?, confirmProduction? }
GET /api/v1/replay-runs/:groupId every execution in a run (Lab runs have several)
POST /api/v1/replays/:executionId/result hwcli reports a local delivery
GET /api/v1/requests/:id/lineage derived replays, sessions and tests
GET /api/v1/requests/:id/diff/:otherId ?hideVolatile=true&ignore=path
GET /api/v1/requests/:id/similar successful events most like this one
GET /api/v1/failures ?endpoint=&event=&provider=&status=&since=
POST /api/v1/requests/:id/debug-sessions then POST /api/v1/debug-sessions/:id/attempts
GET /api/v1/tests POST { yaml } or { name, from, expect }; GET|PATCH|DELETE /api/v1/tests/:name
POST /api/v1/test-runs { names?, suite?, target, ci? } → POST …/:runId/results, …/:runId/finishValidation errors return HTTP 422 with { "error": "<plain-language reason>", "errorCode": "invalid_destination" }. Delivery endpoints return the attempt with status, responseStatus, durationMs and errorType, using HTTP 502 (or 504 for timeouts) when the destination failed. Scoped API keys (/developer) cover read, replay and export under /api/v1.
More
AI-generated documentation
Owners and managers can generate Markdown documentation for a request with AI, add context, polish edits, and save or delete it. Generate and polish each use one AI credit (Free: 30/month, Developer: 100/month, Pro: 300/month, Team: 600/month). Documentation is saved only when you click Save, and saved edits are never overwritten unless you generate or polish again. Sensitive headers are masked before content is sent to the AI provider.
Workspaces
Workspaces
A workspace holds endpoints, their captured requests, forwarding rules, alerts, API keys and an activity log, and the people who can see them. Use one for each project, client or environment. The workspace switcher at the top of the sidebar shows which one you are in; endpoints, search, analytics and live updates always cover only the active workspace.
Switching workspace never changes an endpoint's URL. Providers keep sending to the same address no matter who opens it or from which workspace.
Workspaces
Your Default Workspace
Every account has a Default Workspace. Endpoints you created before workspaces existed are already in it, with their URLs, history, forwarding and settings unchanged. You can rename it and invite people to it, but it can't be deleted while it is your only workspace. HookWatcher opens the workspace you used last.
Workspaces
Workspace limits
Limits come from the workspace owner's plan. The owner doesn't count towards the member limit; pending invitations do.
| Plan | Workspaces you own | Members per workspace | Retention |
|---|---|---|---|
| Free | 1 | 3 | 10 days |
| Developer | 3 | 10 | 30 days |
| Pro | 5 | 20 | 60 days |
| Team | 20 | 50 | 90 days |
| Enterprise | Custom | Custom | Custom |
Workspaces you were invited to don't count towards your own workspace limit. When you reach a limit, HookWatcher tells you which one and which plan raises it.
Workspaces
Members and roles
| Owner | Admin | Member | |
|---|---|---|---|
| View endpoints and requests | Yes | Yes | Yes |
| Create, edit and delete endpoints; forwarding; replay | Yes | Yes | No |
| Invite people and remove members | Yes | Yes (members only) | No |
| See the activity log | Yes | Yes | No |
| Change roles, rename, set retention | Yes | No | No |
| Transfer ownership or delete the workspace | Yes | No | No |
Each workspace has exactly one owner. Anyone except the owner can leave a workspace from its settings; the owner transfers ownership first. Removing a member ends their access straight away, including any page they have open.
Workspaces
Invitations
Owners and admins invite people by email from Workspace settings → Members. The invitation link works for 7 days and only for an account signed in with the invited email address; someone without an account can sign up with that address first. Pending invitations can be revoked at any time. The invited account also sees them as a banner in the app and can accept from there.
Workspaces
Retention
The owner sets how long captured requests are kept for the whole workspace, up to the plan's maximum. An endpoint can keep requests for less time by setting its own retention in Endpoint settings; leave that field empty to follow the workspace. Each request shows the date it will be removed.
Shortening retention removes older requests at the next hourly cleanup, so HookWatcher asks you to confirm first. Lengthening it applies to requests captured from then on.
Workspaces
Changing plans
Upgrading raises the limits at once. Moving to a lower plan brings your account within that plan's limits, and the plan picker shows exactly what will be removed — by name — before you confirm:
- Workspaces above the new limit are deleted with their endpoints and requests. Kept first: your Default Workspace, the workspace you're using, then the most recently active.
- In workspaces with more members than the new limit, pending invitations are revoked first (newest first), then members are removed: admins are kept before members, and recently active people before inactive ones. Removed people keep their own HookWatcher accounts.
- Workspace retention above the new maximum is lowered to it, and requests older than that are deleted.
Deleting stored data requires typing DOWNGRADE to confirm. Afterwards you get an email summarizing what changed. A lower paid plan starts when its payment completes; time left on the previous plan isn't carried over.
Workspaces
If a subscription isn't renewed
Paid plans are prepaid and never renew or charge automatically. If a paid plan ends without a renewal:
| When | What happens | |
|---|---|---|
| The day it expires | Your account stays available with Free limits; nothing is deleted. | Subscription expired, with both dates below |
| 17 days later | Nothing changes yet. | Deactivated in 3 days |
| 20 days after expiry | The account is deactivated: sign-in shows only its status and renewal, endpoints answer 410 and stop capturing, forwarding and alerts stop, API keys are revoked. Members lose access to your workspaces. | Account deactivated, with the deletion date |
| 8 days after deactivation | Still recoverable. | Data deleted in 2 days |
| 10 days after deactivation | The account and its data — endpoints, requests, workspaces you own, forwarding, API keys — are permanently deleted. Payment records are kept for accounting. | Account deleted |
Renewing at any point before deletion restores everything; create new API keys afterwards. Before deactivation you can also switch to the Free plan, which keeps the account within Free limits. Dates are shown exactly (UTC) in the emails, on the account status page and in the plan picker.
Workspaces
Workspace security
Every request is checked against your membership on the server. A workspace you don't belong to looks the same as one that doesn't exist. API keys are bound to the workspace that was active when you created them and can't reach endpoints in any other workspace. Owners and admins can review invitations, role changes, removals and settings changes in the activity log.
Workspaces
Moving from endpoint sharing
Workspaces replace sharing single endpoints. People you shared an endpoint with before keep exactly that access and nothing more; they still see the endpoint in their Default Workspace. New sharing happens by inviting people to a workspace; the Members button on an endpoint opens that workspace's members.
More
Plans
Plans set the number of endpoints, retained requests, retention, forwarding allowance and timeouts, replay and AI limits. Team and Enterprise endpoints can be made public; Enterprise limits are set per contract. See Pricing for current limits.
More
Need help?
Email [email protected] or read the FAQ.