Webhooks
Post the workspace's events to n8n, Zapier, Make or your own server: up to 10 https endpoints, each signed with its own secret, retried for a day and logged. A post can arrive twice: dedupe on the event id each one carries.
GET /webhooks
List the workspace's webhook endpoints.
The address can be a capability URL, so only its host is ever returned. The bodies each event posts, and how to check their signature, are under Events below.
| About this route | Detail |
|---|---|
| Returns | endpoints: each with its id, name, host, events, brandId (null for every client), on, offAt (set when it switched itself off after 3 days of failed deliveries) and its last 30 days counted as delivered, failed and pending. Also events (each type with its meaning) and max (10). Never an address or a secret. |
| Seat | A key on an admin seat or the owner's. |
| Plan | Every plan. |
| Rate limit | None on this route. |
| Engine budget | Nothing this route does is metered. |
curl "https://api.ansengine.co/webhooks" \
-H "Authorization: Bearer ak_your_key"| Status | When |
|---|---|
| 200 | endpoints: each with its id, name, host, events, brandId (null for every client), on, offAt (set when it switched itself off after 3 days of failed deliveries) and its last 30 days counted as delivered, failed and pending. Also events (each type with its meaning) and max (10). Never an address or a secret. |
| 401 | No valid API key was sent. |
| 403 | The key belongs to a seat below admin: a member's and a viewer's keys are refused. |
POST /webhooks
Add a webhook endpoint.
The address must be https on a public host, with no port and no credentials. Every post checks it again: an address that resolves to private, loopback or link local space is refused, and no redirect is followed.
| About this route | Detail |
|---|---|
| Returns | endpoint, and secret: the endpoint's signing secret (whsec_...). It is returned this once and never again; rotate it to get a new one. |
| Seat | A key on an admin seat or the owner's. |
| Plan | Every plan. |
| Rate limit | 30 requests a minute for the workspace, counted across every write that takes this limit, the console's included. A seat limited to named clients gets 15 of its own. |
| Engine budget | Nothing this route does is metered. |
The body is JSON. Its schema, taken from the route's own validation:
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 60
},
"url": {
"type": "string",
"minLength": 1,
"maxLength": 500
},
"events": {
"minItems": 1,
"maxItems": 6,
"type": "array",
"items": {
"type": "string",
"enum": [
"lead.created",
"pitch.won",
"alert.fired",
"report.sent",
"action.completed",
"measurement.completed"
]
}
},
"brandId": {
"anyOf": [
{
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
{
"type": "null"
}
]
}
},
"required": [
"name",
"url",
"events"
],
"additionalProperties": false
}curl -X POST "https://api.ansengine.co/webhooks" \
-H "Authorization: Bearer ak_your_key" \
-H "Content-Type: application/json" \
--data @body.json| Status | When |
|---|---|
| 200 | endpoint, and secret: the endpoint's signing secret (whsec_...). It is returned this once and never again; rotate it to get a new one. |
| 400 | The body did not match, or the address is not an https address on a public host. |
| 401 | No valid API key was sent. |
| 403 | The key belongs to a seat below admin: a member's and a viewer's keys are refused. |
| 404 | brandId is not a client of this workspace. |
| 409 | The workspace already holds 10 endpoints, or another endpoint here already posts to this address. |
| 429 | Over the rate limit: 30 requests a minute for the workspace, counted across every write that takes this limit, the console's included. A seat limited to named clients gets 15 of its own. The Retry-After header says how many seconds to wait. |
| 503 | This deployment holds no encryption key, so no address or secret can be stored. |
PATCH /webhooks/{id}
Change, switch on or switch off a webhook endpoint.
| About this route | Detail |
|---|---|
| Returns | endpoint, as listed. A new address keeps the secret; switching an endpoint on clears a switch off. |
| Seat | A key on an admin seat or the owner's. |
| Plan | Every plan. |
| Rate limit | 30 requests a minute for the workspace, counted across every write that takes this limit, the console's included. A seat limited to named clients gets 15 of its own. |
| Engine budget | Nothing this route does is metered. |
| Parameter | Type | What it is |
|---|---|---|
| id (path, required) | uuid | The id id. |
The body is JSON. Its schema, taken from the route's own validation:
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 60
},
"url": {
"type": "string",
"minLength": 1,
"maxLength": 500
},
"events": {
"minItems": 1,
"maxItems": 6,
"type": "array",
"items": {
"type": "string",
"enum": [
"lead.created",
"pitch.won",
"alert.fired",
"report.sent",
"action.completed",
"measurement.completed"
]
}
},
"brandId": {
"anyOf": [
{
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
{
"type": "null"
}
]
},
"on": {
"type": "boolean"
}
},
"additionalProperties": false
}curl -X PATCH "https://api.ansengine.co/webhooks/{id}" \
-H "Authorization: Bearer ak_your_key" \
-H "Content-Type: application/json" \
--data @body.json| Status | When |
|---|---|
| 200 | endpoint, as listed. A new address keeps the secret; switching an endpoint on clears a switch off. |
| 400 | An id in the path is not a valid id, or the body did not match. |
| 401 | No valid API key was sent. |
| 403 | The key belongs to a seat below admin: a member's and a viewer's keys are refused. |
| 404 | No such endpoint in this workspace, or brandId is not one of its clients. |
| 409 | Another endpoint here already posts to this address. |
| 429 | Over the rate limit: 30 requests a minute for the workspace, counted across every write that takes this limit, the console's included. A seat limited to named clients gets 15 of its own. The Retry-After header says how many seconds to wait. |
| 503 | A new address cannot be stored: this deployment holds no encryption key. |
DELETE /webhooks/{id}
Remove a webhook endpoint and its delivery log.
| About this route | Detail |
|---|---|
| Returns | ok: true. Deliveries still queued for it stop. |
| Seat | A key on an admin seat or the owner's. |
| Plan | Every plan. |
| Rate limit | 30 requests a minute for the workspace, counted across every write that takes this limit, the console's included. A seat limited to named clients gets 15 of its own. |
| Engine budget | Nothing this route does is metered. |
| Parameter | Type | What it is |
|---|---|---|
| id (path, required) | uuid | The id id. |
curl -X DELETE "https://api.ansengine.co/webhooks/{id}" \
-H "Authorization: Bearer ak_your_key"| Status | When |
|---|---|
| 200 | ok: true. Deliveries still queued for it stop. |
| 400 | An id in the path is not a valid id. |
| 401 | No valid API key was sent. |
| 403 | The key belongs to a seat below admin: a member's and a viewer's keys are refused. |
| 404 | No such endpoint in this workspace. |
| 429 | Over the rate limit: 30 requests a minute for the workspace, counted across every write that takes this limit, the console's included. A seat limited to named clients gets 15 of its own. The Retry-After header says how many seconds to wait. |
POST /webhooks/{id}/rotate-secret
Rotate an endpoint's signing secret.
| About this route | Detail |
|---|---|
| Returns | endpoint, and secret: the new signing secret, returned this once. The old secret stops verifying at once. |
| Seat | A key on an admin seat or the owner's. |
| Plan | Every plan. |
| Rate limit | 30 requests a minute for the workspace, counted across every write that takes this limit, the console's included. A seat limited to named clients gets 15 of its own. |
| Engine budget | Nothing this route does is metered. |
| Parameter | Type | What it is |
|---|---|---|
| id (path, required) | uuid | The id id. |
curl -X POST "https://api.ansengine.co/webhooks/{id}/rotate-secret" \
-H "Authorization: Bearer ak_your_key"| Status | When |
|---|---|
| 200 | endpoint, and secret: the new signing secret, returned this once. The old secret stops verifying at once. |
| 400 | An id in the path is not a valid id. |
| 401 | No valid API key was sent. |
| 403 | The key belongs to a seat below admin: a member's and a viewer's keys are refused. |
| 404 | No such endpoint in this workspace. |
| 429 | Over the rate limit: 30 requests a minute for the workspace, counted across every write that takes this limit, the console's included. A seat limited to named clients gets 15 of its own. The Retry-After header says how many seconds to wait. |
| 503 | This deployment holds no encryption key. |
POST /webhooks/{id}/test
Send a test event (ping) to an endpoint now.
| About this route | Detail |
|---|---|
| Returns | ok, delivered (a 2xx came back), statusCode (null when no response came back), note (why it failed) and eventId. The ping is logged as one delivery and never retried. |
| Seat | A key on an admin seat or the owner's. |
| Plan | Every plan. |
| Rate limit | 30 requests a minute for the workspace, counted across every write that takes this limit, the console's included. A seat limited to named clients gets 15 of its own. |
| Engine budget | Nothing this route does is metered. |
| Parameter | Type | What it is |
|---|---|---|
| id (path, required) | uuid | The id id. |
curl -X POST "https://api.ansengine.co/webhooks/{id}/test" \
-H "Authorization: Bearer ak_your_key"| Status | When |
|---|---|
| 200 | ok, delivered (a 2xx came back), statusCode (null when no response came back), note (why it failed) and eventId. The ping is logged as one delivery and never retried. |
| 400 | An id in the path is not a valid id. |
| 401 | No valid API key was sent. |
| 403 | The key belongs to a seat below admin: a member's and a viewer's keys are refused. |
| 404 | No such endpoint in this workspace. |
| 429 | Over the rate limit: 30 requests a minute for the workspace, counted across every write that takes this limit, the console's included. A seat limited to named clients gets 15 of its own. The Retry-After header says how many seconds to wait. |
| 503 | This deployment does not post webhooks. |
GET /webhooks/{id}/deliveries
Read an endpoint's last 50 deliveries.
| About this route | Detail |
|---|---|
| Returns | deliveries, newest first, from the last 30 days: eventId, eventType, status (pending, sending, retrying, delivered or failed), attempts, statusCode, note, createdAt, lastAttemptAt and deliveredAt. No event body is kept. |
| Seat | A key on an admin seat or the owner's. |
| Plan | Every plan. |
| Rate limit | None on this route. |
| Engine budget | Nothing this route does is metered. |
| Parameter | Type | What it is |
|---|---|---|
| id (path, required) | uuid | The id id. |
curl "https://api.ansengine.co/webhooks/{id}/deliveries" \
-H "Authorization: Bearer ak_your_key"| Status | When |
|---|---|
| 200 | deliveries, newest first, from the last 30 days: eventId, eventType, status (pending, sending, retrying, delivered or failed), attempts, statusCode, note, createdAt, lastAttemptAt and deliveredAt. No event body is kept. |
| 400 | An id in the path is not a valid id. |
| 401 | No valid API key was sent. |
| 403 | The key belongs to a seat below admin: a member's and a viewer's keys are refused. |
| 404 | No such endpoint in this workspace. |
Events
7 bodies, one per event. Each is POSTed as JSON to every endpoint that asked for it.
Event: lead.created
A lead form lead lands The business the lead form checked, its website, market and prompts, and where the agency reads its leads. Never the visitor's email or phone. Signed: x-ansengine-signature is v1= an HMAC SHA256 over "<x-ansengine-timestamp>.<raw body>" with the endpoint's secret. Delivered at least once: a post can repeat after a retry, so dedupe on x-ansengine-event-id (the body's id).
{
"id": "evt_340afa2de176f6de831f96941da92253",
"v": 1,
"type": "lead.created",
"createdAt": "2026-10-17T09:00:00.000Z",
"workspace": {
"id": "0f8fad5b-d9cb-469f-a165-70867728950e"
},
"client": null,
"data": {
"leadId": "9b2f3c1e-8d4a-4f6b-9c7e-1a2b3c4d5e6f",
"business": "Brightline Roofing",
"website": "https://brightlineroofing.example",
"market": "Austin, TX",
"questions": [
"best roofer in Austin TX",
"roof repair Austin TX recommendations"
],
"leadsUrl": "https://ansengine.co/app/acquire#leads"
}
}Event: pitch.won
A pitch is marked won The pitch that became a client: its name and domain. client is the new client. Signed: x-ansengine-signature is v1= an HMAC SHA256 over "<x-ansengine-timestamp>.<raw body>" with the endpoint's secret. Delivered at least once: a post can repeat after a retry, so dedupe on x-ansengine-event-id (the body's id).
{
"id": "evt_156b3edd9f26da5eb6652d7592bf9b61",
"v": 1,
"type": "pitch.won",
"createdAt": "2026-10-17T09:00:00.000Z",
"workspace": {
"id": "0f8fad5b-d9cb-469f-a165-70867728950e"
},
"client": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Acme Dental"
},
"data": {
"pitchId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"business": "Acme Dental",
"domain": "acmedental.example",
"clientUrl": "https://ansengine.co/app/overview"
}
}Event: alert.fired
A client's alert fires A rate alert (a mention, citation or support rate that fell or crossed its floor) or an event alert (a new competitor, a page dropped), with the sentence the alert email says. Signed: x-ansengine-signature is v1= an HMAC SHA256 over "<x-ansengine-timestamp>.<raw body>" with the endpoint's secret. Delivered at least once: a post can repeat after a retry, so dedupe on x-ansengine-event-id (the body's id).
{
"id": "evt_44ed3a517530dbba0d03dbd55a223690",
"v": 1,
"type": "alert.fired",
"createdAt": "2026-10-17T09:00:00.000Z",
"workspace": {
"id": "0f8fad5b-d9cb-469f-a165-70867728950e"
},
"client": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Acme Dental"
},
"data": {
"alertKind": "rate",
"rule": "regression",
"ruleId": "1e7d2c4b-3a5f-4e6d-8c9b-0a1b2c3d4e5f",
"summary": "Acme Dental: mention rate fell from 9 of 20 answers to 3 of 20",
"alertsUrl": "https://ansengine.co/app/settings?section=notifications"
}
}Event: report.sent
A scheduled client report is sent How many addresses the scheduled client report went to, never which, and its cadence. Signed: x-ansengine-signature is v1= an HMAC SHA256 over "<x-ansengine-timestamp>.<raw body>" with the endpoint's secret. Delivered at least once: a post can repeat after a retry, so dedupe on x-ansengine-event-id (the body's id).
{
"id": "evt_2977b7092500caa2a943a5ebea37ca91",
"v": 1,
"type": "report.sent",
"createdAt": "2026-10-17T09:00:00.000Z",
"workspace": {
"id": "0f8fad5b-d9cb-469f-a165-70867728950e"
},
"client": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Acme Dental"
},
"data": {
"recipients": 2,
"cadence": "weekly",
"reportsUrl": "https://ansengine.co/app/reports"
}
}Event: action.completed
An action is completed Work marked live from Actions or Impact, and the intervention whose measurement starts. Signed: x-ansengine-signature is v1= an HMAC SHA256 over "<x-ansengine-timestamp>.<raw body>" with the endpoint's secret. Delivered at least once: a post can repeat after a retry, so dedupe on x-ansengine-event-id (the body's id).
{
"id": "evt_34ecf5d22821abc126b923f2ad5ab976",
"v": 1,
"type": "action.completed",
"createdAt": "2026-10-17T09:00:00.000Z",
"workspace": {
"id": "0f8fad5b-d9cb-469f-a165-70867728950e"
},
"client": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Acme Dental"
},
"data": {
"actionKey": "fix:pricing-page",
"interventionId": "4f3e2d1c-0b9a-4876-9543-210fedcba987",
"title": "Add pricing to the services page",
"actionsUrl": "https://ansengine.co/app/actions?package=fix%3Apricing-page"
}
}Event: measurement.completed
A client's measurement finishes The client's answers to date as the Overview counts them: answers, the answers that mention it, the answers that cite it, and the prompts. Signed: x-ansengine-signature is v1= an HMAC SHA256 over "<x-ansengine-timestamp>.<raw body>" with the endpoint's secret. Delivered at least once: a post can repeat after a retry, so dedupe on x-ansengine-event-id (the body's id).
{
"id": "evt_2f7262152f8489df1a57257938b4978a",
"v": 1,
"type": "measurement.completed",
"createdAt": "2026-10-17T09:00:00.000Z",
"workspace": {
"id": "0f8fad5b-d9cb-469f-a165-70867728950e"
},
"client": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Acme Dental"
},
"data": {
"period": "2026-10-17",
"answers": 120,
"mentioned": 31,
"cited": 12,
"prompts": 40,
"overviewUrl": "https://ansengine.co/app/overview"
}
}Event: ping
The test event. The test event Send test event posts. Never retried. Signed: x-ansengine-signature is v1= an HMAC SHA256 over "<x-ansengine-timestamp>.<raw body>" with the endpoint's secret. Delivered at least once: a post can repeat after a retry, so dedupe on x-ansengine-event-id (the body's id).
{
"id": "evt_33d987919d4f030e0b373875e4ccc6a5",
"v": 1,
"type": "ping",
"createdAt": "2026-10-17T09:00:00.000Z",
"workspace": {
"id": "0f8fad5b-d9cb-469f-a165-70867728950e"
},
"client": null,
"data": {
"message": "Test event from AnsEngine. Your endpoint is reachable."
}
}Checking the signature
Check every post before you trust it. In Node:
import { createHmac, timingSafeEqual } from "node:crypto";
// body: the raw request body as a string, before any JSON parsing.
export function verifyAnsEngine(body, headers, secret) {
const ts = headers["x-ansengine-timestamp"];
const sig = String(headers["x-ansengine-signature"] ?? "");
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const want = "v1=" + createHmac("sha256", secret).update(ts + "." + body).digest("hex");
// Compared as bytes: a signature with a character outside ASCII is longer in
// bytes than in letters, and timingSafeEqual throws on a length mismatch.
const a = Buffer.from(sig);
const b = Buffer.from(want);
return a.length === b.length && timingSafeEqual(a, b);
}