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.