Skip to content

Webhooks

ChannelWatch can send notifications to your own HTTP endpoint as outbound webhooks. This is a separate notification surface from Custom (Apprise) and Provider Plugins. Use webhooks when you want ChannelWatch to POST a signed JSON payload to another system you control.

Each enabled webhook sends an HTTP POST request to the URL you configure. ChannelWatch includes a JSON body, an event type, a delivery ID, and an HMAC signature header so your receiver can verify that the request came from a sender that knows the shared secret.

ChannelWatch sends webhooks only when a webhook entry is enabled and has both a URL and a secret. If the secret is missing, delivery is skipped.

ChannelWatch signs the raw request body with HMAC-SHA256 and sends the result in the X-ChannelWatch-Signature header.

  • Algorithm: HMAC-SHA256
  • Signature header: X-ChannelWatch-Signature
  • Format: sha256=<hex-digest>

Two additional headers are included on every request:

  • X-ChannelWatch-Delivery: the per-request delivery ID
  • X-ChannelWatch-Event: the event type, such as test or recording.started

In ChannelWatch, each webhook entry contains three fields:

  • url: the destination endpoint
  • secret: the shared secret used for HMAC signing
  • enabled: whether ChannelWatch should send to that endpoint

Your receiving service should:

  1. Accept POST requests with a JSON body
  2. Read the X-ChannelWatch-Signature header
  3. Recompute the HMAC with the same shared secret
  4. Compare the computed value with the received signature using a constant-time comparison
  5. Return a 2xx response when the payload is accepted

If your receiver depends on links back to the ChannelWatch instance, set one of these environment variables on the ChannelWatch side so instanceUrl in the payload uses the correct public URL:

  • CHANNELWATCH_INSTANCE_URL
  • CW_INSTANCE_URL
  • APP_URL

If none of those variables are set, ChannelWatch uses http://localhost:8501 as the fallback instanceUrl value.

The verification flow is the same in every language:

  1. Read the raw request body as bytes
  2. Compute HMAC-SHA256(secret, body)
  3. Prefix the hex digest with sha256=
  4. Compare it to X-ChannelWatch-Signature
import crypto from 'node:crypto';
function verifyChannelWatchSignature(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const received = Buffer.from(signatureHeader || '', 'utf8');
const computed = Buffer.from(expected, 'utf8');
if (received.length !== computed.length) {
return false;
}
return crypto.timingSafeEqual(received, computed);
}
import hashlib
import hmac
def verify_channelwatch_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature_header or "", expected)

ChannelWatch sends a JSON object with this shape:

{
"eventType": "notification",
"timestamp": "2026-04-21T00:00:00Z",
"instanceName": "ChannelWatch",
"instanceUrl": "http://localhost:8501",
"version": "1.0.0",
"deliveryId": "550e8400-e29b-41d4-a716-446655440000",
"dvr_id": "dvr_1234",
"dvr_name": "Living Room",
"data": {
"title": "Test title",
"message": "Test message",
"imageUrl": null
}
}

Field meanings:

  • eventType: the normalized event name chosen by ChannelWatch
  • timestamp: when the payload was generated, in UTC ISO 8601 format
  • instanceName: the ChannelWatch app name
  • instanceUrl: the configured instance URL, or http://localhost:8501 if no instance URL env var is set
  • version: the ChannelWatch version string
  • deliveryId: a unique ID for this webhook attempt
  • dvr_id: the DVR ID tied to the alert, when available
  • dvr_name: the DVR display name tied to the alert, when available
  • data.title: the notification title
  • data.message: the notification message body
  • data.imageUrl: an image URL when present, otherwise null

Source-backed event types include these values:

  • test
  • channel.watching.start
  • vod.playback.start
  • disk.space.warning
  • disk.space.critical
  • recording.scheduled
  • recording.started
  • recording.cancelled
  • recording.completed
  • recording.updated
  • alert.notification
  • notification

Check these first:

  1. Make sure your receiver uses the raw request body bytes, not parsed JSON that has been serialized again.
  2. Verify that your shared secret exactly matches the secret configured in ChannelWatch.
  3. Confirm that you’re reading X-ChannelWatch-Signature, not X-ChannelWatch-Delivery or X-ChannelWatch-Event.
  4. Make sure your comparison includes the sha256= prefix.

If you rotate the webhook secret in ChannelWatch, update the receiving service at the same time. Old signatures will stop matching immediately.

ChannelWatch attempts each webhook up to 3 times. Each request uses a 5 second timeout, with exponential retry delays of 1 second and 2 seconds between retries.

If your endpoint still receives nothing:

  1. Confirm the webhook entry is enabled.
  2. Confirm the destination URL is correct and reachable from the ChannelWatch container.
  3. Confirm a secret is configured. ChannelWatch skips delivery when the secret is missing.
  4. Send a test notification, then review the Delivery Log and container logs for timeout or HTTP status errors.
  • Custom (Apprise) for third-party notification services that already have an Apprise URL scheme
  • Delivery Log to inspect webhook attempts and failures
  • Provider Plugins if you need custom Python delivery logic instead of an HTTP endpoint