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

# Webhook

> POST Tallwatch incident events to any HTTPS URL, signed with HMAC-SHA256. Shape the body with Handlebars.

The webhook channel is the escape hatch for anything not in the native list. Accept an HTTPS POST and you can route Tallwatch incidents into your own systems. Every request is signed with HMAC-SHA256 over the raw body, so you can prove it came from us.

This page covers setup. Three companion pages go deeper:

* [Build a receiver](/webhooks/quickstart) for complete, verified endpoints in Express, Next.js, FastAPI, Go, Rails, Laravel, and the edge
* [Payload reference](/webhooks/payload) for the exact JSON, field by field
* [Signing](/webhooks/signing) for the signature math and per-language verification snippets

## Before you start

* An HTTPS endpoint that accepts a POST. Plain HTTP is rejected; Tallwatch won't send credentials over an unencrypted connection.
* The ability to read a request header and the raw request body. Verification happens server-side.

## 1. Generate a signing secret

Make a random secret of at least 16 characters. Both Tallwatch and your receiver hold it; both need it to compute the signature.

<CodeGroup>
  ```bash openssl theme={null}
  openssl rand -base64 32
  ```

  ```bash node theme={null}
  node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
  ```

  ```bash python theme={null}
  python -c "import secrets; print(secrets.token_urlsafe(32))"
  ```
</CodeGroup>

Save the output. You'll paste it into Tallwatch and into your receiver's config.

## 2. Add the channel in Tallwatch

<Steps>
  <Step title="Open the channels page">
    Go to **Settings → Alerts → Channels** and click **Add channel**.
  </Step>

  <Step title="Pick Webhook">
    Choose the Webhook kind and name it for the receiver, like `Datadog incidents` or `Internal ticketing`.
  </Step>

  <Step title="Paste the URL and secret">
    Put your HTTPS endpoint in **Webhook URL** and the secret in **Signing secret**. Tallwatch won't show the secret again after save, so keep your own copy.
  </Step>

  <Step title="Optionally template the body">
    Leave **Payload template** blank for the [canonical JSON](/webhooks/payload), or paste a [Handlebars template](/webhooks/templates) to reshape it.
  </Step>

  <Step title="Save and test">
    Click **Save**, then **Send test alert**. Your receiver should log one POST with a sample payload.
  </Step>
</Steps>

## What gets POSTed

Every dispatch is a POST with these headers:

| Header                  | Value                                                                                      |
| ----------------------- | ------------------------------------------------------------------------------------------ |
| `Content-Type`          | `application/json`, or `text/plain` if your template renders a non-JSON body               |
| `User-Agent`            | `Tallwatch-Webhook/1.0`                                                                    |
| `X-Tallwatch-Event`     | The event type: `incident.opened`, `incident.acknowledged`, `incident.resolved`, or `test` |
| `X-Tallwatch-Timestamp` | Unix seconds at signing time, also embedded in the signature                               |
| `X-Tallwatch-Signature` | `t=<timestamp>,v1=<hmac>`, the HMAC-SHA256 covering the timestamp and raw body             |

The body is the [canonical payload](/webhooks/payload), or your rendered template if you set one.

<Warning>
  Verify the signature before processing the body. A spoofed POST is indistinguishable from a real one if you skip verification. The [Signing page](/webhooks/signing) has copy-paste snippets and explains the exact string that's signed.
</Warning>

## What Tallwatch expects back

Return any 2xx. Anything else is a delivery failure.

* **2xx**: marked `sent`, response body discarded.
* **5xx, 429, network error, or timeout**: retried in place, up to 3 attempts (500 ms then 2 s between them). If all three miss, the dispatch is `failed` and shows on the incident.
* **Other 4xx**: marked `failed` and not retried. A 4xx means your receiver rejected the request, so retrying won't help.

Redirects are followed by the underlying HTTP client, so point the channel at a URL that responds directly rather than relying on a 3xx hop. The per-request timeout is 10 seconds; if your receiver needs longer, return `202 Accepted` immediately and process the event asynchronously.

## Reshape the body

The canonical payload is verbose. If your receiver expects a specific shape, paste a Handlebars template into the channel form. Here's a minimal Slack-style body:

```handlebars Webhook payload template theme={null}
{
  "text": "{{monitor.name}} is {{incident.status}}",
  "channel": "#alerts"
}
```

The full variable set and more examples live on the [Templates page](/webhooks/templates).

## Troubleshooting

<AccordionGroup>
  <Accordion title="My receiver returns 401 to every request">
    Most often the signature doesn't match because the secret differs. Confirm the receiver's secret is byte-for-byte the one in Tallwatch, with no stray whitespace or newline.

    Next most often, you're verifying against the parsed JSON instead of the raw bytes. Always verify the raw body. See [Signing](/webhooks/signing).
  </Accordion>

  <Accordion title="Dispatches show `failed` with a network error">
    The request never reached your receiver. Usual causes: a wrong URL (typo, dead domain, DNS failure), a firewall blocking Tallwatch's egress, or a TLS handshake failure from a self-signed or expired certificate.
  </Accordion>

  <Accordion title="Some events deliver, others fail">
    A 5xx is retried up to 3 times in the same pass, then left `failed`. There's no queue: the only re-send is that bounded inline retry. Fix the receiver and the next new event delivers fine. Past failures stay on the record as `failed`.
  </Accordion>

  <Accordion title="A field is missing from the payload">
    The canonical payload sets every field. When a value is genuinely unknown (like `resolved_at` on an open incident) it's `null`, not omitted, so check your receiver isn't dropping nullable fields. If you templated the body, your template controls the shape.
  </Accordion>
</AccordionGroup>

## Reference

| Property         | Value                                                                   |
| ---------------- | ----------------------------------------------------------------------- |
| Channel kind     | `webhook`                                                               |
| Required config  | `url` (HTTPS), `signing_secret` (16+ chars)                             |
| Optional config  | `payload_template` (Handlebars)                                         |
| Method           | `POST`                                                                  |
| Signature header | `X-Tallwatch-Signature: t=<ts>,v1=<hmac>`                               |
| Timeout          | 10 seconds                                                              |
| Retry            | Up to 3 inline attempts on 5xx, 429, network, or timeout, then `failed` |
| Delivery         | Immediate, no proactive rate limit                                      |
| User-Agent       | `Tallwatch-Webhook/1.0`                                                 |
