Y2 Elite workspaces are rolling out for teams
Y2Y2Docs
Delivery

Webhook Delivery

Deliver compact CloudEvents when intelligence reports are published

Webhook delivery sends a signed CloudEvents 1.0 notification to an HTTPS endpoint after a report is published. It is available to active Lite, Pro, and Elite workspaces.

A webhook notification is compact. It identifies the report and provides API links; it does not embed the full HTML report or all source records.

Create an endpoint in Y2

You do not need an API key when configuring webhooks in the app.

Open Webhooks

In an active paid workspace, go to Developers → Webhooks.

Create a configuration

Enter a name and public endpoint URL. Production webhook URLs must use HTTPS. Localhost, loopback, private-network, link-local, and other blocked addresses are rejected.

Add verification and authentication

Generate or enter an optional signing secret. You can also add custom string headers, such as an authorization token for your endpoint.

Host, Content-Length, and Content-Type cannot be supplied as custom headers. Y2's own delivery headers take precedence over custom values.

Save and test

Create the webhook, then select Test Webhook. The test sends a dev.y2.webhook.test.v1 CloudEvent and waits up to five seconds for a response.

Assign it to a profile

Go to InfoOps → My Profiles → Delivery Preferences, choose Webhook, select the active configuration, and save.

Selecting Webhook replaces email and SMS for that subscription. There is no combined Email + Webhook delivery method.

Receive a report event

Report deliveries use Content-Type: application/cloudevents+json and the event type dev.y2.report.generated.v1.

{
  "specversion": "1.0",
  "id": "y2mexampleeventid",
  "source": "https://api.y2.dev/api/v1/profiles/prf_0123456789abcdef01234567",
  "type": "dev.y2.report.generated.v1",
  "subject": "reports/rpt_0123456789abcdef01234567",
  "time": "2026-07-21T12:00:00.000Z",
  "datacontenttype": "application/json",
  "dataschema": "https://api.y2.dev/schemas/events/report-generated-v1.json",
  "data": {
    "report": {
      "id": "rpt_0123456789abcdef01234567",
      "profileId": "prf_0123456789abcdef01234567",
      "summary": "Concise report summary...",
      "status": "published",
      "generatedAt": "2026-07-21T11:59:48.000Z",
      "language": "en",
      "intelligence": {
        "signalCount": 4,
        "graphNodeCount": 12
      },
      "audio": {
        "status": "available",
        "durationSeconds": 318
      }
    },
    "subscription": {
      "id": "sub_0123456789abcdef01234567"
    },
    "links": {
      "report": "https://api.y2.dev/api/v1/reports/rpt_0123456789abcdef01234567",
      "markdown": "https://api.y2.dev/api/v1/reports/rpt_0123456789abcdef01234567?format=markdown",
      "sources": "https://api.y2.dev/api/v1/reports/rpt_0123456789abcdef01234567?include=sources",
      "signals": "https://api.y2.dev/api/v1/reports/rpt_0123456789abcdef01234567/signals",
      "graph": "https://api.y2.dev/api/v1/reports/rpt_0123456789abcdef01234567/graph",
      "audio": "https://api.y2.dev/api/v1/reports/rpt_0123456789abcdef01234567/audio"
    }
  }
}

summary can be null, and audio can be marked unavailable. Following a link can require an appropriately scoped API key or the endpoint's documented x402 flow.

Request headers

Prop

Type

Verify the signature

Compute HMAC-SHA256 over the exact raw request body bytes using the configured secret. Prefix the hex digest with sha256= and compare it with X-Y2-Signature in constant time.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyY2Webhook(rawBody, headers, secret) {
  const supplied = headers["x-y2-signature"] ?? "";
  const timestamp = Number(headers["x-y2-timestamp"] ?? 0);
  const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - timestamp);

  if (!timestamp || ageSeconds > 300) return false;

  const expected = `sha256=${createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex")}`;
  const suppliedBytes = Buffer.from(supplied);
  const expectedBytes = Buffer.from(expected);

  return (
    suppliedBytes.length === expectedBytes.length &&
    timingSafeEqual(suppliedBytes, expectedBytes)
  );
}

Use the raw bytes before JSON parsing; re-serializing an object can change the signed body. Keep your own replay window policy and deduplicate with Idempotency-Key or X-Y2-Event-Id.

A secret is optional in the configuration form. If you omit it, Y2 does not send X-Y2-Signature; secure the endpoint with a custom authorization header or another control.

Acknowledge deliveries safely

The production delivery request has a 10-second timeout. Return a 2xx response only after the event is durably accepted, then process long-running work asynchronously.

app.post("/y2/webhook", rawBodyMiddleware, async (request, response) => {
  if (!verifyY2Webhook(request.rawBody, request.headers, process.env.Y2_WEBHOOK_SECRET)) {
    return response.sendStatus(401);
  }

  await queue.put({
    id: request.headers["x-y2-event-id"],
    event: JSON.parse(request.rawBody.toString("utf8")),
  });

  return response.sendStatus(202);
});

Y2 does not run an independent automatic webhook retry loop. A timeout, network error, or non-2xx response increments the configuration's consecutive failure count. A successful delivery resets the count; five consecutive failures disable the webhook.

Manage configurations

  • Editing a masked webhook leaves its existing secret unchanged when the secret field is blank.
  • Activating a webhook resets its failure count.
  • An inactive webhook cannot be assigned for delivery.
  • A configuration cannot be deleted while any subscription references it. Reassign those profiles first.
  • The app exposes 30-day success/failure statistics, last-use time, subscription count, and current consecutive failures.

You can also manage configurations through the public API. API-key requests use the webhooks:manage scope; creating new API keys requires Pro or Elite, while webhook delivery itself is available on Lite.

Next steps