Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.messari.io/llms.txt

Use this file to discover all available pages before exploring further.

Webhooks Overview

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.

Setting Up a Webhook

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.

Fields

  • 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.

Request Format

Each webhook is delivered as an HTTP POST with the following headers:
HeaderValue
Content-Typeapplication/json
User-AgentMessariCNS/1.0
X-Webhook-SignatureHMAC 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"
    }
  }
}

Envelope Fields

FieldTypeDescription
idstring (UUID)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.
typestringThe payload type. For monitoring alerts this is always monitoring_event.
timestampstring (RFC 3339)When the event was created in Messari.
dataobjectThe event payload — see below.

Data Fields

FieldTypeRequiredDescription
viewobjectyesThe monitoring view that matched (id, name).
eventobjectyesThe underlying Messari event the development belongs to (id, title).
assetsarrayoptionalAssets extracted from the development (id, name, logo_url). Omitted when empty.
latest_developmentobjectyesThe development that triggered the alert — see fields below.

latest_development fields

FieldTypeRequiredDescription
idstring (UUID)yesDevelopment ID.
titlestringyesDevelopment title.
summarystringyesDevelopment summary (Markdown).
categoryarray of stringoptionalCategory taxonomy tags. Omitted when empty.
subcategoryarray of stringoptionalSubcategory taxonomy tags. Omitted when empty.
importancestringoptionalOne of low, medium, high. Omitted when the development hasn’t been classified.
actionableboolyesWhether the development is marked actionable. Always present.
created_atstring (RFC 3339)yesWhen the development started. Same value as started_at — included for compatibility.
started_atstring (RFC 3339)yesWhen the development started.
ended_atstring (RFC 3339)optionalWhen the development ended. Omitted for ongoing developments.

Verifying Signatures

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.

Header Format

The signature header uses a Stripe-style format:
X-Webhook-Signature: t=<unix_timestamp>,v1=<hex_hmac>
  • t — the Unix timestamp (seconds) at which the signature was generated
  • v1 — the HMAC-SHA256 signature as a hex string

Computing the Expected Signature

The signed payload is the concatenation of the timestamp, a literal ., and the raw request body:
signed_payload = "{timestamp}.{raw_body}"
expected_sig   = HMAC_SHA256(webhook_secret, signed_payload)
Compare your computed expected_sig against the v1 value from the header using a constant-time comparison. If they match, the request is authentic.

Rejecting Stale Signatures

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.

Responding to Webhooks

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 codeTreated as
2xxSuccess
429Retryable (rate limit)
4xx (other)Non-retryable failure
5xxRetryable failure
network error / timeoutRetryable failure
We recommend returning 200 OK quickly after durably queuing the alert for processing on your side, rather than doing heavy work synchronously.

Retries

When a webhook delivery fails with a retryable status code, network error, or timeout, Messari automatically retries with exponential backoff:
  • Maximum attempts: 5
  • Initial interval: 1 second
  • Backoff coefficient: 2×
  • Maximum interval: 5 minutes
Non-retryable 4xx responses (excluding 429) are not retried — if your endpoint returns 400 Bad Request, the delivery is permanently marked as failed.

Best Practices

  • 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.

Example: Creating Tickets on Linear

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:
  1. Captures the raw request body (signatures are computed over raw bytes, not re-serialized JSON).
  2. Verifies the X-Webhook-Signature header.
  3. Returns 200 OK immediately so Messari isn’t waiting on Linear’s response.
  4. Hands the envelope to a background worker.
  5. Maps the alert into a Linear issueCreate mutation and posts it.

The Server

Instead of running a server, you can also use no-code software like Zapier or n8n to handle and transform Messari’s webhook requests.
const express = require("express");
const app = express();

const SECRET = process.env.MESSARI_WEBHOOK_SECRET;

// Capture the raw body so signature verification matches exactly what was signed
app.post(
  "/messari/webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.header("X-Webhook-Signature") ?? "";
    if (!verifyMessariSignature(req.body, signature, SECRET)) {
      return res.status(401).send("invalid signature");
    }

    const envelope = JSON.parse(req.body.toString("utf8"));

    // Ack immediately — process in the background
    res.status(200).send("ok");
    setImmediate(() => forwardToLinear(envelope).catch(console.error));
  }
);

app.listen(3000);
verifyMessariSignature and verify_messari_signature are the helpers from Verifying Signatures above.

Transforming and Posting to Linear

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.
const LINEAR_API_KEY = process.env.LINEAR_API_KEY;
const LINEAR_TEAM_ID = process.env.LINEAR_TEAM_ID;

// Linear priority enum: 0 none, 1 urgent, 2 high, 3 medium, 4 low
const PRIORITY = { high: 2, medium: 3, low: 4 };

async function forwardToLinear(envelope) {
  if (envelope.type !== "monitoring_event") return;

  const { view, latest_development: dev, assets = [] } = envelope.data;
  const assetList = assets.map((a) => a.name).join(", ") || "—";

  const input = {
    teamId: LINEAR_TEAM_ID,
    title: `[${view.name}] ${dev.title}`,
    description: [
      `**View:** ${view.name}`,
      `**Assets:** ${assetList}`,
      `**Detected:** ${dev.created_at}`,
      "",
      dev.summary,
    ].join("\n"),
    priority: PRIORITY[dev.importance] ?? 0,
  };

  const res = await fetch("https://api.linear.app/graphql", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: LINEAR_API_KEY,
    },
    body: JSON.stringify({
      query: `mutation($input: IssueCreateInput!) {
        issueCreate(input: $input) { success issue { identifier url } }
      }`,
      variables: { input },
    }),
  });

  if (!res.ok) throw new Error(`Linear ${res.status}: ${await res.text()}`);
}
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.