LogTide

Webhooks

LogTide delivers events — alerts, anomalies, errors, monitor status changes, security incidents, and notification-channel webhooks — to your endpoints through a centralized dispatcher with SSRF protection, retry with exponential backoff, a dead-letter queue, and delivery logging.

Overview

Every outbound delivery — whether it originates from an alert rule, an uptime monitor, an error group, a security incident, or a notification channel — flows through one well-tested dispatcher. That gives every webhook the same guarantees:

Signed payloads

Optional HMAC-SHA256 signature so you can verify each delivery came from LogTide untampered.

Automatic retries

Transient failures are retried with exponential backoff before being parked.

Dead-letter queue

Exhausted deliveries are kept and can be replayed from the dashboard.

SSRF protection

Targets are resolved and validated on every request and every redirect hop.

Delivery Semantics

  • Retries — transient failures (network errors, timeouts, HTTP 5xx, 429) are retried with exponential backoff (1s, 5s, 25s, 2m, 10m), up to WEBHOOK_MAX_ATTEMPTS (default 5). Other 4xx responses and SSRF-blocked targets are terminal and not retried.
  • Dead-letter queue — after the final failed attempt a delivery is marked dead. Dead and failed deliveries can be replayed from Settings → Webhook Deliveries.
  • Idempotency — each delivery carries a stable id, and the same logical event is never enqueued twice. A receiver can safely deduplicate on the envelope id.
  • Timeout — each attempt times out after WEBHOOK_REQUEST_TIMEOUT_MS (default 10s).

Return 2xx quickly

Acknowledge the delivery with a 2xx as soon as you have persisted it, then do your work asynchronously. A slow handler that exceeds the request timeout is treated as a failed attempt and will be retried.

Event Envelope

Every delivery body is a JSON object with a top-level envelope. The data field carries the event-specific payload.

{
  "id": "evt_4b3e1a2c-8f7d-4e6b-9c0a-1d2e3f4a5b6c",
  "type": "alert.triggered",
  "version": 1,
  "occurredAt": "2026-06-11T14:32:07.841Z",
  "organizationId": "9f8e7d6c-5b4a-3c2d-1e0f-a1b2c3d4e5f6",
  "projectId": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
  "data": {
    "alert_name": "Error rate spike",
    "log_count": 142,
    "threshold": 50,
    "time_window": 300,
    "baseline_metadata": null,
    "link": "https://app.logtide.dev/alerts/history/abc123"
  }
}

Envelope fields

Field Type Description
idstringStable delivery id, prefix evt_ followed by a UUID. Doubles as the dedup id when the producer does not supply one.
typestringEvent type (see table below).
versionnumberSchema version. Currently always 1; the X-Logtide-Event-Version header carries the same value for routing without parsing the body.
occurredAtstringISO 8601 timestamp of when the event occurred.
organizationIdUUIDOrganization that owns the event.
projectIdUUID | nullProject scope. null for org-wide events (e.g. alert rules without a project filter).
dataobjectEvent-specific payload. See per-type fields below.

Validating with @logtide/shared

The envelope and per-type data schemas ship as Zod schemas, so a TypeScript receiver can parse and validate in one step:

import { webhookEnvelopeSchema, parseWebhookEvent } from '@logtide/shared';

// Parse the envelope only (data stays Record<string, unknown>)
const envelope = webhookEnvelopeSchema.parse(body);

// Parse the envelope AND validate the per-type data schema
const event = parseWebhookEvent(body);

parseWebhookEvent throws a Zod ZodError if either the envelope shape or the per-type data fields are invalid.

Event Types

Type Description
alert.triggeredAn alert rule or anomaly detection fired. Anomaly detections carry a non-null data.baseline_metadata.
incident.createdA new security incident was created (typically from a Sigma rule detection).
error.detectedAn exception group crossed its notification threshold.
monitor.status_changedAn uptime monitor changed status (e.g. up → down).
channel.testA test delivery sent when verifying a notification channel.

Each type's data payload keeps the snake_case fields you may already consume, minus the legacy event_type / timestamp keys (now type and occurredAt on the envelope). The most common shapes:

alert.triggered data

