Documentation Index
Fetch the complete documentation index at: https://docs.tallwatch.com/llms.txt
Use this file to discover all available pages before exploring further.
A webhook channel needs one thing on your side: an HTTPS endpoint that accepts a POST, verifies the signature, and returns a 2xx. The verification has a few sharp edges (raw body, constant-time compare, a timestamped signature), so the fastest reliable way to build it is to let your coding agent do it from the prompt below.
Use the Copy page button at the top of this page, or Open in Claude / Open in ChatGPT, to hand the whole spec to your agent at once. The prompt below is the short version.
Before you start
- An HTTPS URL that can accept a
POST. Plain HTTP is rejected. In development, expose localhost with a tunnel (ngrok http 3000, cloudflared tunnel) so Tallwatch can reach it.
- A signing secret of at least 16 characters, shared between Tallwatch and your app:
Store it as
TALLWATCH_WEBHOOK_SECRET in your app, and paste the same value into the channel form when you add the webhook.
Generate it with your coding agent
Open your agent in the project
Claude Code, Cursor, or whatever you use, with your repo as context.
Paste the prompt
Copy the prompt below and replace the stack line with yours.
Let it scaffold and test
The prompt asks for a verified endpoint plus a unit test, so you can run the test to confirm both the accept and reject paths before going live.
Prompt for your coding agent
Build a webhook receiver for Tallwatch (an uptime monitoring service) in this project.
My stack: <REPLACE: e.g. Next.js App Router / Express / FastAPI / Go net/http / Rails>.
Requirements:
1. Add a POST endpoint at /webhooks/tallwatch.
2. Read the RAW request body (bytes/string) BEFORE any JSON parsing. The signature
is computed over the exact bytes sent; parsing and re-serializing JSON breaks it.
3. Verify the signature before anything else:
- The request header `X-Tallwatch-Signature` looks like `t=1717337041,v1=<hex>`.
Split on "," then "=" to read `t` (unix seconds) and `v1` (the signature).
- Compute HMAC-SHA256(secret, "t=" + t + "." + rawBody) and hex-encode it. Keep
the literal `t=` prefix and the `.` separator before the body.
- Compare your digest to `v1` with a CONSTANT-TIME comparison (timingSafeEqual /
hmac.compare_digest / hmac.Equal), never ==.
- Return 401 if it doesn't match, or if `t` is more than 300 seconds from now
(replay protection).
- Read the secret from the env var TALLWATCH_WEBHOOK_SECRET.
4. On a valid request, parse the JSON and branch on the `event` field. Handle at
least "incident.opened", "incident.resolved", and "test". Body shape:
{ event, occurred_at, org, monitor, incident, check }.
5. Respond 2xx (a bare 204 is ideal) within 10 seconds. For slow work, respond 204
immediately and process asynchronously.
6. Be idempotent: the same event can arrive more than once because transient
failures are retried. Dedupe on a stable key such as incident.id.
7. Add a unit test: one correctly signed request asserting 204, and one tampered
request asserting 401.
Use the standard library for HMAC. Do not add a webhook framework dependency.
After it runs, skim the result against the signature spec and payload reference, then test it locally.
What you receive
Headers on every delivery:
| Header | Example |
|---|
X-Tallwatch-Event | incident.opened, incident.resolved, test |
X-Tallwatch-Timestamp | 1717337041 |
X-Tallwatch-Signature | t=1717337041,v1=<hex> |
User-Agent | Tallwatch-Webhook/1.0 |
The body is the canonical JSON payload, or your Handlebars output if the channel uses a template. Field-by-field reference: Payload.
How delivery behaves
- Return any
2xx. A bare 204 with no body is ideal. Anything else counts as failed.
- Answer within 10 seconds. That’s the per-request cap. If your work takes longer, return
202 immediately and process asynchronously.
- Transient failures retry. A
5xx, 429, or timeout is retried inline up to 3 times with a short backoff, then marked failed on the incident. A 4xx is treated as a permanent rejection and not retried.
Test it locally
You don’t need Tallwatch to test verification. This produces a correctly signed request with openssl and curl:
SECRET="paste-your-secret-here"
URL="http://localhost:3000/webhooks/tallwatch"
TS=$(date +%s)
BODY='{"event":"test","occurred_at":"2026-01-01T00:00:00Z","org":{"id":"00000000-0000-0000-0000-000000000000","slug":"demo","name":"Demo"},"monitor":{"id":"00000000-0000-0000-0000-000000000001","name":"My site","type":"http","url":"https://example.com"},"incident":null,"check":null}'
SIG=$(printf '%s' "t=${TS}.${BODY}" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$URL" \
-H "Content-Type: application/json" \
-H "X-Tallwatch-Timestamp: ${TS}" \
-H "X-Tallwatch-Event: test" \
-H "X-Tallwatch-Signature: t=${TS},v1=${SIG}" \
-d "$BODY"
Expected: 204. Change one character of SECRET and rerun. You should get 401. That confirms both paths before you point a real channel at it.
When the endpoint is live over HTTPS, add the channel in Settings → Alerts → Channels → Add channel → Webhook, paste the URL and the same secret, and click Send test alert.
Make it production-ready
- Be idempotent. Tallwatch retries transient failures, so the same event can arrive more than once. Derive a stable key (for example
event + incident.id) and skip work you’ve already done.
- Keep the replay window tight. 300 seconds is a reasonable tolerance. Reject anything older.
- Rotate without downtime. Accept the old and new secret during a rollover, then drop the old one. See Signing.
- Reshape the body if you want. Set a Handlebars template on the channel and the body becomes whatever you render. The signature still covers the rendered bytes, so verification is unchanged. See Templates.