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

# Build a receiver

> Hand a ready-made prompt to your coding agent and let it scaffold a signature-verified Tallwatch webhook receiver in your stack.

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.

<Tip>
  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.
</Tip>

## 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:
  ```bash theme={null}
  openssl rand -base64 32
  ```
  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

<Steps>
  <Step title="Open your agent in the project">
    Claude Code, Cursor, or whatever you use, with your repo as context.
  </Step>

  <Step title="Paste the prompt">
    Copy the prompt below and replace the stack line with yours.
  </Step>

  <Step title="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.
  </Step>
</Steps>

```text Prompt for your coding agent theme={null}
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.acknowledged", "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](/webhooks/signing) and [payload reference](/webhooks/payload), then [test it locally](#test-it-locally).

## What you receive

Headers on every delivery:

| Header                  | Example                                                                 |
| ----------------------- | ----------------------------------------------------------------------- |
| `X-Tallwatch-Event`     | `incident.opened`, `incident.acknowledged`, `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](/webhooks/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`:

```bash theme={null}
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](/webhooks/signing#rotate-the-secret).
* **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](/webhooks/templates).