{
  "alert_name": "Error rate spike",
  "log_count": 142,
  "threshold": 50,
  "time_window": 300,
  "baseline_metadata": {
    "baseline_value": 30,
    "current_value": 142,
    "deviation_ratio": 4.7,
    "baseline_type": "rate_of_change",
    "evaluation_time": "2026-06-11T14:32:07.841Z"
  },
  "link": "https://app.logtide.dev/alerts/history/abc123"
}

baseline_metadata is null for plain threshold rules and non-null for anomaly / rate-of-change detections.

error.detected data

{
  "title": "TypeError: cannot read property 'id' of undefined",
  "message": "TypeError: cannot read property 'id' of undefined",
  "severity": "high",
  "organization": { "id": "9f8e...", "name": "Acme" },
  "project": { "id": "3a4b...", "name": "api" },
  "error_group_id": "grp_8d2f...",
  "exception_type": "TypeError",
  "language": "nodejs",
  "service": "checkout",
  "is_new": true,
  "link": "https://app.logtide.dev/errors/grp_8d2f"
}

monitor.status_changed data

{
  "monitor_id": "mon_1a2b...",
  "monitor_name": "Checkout API",
  "status": "down",
  "severity": "critical",
  "title": "Checkout API is down",
  "message": "HTTP 503 from https://api.example.com/health",
  "organization": { "id": "9f8e...", "name": "Acme" },
  "target": "https://api.example.com/health",
  "error_code": "503",
  "response_time_ms": null,
  "consecutive_failures": 3,
  "downtime_duration": null,
  "link": "https://app.logtide.dev/monitoring/mon_1a2b"
}

incident.created and channel.test

incident.created carries title, message, severity, organization, incident_id, optional affected_services, and link. channel.test carries a title / message with optional severity, organization, link and a free-form metadata map.

Verifying Signatures

When a signing secret is configured for a webhook, LogTide signs each delivery so you can verify it came from LogTide and was not tampered with. Two headers are added:

  • X-Logtide-Timestamp: <unix seconds>
  • X-Logtide-Signature: t=<unix>,v1=<hex hmac>

The signature is HMAC-SHA256(secret, "<timestamp>.<raw request body>"), hex-encoded, and covers the serialized envelope.

import crypto from 'crypto';

function verify(secret, headers, rawBody) {
  const ts = headers['x-logtide-timestamp'];
  const sig = headers['x-logtide-signature'].split('v1=')[1];
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`)
    .digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(sig, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Verify against the raw body

Compute the HMAC over the raw request body (before JSON parsing) — any reserialization can change the bytes and break the signature. Reject deliveries whose timestamp is older than your tolerance (e.g. 5 minutes) to prevent replay.

SSRF Protection

Outbound targets are resolved and validated before each request. Loopback, private, link-local (including cloud metadata), CGNAT, multicast, and reserved IPv4/IPv6 ranges are rejected, and every redirect hop is revalidated.

Self-hosted deployments that need to reach internal endpoints can opt in by setting MONITOR_ALLOW_PRIVATE_TARGETS=true.

Replaying Deliveries

Every delivery and each of its attempts are recorded in a bounded delivery log, browsable under Settings → Webhook Deliveries with a status filter. Failed and dead-lettered deliveries can be replayed from there.

Historical replays

Deliveries created before the envelope change re-send their stored pre-envelope payload when replayed, while still carrying the X-Logtide-Event-Version header. Treat the header as advisory for replayed historical deliveries.

Configuration

Variable Default Description
WEBHOOK_MAX_ATTEMPTS5Maximum delivery attempts before the dead-letter queue.
WEBHOOK_PER_ORG_CONCURRENCY5Concurrent deliveries per organization.
WEBHOOK_GLOBAL_CONCURRENCY50Concurrent deliveries across all organizations.
WEBHOOK_DELIVERY_LOG_LIMIT1000Attempts retained per delivery in the delivery log.
WEBHOOK_REQUEST_TIMEOUT_MS10000Per-attempt request timeout in milliseconds.
MONITOR_ALLOW_PRIVATE_TARGETSfalseAllow delivery to private/internal addresses (self-hosted).

Want to observe, enrich, or reject deliveries before they leave the backend? See the Lifecycle Hooks guide — the beforeWebhookDispatch and afterWebhookDispatch phases hook directly into this dispatcher.

Esc

Type to search across all documentation pages