Webhooks allow you to receive monitoring alerts as HTTP POST requests to an endpoint you control, instead of (or in addition to) email or Slack. This makes it easy to integrate monitoring alerts directly into your own systems — pipe them into a custom dashboard, trigger downstream automation, or store them in your own database.When a development matches one of your monitoring views, Messari serializes the matched event as JSON and POSTs it to your configured URL. Each request can be signed with an HMAC signature so you can verify it came from Messari.This page covers how to configure webhook delivery, the payload format, signature verification, and retry behavior.
To enable webhook delivery for a monitoring view, create or update an alert policy through the UI and provide a webhook_url. Optionally include a webhook_secret to enable HMAC signature verification.
webhook_url (required when delivery_types contains webhook) — the HTTP(S) endpoint Messari will POST to.
webhook_secret (optional) — a shared secret used to compute the HMAC signature. We strongly recommend setting one so your endpoint can verify request authenticity. If omitted, the X-Webhook-Signature header will not be included.
Webhooks are only delivered for the immediately cadence. A policy with daily or weekly cadence will not dispatch to webhook endpoints even if webhook is listed in delivery_types and a webhook_url is set.
Each webhook is delivered as an HTTP POST with the following headers:
Header
Value
Content-Type
application/json
User-Agent
MessariCNS/1.0
X-Webhook-Signature
HMAC signature (only present when webhook_secret is set) — see Verifying Signatures
The request body is a JSON envelope. Optional fields that aren’t set are omitted entirely from the JSON, not present as null. The example below shows a fully populated payload:
{ "id": "918be734-813f-4c00-91df-9bb5664a5120", "type": "monitoring_event", "timestamp": "2026-04-10T11:29:06.987283Z", "data": { "view": { "id": "f4fbb8a3-aa3f-4be3-9130-01a023f40b1e", "name": "Bitcoin Core Releases" }, "event": { "id": "07aeb98e-b5cb-4756-ba18-44b6c0fd2f7f", "title": "Bitcoin Core 31.0 RC Release Testing" }, "assets": [ { "id": "1e31218a-e44e-4285-820c-8282ee222035", "name": "Bitcoin", "logo_url": "https://messari.io/asset-images/bitcoin.png" } ], "latest_development": { "id": "456001d9-ae84-4371-b99e-36d050e96a53", "title": "Bitcoin Core Release Candidate Version 31.0rc1 Testing Available", "summary": "A new release candidate of Bitcoin Core, version 31.0rc1, is now available for testing...", "category": ["launches_and_releases"], "subcategory": ["software_release"], "importance": "medium", "actionable": false, "created_at": "2026-03-17T16:10:25Z", "started_at": "2026-03-17T16:10:25Z", "ended_at": "2026-03-17T16:10:25Z" } }}
The CNS event ID. The same id is used across retries of a single delivery, and is shared across deliveries of the same event to multiple policies. Use it as an idempotency key on your side.
type
string
The payload type. For monitoring alerts this is always monitoring_event.
When you provide a webhook_secret, every webhook delivery includes an X-Webhook-Signature header. Verify this signature on every incoming request to confirm the webhook came from Messari and was not tampered with.
A valid HMAC alone is not enough — without a freshness check, an attacker who captures a legitimate webhook could replay it indefinitely. Reject any request whose t timestamp is more than a small tolerance away from your server’s current time. We recommend a tolerance of 5 minutes, which comfortably covers network delay, clock skew, and the retry window described in Retries.The helpers below combine both checks: they parse the header defensively (returning false on any missing or malformed component), enforce the timestamp tolerance, and then compare the HMAC in constant time.
const crypto = require("crypto");const SIGNATURE_TOLERANCE_SECONDS = 5 * 60;function verifyMessariSignature(rawBody, header, secret) { if (!header) return false; const parts = {}; for (const part of header.split(",")) { const [k, v] = part.split("="); if (k && v) parts[k.trim()] = v.trim(); } const timestamp = parts.t; const provided = parts.v1; if (!timestamp || !provided || !/^[a-f0-9]{64}$/i.test(provided)) { return false; } const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)); if (!Number.isFinite(age) || age > SIGNATURE_TOLERANCE_SECONDS) { return false; } const signedPayload = `${timestamp}.${rawBody}`; const expected = crypto .createHmac("sha256", secret) .update(signedPayload) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(expected, "hex"), Buffer.from(provided, "hex"), );}
Always verify against the raw request body bytes. If your framework re-serializes the JSON before you receive it, the bytes will not match what was signed and verification will fail.
Your endpoint should return an HTTP 2xx status code to acknowledge successful receipt. The HTTP client has a 30-second transport timeout, and each delivery attempt has a 1-minute overall timeout. Slow or non-2xx responses may trigger retries (see below).
Status code
Treated as
2xx
Success
429
Retryable (rate limit)
4xx (other)
Non-retryable failure
5xx
Retryable failure
network error / timeout
Retryable failure
We recommend returning 200 OK quickly after durably queuing the alert for processing on your side, rather than doing heavy work synchronously.
Always set a webhook_secret and verify the X-Webhook-Signature header on every request, including the timestamp freshness check to prevent replay attacks.
Use HTTPS for your webhook URL. Plain HTTP endpoints are accepted but strongly discouraged.
Make your handler idempotent. Retries reuse the same envelope id; use it as an idempotency key on your side to avoid double-processing the same event.
Respond quickly. Return 2xx as soon as you have durably queued the event; do downstream processing asynchronously.
Treat the body as authoritative. Do not trust query strings or other unsigned inputs.
This walkthrough builds a complete webhook handler that receives a Messari monitoring alert, verifies its signature, acknowledges receipt, and creates a Linear issue in the background. The same scaffolding works for any downstream destination — swap the Linear call for Slack, PagerDuty, an internal API, or a queue.The handler does five things in order:
Captures the raw request body (signatures are computed over raw bytes, not re-serialized JSON).
Verifies the X-Webhook-Signature header.
Returns 200 OK immediately so Messari isn’t waiting on Linear’s response.
Hands the envelope to a background worker.
Maps the alert into a Linear issueCreate mutation and posts it.
Linear exposes a GraphQL API; new issues are created with the issueCreate mutation. The transform takes a Messari monitoring_event envelope and produces an IssueCreateInput — mapping the development title to the issue title, building a Markdown description from the view, asset list, and development summary, and translating Messari’s importance to Linear’s priority enum.Two pieces of config are needed: a Linear API key for the Authorization header and the target team ID.
Linear personal API keys are passed directly in the Authorization header (no Bearer prefix). OAuth access tokens use Authorization: Bearer <token>.
Because Messari retries failed deliveries, the same envelope can arrive more than once. To avoid duplicate Linear issues, dedupe on envelope.id — store seen IDs in a short-TTL cache or include the ID in the issue description and search before creating. See Best Practices.