Webhooks
Get a signed HTTPS POST from Fovea when creatives land, finish AI analysis, collections are created or a Meta sync completes.
Add an endpoint in Settings > Integrations > Webhooks (workspace Admins, paid plans). Pick the events, then copy the signing secret from the dialog: it is shown once.
Events
| Event | When |
|---|---|
asset.created | A creative finished uploading to the library |
asset.analyzed | AI analysis finished for a creative (also after a re-analysis) |
collection.created | A collection was created in the app, through the API, over MCP or by Fovi |
ad_sync.completed | A Meta ads sync finished for the workspace |
Send test delivers a webhook.test event straight away so you can check your receiver.
Payload
{
"id": "4b1f0c8e-...",
"type": "asset.analyzed",
"created_at": "2026-10-21T14:03:11+00:00",
"workspace_id": "0b5e...",
"data": {
"id": "8f2c...", "name": "UGC hook v3", "media_type": "video",
"tags": ["ugc", "summer"], "analysis_status": "completed",
"hook": "Stop scrolling", "angle": "Problem solution", "style": "UGC",
"url": "https://foveahq.com/library/8f2c...", "created_at": "2026-10-21T13:58:40"
}
}
Webhook payloads never include spend, revenue or ROAS. id is the event id: the same event sent to two endpoints shares it, and a retry repeats it, so use it to ignore duplicates.
Headers and signature
| Header | Value |
|---|---|
Fovea-Event | The event type |
Fovea-Event-Id | The event id |
Fovea-Delivery | This delivery's id |
Fovea-Signature | t=<unix seconds>,v1=<hex> |
v1 is an HMAC-SHA256 of t, a dot, and the raw request body, keyed with your signing secret. Compare it in constant time and reject timestamps older than five minutes:
import crypto from "node:crypto";
function verify(rawBody, header, secret, toleranceSec = 300) {
const items = header.split(",").map((p) => p.split("="));
const t = items.find(([k]) => k === "t")?.[1];
const sigs = items.filter(([k]) => k === "v1").map(([, v]) => v);
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return sigs.some((s) => s.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}
Retries
Answer with any 2xx within 10 seconds. Anything else is retried after 30 seconds, 2 minutes, 10 minutes and 1 hour (5 attempts), then marked failed. Redirects are not followed. After 15 failed deliveries in a row the endpoint is turned off; fix it and switch it back on. Every attempt, with your response code and the first 1,000 characters of the body, is in Settings > Integrations > Event log, where a failed delivery can be sent again.
Rotating the secret
Rotate secret issues a new one. For 24 hours each delivery carries two v1= values (new and old secret), so you can deploy the new secret without dropping events.
Endpoint rules
Endpoints must be public https:// URLs. Addresses inside private networks, localhost and cloud metadata addresses are refused, both when you save the endpoint and on every delivery.