Configure Report Webhooks
Create a signed webhook, attach it to a subscription, and process CloudEvents safely
Use a webhook when your application should react as soon as Y2 publishes a report. This guide covers configuration and consumption through the public API. For the delivery product overview, see Webhook delivery.
Before you begin
You need:
- a paid workspace plan with webhook delivery;
- an API key with
webhooks:manage; - a public HTTPS endpoint that accepts
POSTrequests; and - a secret generated and stored by your application if you want signed deliveries.
Y2 rejects loopback, private, link-local, cloud metadata, multicast, and other reserved network
destinations. In production, webhook URLs must use HTTPS. Custom headers must have string values;
Host, Content-Length, and Content-Type cannot be configured.
1. Create a webhook configuration
Send a stable Idempotency-Key when a client may retry the request:
curl --request POST "https://api.y2.dev/api/v1/webhooks" \
--header "Authorization: Bearer $Y2_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: create-primary-webhook-01" \
--data '{
"name": "Primary report receiver",
"url": "https://example.com/webhooks/y2",
"secret": "replace-with-a-random-secret",
"headers": {
"X-Integration": "intelligence-pipeline"
}
}'A successful request returns 201, a canonical whk_... ID, Location, and ETag. Store the ID;
you need it to test the endpoint and attach it to a subscription.
Secrets and headers are write-only
List and mutation responses reveal whether signing is configured and return custom header names, but never return the secret or custom header values.
2. Test the receiver
curl --request POST "https://api.y2.dev/api/v1/webhooks/$WEBHOOK_ID/test" \
--header "Authorization: Bearer $Y2_API_KEY"The test request is a CloudEvents 1.0 event with type dev.y2.webhook.test.v1. Y2 waits up to five
seconds. A 2xx response from your receiver produces 200; a timeout, network error, or non-2xx
response produces 422.
The test verifies connectivity and signing, but it does not attach the configuration to a report subscription.
3. Attach the webhook to a subscription
Use the subscription's canonical sub_... ID, not its profile ID:
curl --request PATCH \
"https://api.y2.dev/api/v1/subscriptions/$SUBSCRIPTION_ID/delivery" \
--header "Authorization: Bearer $Y2_API_KEY" \
--header "Content-Type: application/json" \
--data "{\"deliveryMethod\":\"webhook\",\"webhookConfigId\":\"$WEBHOOK_ID\"}"The webhook must be active and belong to the same user or workspace scope as the subscription. Changing a profile does not change its subscription delivery settings.
4. Process the report event
Production deliveries use structured CloudEvents with media type
application/cloudevents+json and event type dev.y2.report.generated.v1:
{
"specversion": "1.0",
"id": "y2mexamplelogicalevent01",
"source": "https://api.y2.dev/api/v1/profiles/prf_0123456789abcdef01234567",
"type": "dev.y2.report.generated.v1",
"subject": "reports/rpt_0123456789abcdef01234567",
"time": "2026-07-21T18: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": "A concise report summary.",
"status": "published",
"generatedAt": "2026-07-21T17:59:42.000Z",
"language": "en",
"intelligence": { "signalCount": 3, "graphNodeCount": 12 },
"audio": { "status": "available", "durationSeconds": 284 }
},
"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"
}
}
}The event is intentionally compact. Use the supplied links and a key with the relevant Reports scope to retrieve full Markdown, sources, signals, graph, or audio.
Delivery headers
| Header | Meaning |
|---|---|
Content-Type | application/cloudevents+json |
User-Agent | Y2-Webhook-Delivery/3.0 |
X-Y2-Timestamp | Attempt time as Unix seconds |
X-Y2-Event-Id | Stable logical event ID; matches the body id |
X-Y2-Attempt-Id | Unique transport-attempt ID |
X-Y2-Attempt | One-based attempt number |
Idempotency-Key | Stable logical event ID |
X-Y2-Signature | sha256=<hex> when a secret is configured |
Configured custom headers are sent first. Y2's delivery headers take precedence if names collide.
Verify the signature
Compute HMAC-SHA256 over the exact raw request bytes. Do not parse and reserialize the JSON before verification.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyY2Webhook(
rawBody: Buffer,
signatureHeader: string | null,
secret: string,
): boolean {
if (!signatureHeader) return false;
const expected = Buffer.from(
`sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`,
);
const actual = Buffer.from(signatureHeader);
return actual.length === expected.length && timingSafeEqual(actual, expected);
}Also compare X-Y2-Timestamp with your current time and reject values outside your replay window;
five minutes is a practical default. Only return 2xx after the event has been durably accepted.
Handle attempts and failures
The event body and X-Y2-Event-Id remain stable if the same logical delivery is attempted again.
X-Y2-Attempt-Id and X-Y2-Attempt identify the transport attempt. Deduplicate on the body id,
X-Y2-Event-Id, or Idempotency-Key before starting downstream work.
Production delivery times out after 10 seconds. Y2 does not run an independent HTTP retry loop for a failed attempt, although a replayed delivery workflow can attempt the same persisted logical event again. Five consecutive failed attempts automatically disable the webhook; a successful attempt resets the failure counter.
GET /webhooks exposes status, deliveryHealth.consecutiveFailures, and
deliveryHealth.lastUsedAt. To re-enable a destination, replace its complete configuration with
PUT and set isActive to true, then test it again.
Replace or delete a configuration
PUT /webhooks/{webhookId} replaces the complete mutable configuration. name and url are
required, and omitted optional fields reset to defaults. Send the latest returned ETag in
If-Match when you have one to prevent overwriting a concurrent change.
Before deleting a webhook, move every attached subscription to another delivery method or webhook.
Deletion returns 409 WEBHOOK_IN_USE while any subscription still references the configuration;
a successful deletion returns 204 with no body.