The Watchbillby LatticeDDI

Signed webhook alerts and the payload

Published . Updated .

This guide explains the webhook body, the two headers that sign it, and what a receiver should do when the signature does not match.

Give each webhook its own https address and a signing secret of at least 16 characters. Compute HMAC-SHA256 over the timestamp, a period, and the raw body, and compare it to the signature header. Accept the message only when the timestamp is close to now and the signature matches. A bad signature is not retried.

What is sent?

The request is HTTPS POST. The body is JSON. The content type is application/json. Two headers travel with it.

x-watchbill-timestamp is unix time in seconds. x-watchbill-signature is sha256= followed by the hex HMAC-SHA256 of the timestamp, a period, and the exact body bytes. The key is the signing secret you saved. No other header carries the secret.

Other headers name the event type and the delivery id: x-watchbill-event and x-watchbill-delivery. Use the delivery id in your log. Do not log the secret or the query string of your own URL if that URL contains a credential.

A down and an up for the same check share dedupKey. The value looks like watchbill: then the check id, then :outage. A test sets test to true and does not open an outage.

The body fields used in v1 are schema (the number 1), id, type, occurredAt, accountId, siteId, siteName, targetUrl, dedupKey, severity, summary, detail, statusUrl, and test. type is site.down or site.up for these alerts. severity is critical on the way down and info on the way up. detail is null unless a later field is added. Do not require a field that is not in this list.

An example down body, with the ids shortened:

{
  "schema": 1,
  "id": "delivery-id",
  "type": "site.down",
  "occurredAt": "2026-10-10T15:04:00.000Z",
  "accountId": "account-id",
  "siteId": "check-id",
  "siteName": "Northwind",
  "targetUrl": "https://northwind.example/",
  "dedupKey": "watchbill:check-id:outage",
  "severity": "critical",
  "summary": "Northwind is down.",
  "detail": null,
  "statusUrl": "https://thewatchbill.com/portal",
  "test": false
}

The up message uses the same dedupKey, type site.up, and severity info.

How do you check the signature?

Read the raw body before you parse the JSON. Build the string timestamp + "." + body. Compute HMAC-SHA256 with the signing secret as the key. Hex-encode the result and prefix sha256=. Compare that to x-watchbill-signature with a constant-time compare.

Reject a timestamp that is more than a few minutes from your clock. A matching signature on an old timestamp is a replay. Reject a body you parsed and then serialized again. The bytes will not match.

If the signature does not match, respond with a client error other than 429. That response is terminal. It is not retried. Rotate the secret on the alerting form and send a test after you deploy the new compare.

What is retried?

The send waits up to 8 seconds. HTTP 429, HTTP 500 and above, and a network failure are retried after 60 seconds, then 5 minutes, then 15 minutes, then 60 minutes. Another HTTP 400-class status, including a redirect, is terminal. Five terminal failures turn the destination off and email the account. A test send does not count toward those five.

Respond with a 2xx when you have stored the event. Respond with 429 only when you want the retry schedule. Do not answer 200 and then drop the body.

At most four integration sends run in one minute for a check, including retries that are due. A burst of checks does not flush an unbounded queue inside the probe.

A client webhook

The account webhook is the default for every client that inherits the account set. A client can store its own URL and secret, or you can assign the account webhook to selected clients. The list on the webhook card names the clients that use it. A check can replace the client set.

Assigning a webhook to selected clients does not change a client that already has its own set. Edit that set on the client.

Store the secret in the receiver's secret store, not in the repository that handles the POST. Rotate it when someone who knew it leaves. The old secret stops matching as soon as you save the new one, so deploy the compare first or accept a short gap and send a test after both sides use the new secret.

Log the delivery id, the event type, and the HTTP status you returned. Do not log the body if your policy treats the target URL as sensitive, and never log the signing secret. A mismatch at 3 a.m. is much faster to diagnose from those three fields than from a pasted payload.

Answer quickly. The sender waits 8 seconds and then treats a hang as a network failure, which is retried. If your handler creates a ticket before it answers, a slow PSA can turn one down into four attempts. Store the event, answer 200, and do the slow work after the response. A receiver that answers 200 and then loses the body has no second chance, because a success is not retried.

How The Watchbill helps

On Pro and Business, a webhook is sent when a confirmed outage opens and when it closes. The body is the JSON above. The signature is HMAC-SHA256 over the timestamp, a period, and the body. Checking mail and the first confirmed up do not use the webhook. The same plans can send ticket email, Teams, and PagerDuty.

Sources

  1. RFC 2104, HMAC. Accessed October 10, 2026.

Related guides