Skip to content
Documentation menu

API reference

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.

Note

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 routeDetail
Returnsendpoints: 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.
SeatA key on an admin seat or the owner's.
PlanEvery plan.
Rate limitNone on this route.
Engine budgetNothing this route does is metered.
curl "https://api.ansengine.co/webhooks" \
  -H "Authorization: Bearer ak_your_key"
StatusWhen
200endpoints: 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.
401No valid API key was sent.
403The key belongs to a seat below admin: a member's and a viewer's keys are refused.

POST /webhooks

Add a webhook endpoint.

Note

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 routeDetail
Returnsendpoint, and secret: the endpoint's signing secret (whsec_...). It is returned this once and never again; rotate it to get a new one.
SeatA key on an admin seat or the owner's.
PlanEvery plan.
Rate limit30 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 budgetNothing 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
StatusWhen
200endpoint, and secret: the endpoint's signing secret (whsec_...). It is returned this once and never again; rotate it to get a new one.
400The body did not match, or the address is not an https address on a public host.
401No valid API key was sent.
403The key belongs to a seat below admin: a member's and a viewer's keys are refused.
404brandId is not a client of this workspace.
409The workspace already holds 10 endpoints, or another endpoint here already posts to this address.
429Over 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.
503This 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 routeDetail
Returnsendpoint, as listed. A new address keeps the secret; switching an endpoint on clears a switch off.
SeatA key on an admin seat or the owner's.
PlanEvery plan.
Rate limit30 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 budgetNothing this route does is metered.
ParameterTypeWhat it is
id (path, required)uuidThe 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
StatusWhen
200endpoint, as listed. A new address keeps the secret; switching an endpoint on clears a switch off.
400An id in the path is not a valid id, or the body did not match.
401No valid API key was sent.
403The key belongs to a seat below admin: a member's and a viewer's keys are refused.
404No such endpoint in this workspace, or brandId is not one of its clients.
409Another endpoint here already posts to this address.
429Over 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.
503A new address cannot be stored: this deployment holds no encryption key.

DELETE /webhooks/{id}

Remove a webhook endpoint and its delivery log.

About this routeDetail
Returnsok: true. Deliveries still queued for it stop.
SeatA key on an admin seat or the owner's.
PlanEvery plan.
Rate limit30 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 budgetNothing this route does is metered.
ParameterTypeWhat it is
id (path, required)uuidThe id id.
curl -X DELETE "https://api.ansengine.co/webhooks/{id}" \
  -H "Authorization: Bearer ak_your_key"
StatusWhen
200ok: true. Deliveries still queued for it stop.
400An id in the path is not a valid id.
401No valid API key was sent.
403The key belongs to a seat below admin: a member's and a viewer's keys are refused.
404No such endpoint in this workspace.
429Over 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 routeDetail
Returnsendpoint, and secret: the new signing secret, returned this once. The old secret stops verifying at once.
SeatA key on an admin seat or the owner's.
PlanEvery plan.
Rate limit30 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 budgetNothing this route does is metered.
ParameterTypeWhat it is
id (path, required)uuidThe id id.
curl -X POST "https://api.ansengine.co/webhooks/{id}/rotate-secret" \
  -H "Authorization: Bearer ak_your_key"
StatusWhen
200endpoint, and secret: the new signing secret, returned this once. The old secret stops verifying at once.
400An id in the path is not a valid id.
401No valid API key was sent.
403The key belongs to a seat below admin: a member's and a viewer's keys are refused.
404No such endpoint in this workspace.
429Over 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.
503This deployment holds no encryption key.

POST /webhooks/{id}/test

Send a test event (ping) to an endpoint now.

About this routeDetail
Returnsok, 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.
SeatA key on an admin seat or the owner's.
PlanEvery plan.
Rate limit30 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 budgetNothing this route does is metered.
ParameterTypeWhat it is
id (path, required)uuidThe id id.
curl -X POST "https://api.ansengine.co/webhooks/{id}/test" \
  -H "Authorization: Bearer ak_your_key"
StatusWhen
200ok, 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.
400An id in the path is not a valid id.
401No valid API key was sent.
403The key belongs to a seat below admin: a member's and a viewer's keys are refused.
404No such endpoint in this workspace.
429Over 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.
503This deployment does not post webhooks.

GET /webhooks/{id}/deliveries

Read an endpoint's last 50 deliveries.

About this routeDetail
Returnsdeliveries, 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.
SeatA key on an admin seat or the owner's.
PlanEvery plan.
Rate limitNone on this route.
Engine budgetNothing this route does is metered.
ParameterTypeWhat it is
id (path, required)uuidThe id id.
curl "https://api.ansengine.co/webhooks/{id}/deliveries" \
  -H "Authorization: Bearer ak_your_key"
StatusWhen
200deliveries, 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.
400An id in the path is not a valid id.
401No valid API key was sent.
403The key belongs to a seat below admin: a member's and a viewer's keys are refused.
404No 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);
}