Skip to main content

Webhooks

Webhooks allow you to receive real-time notifications when events occur in the Helix platform.

Overview

Helix sends HTTP POST requests to your configured webhook endpoints when specific events occur. This allows your application to react to changes without polling.

Understanding Webhooks

Webhooks Are Synchronization Signals

Treat webhooks as signals that something changed, not as the source of truth.

Webhooks are designed to notify your system that data has changed, prompting you to synchronize by fetching the current state from our API. This design pattern is intentional and has important implications for how you should handle webhooks.

Why Minimal Payloads?

Our webhook payloads are intentionally minimal (typically just IDs and timestamps). This design:

  1. Prevents stale data - You always fetch the current state from the API
  2. Handles race conditions - Multiple rapid changes result in one sync, with you getting the final state
  3. Simplifies your logic - Your sync code handles any state, not incremental changes
  4. Reduces payload size - Smaller payloads are more reliable to deliver

How to Handle Webhooks

The API is the source of truth. When you receive a webhook, always fetch the current state from our API rather than relying on the webhook payload. The payload tells you what changed; the API tells you the current state.

Make your handlers idempotent. The same webhook may be delivered multiple times due to network issues, retries, or edge cases. Your code should handle receiving the same notification twice without causing duplicate actions or errors.

Don't assume delivery order. Webhooks may arrive in a different order than the changes actually occurred. If event A happens before event B, you might receive the webhook for B first. Always fetch current state rather than applying incremental changes.

Rapid changes are debounced. If the same resource is updated multiple times within a short window (typically 2 minutes), you'll receive a single webhook notification rather than one per change. This is why fetching current state matters—you'll get the final result regardless of intermediate changes.

Acknowledge receipt immediately. Return a 200 status code as soon as you receive the webhook, then process it asynchronously. If your processing takes too long, we may retry the delivery thinking it failed, leading to duplicate processing.

Payload Envelope

Every webhook delivery is a JSON object with the same top-level shape, regardless of event type:

FieldTypeRequiredDescription
eventTypestringYesType of the event being delivered. See the per-event-type pages for the full list.
timestampnumberYesProducer timestamp (Unix ms) — when the underlying event occurred. NOT the delivery time. For delivery time use the X-Webhook-Timestamp header (regenerated per retry).
idstring (uuid)YesUnique webhook event ID. Stable across retries — use as your idempotency key.
dataobjectYesEvent-specific payload. Shape depends on eventType — see per-event-type pages.
Two timestamps, two meanings
  • Body timestamp — when the producer event happened. Stable across retries.
  • X-Webhook-Timestamp header — when this delivery attempt was sent. Regenerated on each retry. Use this for replay-attack windows, not the body field.
Idempotency

The body id is the same across all retry attempts of the same delivery. Use it (not data.eventId, which is event-specific and absent for news.item_added) as your dedupe key.

Webhook Types

We support webhooks for the following domains:

Fact Checking

Receive notifications about fact-check operations:

  • fact_check.completed - Fact-check finished successfully
  • fact_check.failed - Fact-check encountered an error
  • fact_check.status_changed - Fact-check status transitioned

News

Get notified about news feed changes:

  • news.item_added - New news item added to feed

Events

Receive updates about event feed changes:

  • event.item_added - New event added to feed
  • event.item_updated - Existing event was updated
  • event.item_removed - Event(s) removed from feed

Configuration

Webhook endpoints can be configured through the Helix dashboard or API. Each webhook requires:

  • Endpoint URL - The HTTPS URL where notifications will be sent
  • Event Types - Select which events should trigger this webhook
  • Secret Key - Used to verify webhook authenticity

HTTP Headers

All webhooks include the following HTTP headers for security and tracking:

HeaderDescriptionExample
Content-TypeContent type of the requestapplication/json
User-AgentUser agent identifying the webhook senderHelix-Data-Feeds/1.0
X-Webhook-SignatureHMAC-SHA256 signature for verifying webhook authenticityv1=a3c8d9e7f2b1a4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9
X-Webhook-TimestampUnix timestamp (milliseconds) when the webhook was sent1704123456789
X-Webhook-IDUnique identifier for the webhook configuration123e4567-e89b-12d3-a456-426614174000
X-Webhook-Event-IDUnique identifier for this specific webhook event987e6543-e21b-12d3-a456-426614174111

Security & Verification

To verify webhook authenticity, validate the signature in the X-Webhook-Signature header using your webhook secret. The signature is computed as:

HMAC-SHA256(secret, `${X-Webhook-Timestamp}.${rawRequestBody}`) → "v1=<hex>"

Two important details:

  1. The timestamp comes from the X-Webhook-Timestamp header, not the timestamp field inside the body.
  2. The signed payload is the raw request body bytes, exactly as received. Do not parse and re-stringify the JSON — key ordering may differ and the signature will not match.
// Example signature verification (Express-style, with a raw body buffer)
const crypto = require('crypto');

function verifyWebhookSignature(rawBody, headers, secret, maxAgeMs = 5 * 60 * 1000) {
const timestamp = headers['x-webhook-timestamp'];
const signature = headers['x-webhook-signature'];

// Reject anything we can't safely use. A malformed/missing header should
// never be able to crash the verifier — an attacker controls these values.
if (typeof timestamp !== 'string' || typeof signature !== 'string') return false;

// Reject stale deliveries (replay-attack protection)
if (Date.now() - Number(timestamp) > maxAgeMs) return false;

const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature =
'v1=' +
crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');

// Compare BYTE lengths, not string lengths. Buffer.from(str, 'utf8') can
// produce a buffer whose length differs from str.length when the string
// contains multi-byte characters. timingSafeEqual throws RangeError on
// length mismatch, so guarding by string length leaves an attacker-crafted
// X-Webhook-Signature header (matching char count, mismatched byte count)
// able to crash the handler.
const expectedBuf = Buffer.from(expectedSignature, 'utf8');
const signatureBuf = Buffer.from(signature, 'utf8');
if (expectedBuf.length !== signatureBuf.length) return false;

return crypto.timingSafeEqual(expectedBuf, signatureBuf);
}
Capture the raw body

Most frameworks parse JSON for you, which mutates whitespace and key order. Capture the raw body before parsing — e.g. in Express, app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString('utf8'); } })).

Best Practices

  • Always verify the signature before processing webhook data
  • Check X-Webhook-Timestamp to prevent replay attacks (reject requests older than 5 minutes)
  • Use HTTPS endpoints only
  • Return a 2xx status code promptly to acknowledge receipt

Retry Policy

Failed webhook deliveries are automatically retried with exponential backoff:

  • 1st retry: after 1 minute
  • 2nd retry: after 5 minutes
  • 3rd retry: after 15 minutes
  • 4th retry: after 1 hour
  • 5th retry: after 6 hours

Webhooks are considered successful on HTTP 2xx responses. Other status codes trigger retries.

After all retries are exhausted, the webhook delivery is marked as failed and you'll need to manually retrieve the missed data via the API.