24observe
checking… Sign in Start free
Docs · event webhook subscriptions

Stop polling. Subscribe to events.

Register a URL with one POST. We push a signed JSON envelope to it the moment an event fires anywhere in your org — typically under 500 ms from API action to your receiver. Retries are automatic, signing is HMAC-SHA256, broken receivers auto-disable.

Distinct from the per-monitor alert webhook (one URL on a monitor row, fires when that monitor goes down). Subscriptions are org-scoped, fire on event types, and carry a structured envelope. Same signing scheme — the same receiver code verifies both.

Event types

An empty eventTypes: [] on the subscription means "send me every event type the org produces." Useful for the first-day configuration; agents tend to narrow it later.

Create a subscription

curl -X POST https://api.24observe.com/api/v1/webhook-subscriptions \
  -H 'Authorization: Bearer obs_<pat-with-webhooks:write>' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://agent.example.com/24observe-events",
    "eventTypes": ["incident.opened", "incident.resolved"],
    "description": "incident-bot in prod"
  }'

# Response (201)
{
  "id": 42,
  "url": "https://agent.example.com/24observe-events",
  "eventTypes": ["incident.opened", "incident.resolved"],
  "description": "incident-bot in prod",
  "enabled": true,
  "disabledAt": null,
  "consecutiveFailures": 0,
  "lastDeliveryAt": null,
  "lastDeliveryStatus": null,
  "lastDeliveryStatusCode": null,
  "createdAt": "2026-05-23T18:00:00.000Z"
}

Required scope: webhooks:write. Required role: owner or admin. SSRF preflight runs at write time — URLs resolving to private/loopback/link-local addresses are rejected with 400 WEBHOOK_URL_UNSAFE; DNS failures get 400 WEBHOOK_URL_UNRESOLVABLE.

Delivery envelope

POST https://agent.example.com/24observe-events
Content-Type: application/json
User-Agent: 24observe-webhooks/1.0
X-24Observe-Event: incident.opened
X-24Observe-Delivery-Id: sub-42-incident.157.opened.1
X-24Observe-Timestamp: 1716480000
X-24Observe-Signature: t=1716480000,v1=<64-char-hex>

{
  "id": "incident.157.opened",
  "type": "incident.opened",
  "created": 1716480000,
  "data": {
    "incident_id": 157,
    "monitor_id": 7,
    "monitor_name": "checkout-api",
    "monitor_url": "https://api.example.com/healthz",
    "status": "investigating",
    "error_message": "HTTP 503",
    "opened_at": "2026-05-23T18:00:00.000Z"
  }
}

Verify the signature exactly as for per-monitor alert webhooks — same HMAC-SHA256 scheme, same org-level secret. Working Node / Python / Go examples on /docs/webhooks/.

Retry + auto-disable

Re-enable via PATCH after fixing the receiver. The consecutiveFailures counter is cleared and any new event fan-out lands again.

List subscriptions + delivery history

# List all subscriptions for your org (bare array — no envelope)
GET /api/v1/webhook-subscriptions

# Disable / re-enable / change eventTypes / change URL
PATCH /api/v1/webhook-subscriptions/:id
  { "enabled": false }
  { "enabled": true }   # also clears disabledAt + disabledReason + consecutiveFailures
  { "eventTypes": ["incident.opened"] }
  { "url": "https://new-receiver.example.com/x" }

# Delete (cascades the delivery rows)
DELETE /api/v1/webhook-subscriptions/:id

# Inspect the last 50 delivery attempts for one subscription
GET /api/v1/webhook-subscriptions/:id/deliveries
# Returns: id, eventType, eventId, attempt, status, statusCode, errorMessage,
#          payloadJson (exact bytes we sent), responseBodyExcerpt
#          (first 1 KB of receiver's response), deliveredAt, createdAt

The payloadJson field on each delivery row is the exact bytes we POSTed to your receiver — including the timestamp the signature was computed against. If your receiver's signature check is failing, this is the byte sequence to verify against.

Required scopes

Plus role: owner or admin on writes. Members and viewers can list but not modify.

Reference receiver (Node)

Minimal Express receiver that verifies signatures and logs deliveries. Drop in, set OBSERVE24_WEBHOOK_SECRET from GET /api/v1/me/webhook-secret, and you're done.

import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

const SECRET = process.env.OBSERVE24_WEBHOOK_SECRET;
const app = express();

app.post('/24observe-events', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.header('x-24observe-signature') || '';
  const parts = Object.fromEntries(sig.split(',').map(p => p.split('=')));
  const t = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400); // replay defense

  const expected = createHmac('sha256', SECRET).update(`${t}.${req.body.toString('utf8')}`).digest();
  const actual = Buffer.from(parts.v1, 'hex');
  if (actual.length !== expected.length || !timingSafeEqual(expected, actual)) {
    return res.sendStatus(401);
  }

  const envelope = JSON.parse(req.body.toString('utf8'));
  const eventType = req.header('x-24observe-event');
  console.log(`[${eventType}] ${envelope.id}`, envelope.data);

  // Return 200 within 10s. Heavy work belongs in a job queue.
  res.sendStatus(204);
});

app.listen(3000);

Common mistakes

When NOT to use this

Related