Webhooks
Subscribe to Rezolve Ai events, verify deliveries, and handle retries.
Webhooks let Rezolve Ai notify your systems when something happens, so you do not have to poll the API. You register an HTTPS endpoint, choose the events you care about, and Rezolve Ai posts a signed JSON payload to that endpoint whenever a matching event fires.
Create a subscription
Send a POST /api/v1/webhooks request with the endpoint URL and the events to subscribe to. The URL must start with https://, and events needs at least one entry.
Your key needs the webhooks:write scope to create, update, or delete a subscription, and webhooks:read to list subscriptions and read their deliveries. Neither is part of the default selection, so tick them when you create the key. See API Keys.
curl -X POST "https://ace.authoritas.com/api/v1/webhooks" \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/rezolve",
"events": ["enrichment.job.succeeded", "enrichment.job.partial", "enrichment.job.failed"],
"description": "Production listener"
}'The 201 response holds the subscription, including its secret. Store the secret now: it is returned only here, and later reads show [hidden] in its place. You use it to verify that incoming deliveries really came from Rezolve Ai. See the Webhooks API reference for the full request and response shapes.
An event name that Rezolve Ai does not recognise is rejected with 400, and the message says which one, on create and on update. Subscribe to the events in the table below.
From an AI assistant, the create_webhook MCP tool creates the same kind of subscription for the job and article events. It needs a key with webhooks:write.
Events you can subscribe to
| Event | Sent when |
|---|---|
enrichment.job.succeeded | A rules, content, pipeline, or lookup job finished and every item succeeded. |
enrichment.job.partial | A job finished with some items succeeded and some failed. |
enrichment.job.failed | A job failed. |
article.completed | An article job started from a connected store's app or plugin finished. |
article.failed | An article job started from a connected store's app or plugin failed. |
content.approved | A review item was approved, by a person in the app or automatically by Autopilot. One event per item, sent to the item owner's subscriptions whoever approved it. |
content.rejected | A review item was rejected. |
content.moved_to_approval | New content arrived in Review and is waiting for approval. One event for the batch, with the ids of the items in it. |
content.generated | A generation run started in the app finished, when in-app notifications were on for that run. |
content.published | An article was reported live by a connected store, or a published guide was refreshed after a product change was approved. |
feed.ingested | A product feed finished importing. |
feed.source.refreshed | A scheduled or requested pull of a feed's source URL finished, including when the file had not changed. |
feed.autopilot.completed | An Autopilot run for a feed finished: its content is written and the quality check has run. |
feed.autopilot.failed | An Autopilot run for a feed found products to work on but could not start. |
alert.triggered | An alert rule on one of your projects fired. |
Job events cover jobs created through the API, a connected store, or the MCP server, whatever the job's kind: rules, content, pipeline, or product lookup. Jobs you start in the dashboard do not send them, and a cancelled job sends nothing.
The feed events are described with their payloads in the Feeds API guide.
What a delivery looks like
Each delivery is an HTTP POST with a JSON body:
{
"event": "enrichment.job.succeeded",
"timestamp": "2026-10-09T10:15:00.000Z",
"user_id": "<account-id>",
"project_id": "<project-id>",
"data": {
"jobId": "<job-id>",
"kind": "pipeline",
"status": "completed",
"succeededCount": 10,
"failedCount": 0,
"resultsUrl": "/api/v1/jobs/<job-id>/results"
}
}event and timestamp are always present. project_id is present when the event belongs to one project. data depends on the event:
| Event | data fields |
|---|---|
enrichment.job.* | jobId, kind, status, succeededCount, failedCount, resultsUrl, and storeId when the job belongs to a connected store |
article.* | The job fields above, plus briefId |
content.approved | review_item_id, product_id, content_type |
content.rejected | review_item_id, and product_id and content_type when the item was rejected from the review queue |
content.moved_to_approval | review_item_ids, content_types, count |
content.generated | job_id, completed_count |
feed.ingested | feed_id, product_count, filename |
content.published | briefId, title, destination, and either url for a newly published article or reason and changeSummary for a refreshed guide |
feed.source.refreshed | feedId, kind, and jobId, via, or reason depending on kind |
feed.autopilot.* | feedId, runId, trigger, cause, productsEvaluated, productsQueued, skippedApproved, truncated, jobIds, error, resultsUrl |
alert.triggered | alert_id, alert_rule_id, rule_name, metric_name, metric_value, previous_value, severity, message |
resultsUrl is a path relative to https://ace.authoritas.com.
The request carries these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Webhook-Signature | The signature of the body. See below. |
X-Webhook-Event | The event name, the same as event in the body. |
X-Webhook-Timestamp | When the event happened, the same as timestamp in the body. |
User-Agent | PDP-Enhancement-Webhook/1.0 |
Verify a delivery
Every delivery is signed with your subscription secret. The signature is an HMAC-SHA256 of the exact request body, keyed with the secret and written as lowercase hex. It arrives in the X-Webhook-Signature header with no prefix.
To verify, compute the same HMAC over the raw bytes you received and compare. Use the raw body, not a re-serialised copy: parsing the JSON and stringifying it again can change the bytes, and the signature will not match.
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.REZOLVE_WEBHOOK_SECRET;
function isGenuine(rawBody, signatureHeader) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const given = Buffer.from(signatureHeader ?? "", "utf8");
const wanted = Buffer.from(expected, "utf8");
// timingSafeEqual throws on unequal lengths, so check the length first.
return given.length === wanted.length && crypto.timingSafeEqual(given, wanted);
}
// express.raw keeps the body as a Buffer, which is what the signature covers.
app.post("/hooks/rezolve", express.raw({ type: "application/json" }), (req, res) => {
if (!isGenuine(req.body, req.get("X-Webhook-Signature"))) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString("utf8"));
queueForProcessing(event); // do the real work after you have answered
res.status(204).end();
});Reject any delivery whose signature does not match. The body's timestamp is covered by the signature, so you can also refuse events older than you are willing to accept.
Retries and the delivery log
A delivery succeeds when your endpoint answers with a 2xx status within 30 seconds. Anything else, including no answer in that time, counts as a failed attempt.
- The delivery queue runs every minute. A failed delivery is tried again on a later run, up to six attempts in total.
- A delivery moves through the statuses
pending,retrying, and thendeliveredorfailed. A delivery that reachesfailedis not sent again. - If you set a subscription inactive while deliveries are waiting, they are marked
failedrather than sent.
To keep deliveries healthy:
- Respond quickly with a
2xxstatus, then do the heavy work asynchronously. A slow endpoint looks like a failure and gets retried. - Make your handler idempotent. A retry can deliver the same event more than once, and the payload has no delivery id, so key your processing on something in the body, such as
eventtogether withdata.jobId.
Review delivery history with GET /api/v1/webhooks/{id}/deliveries, most recent first. Filter with status (pending, retrying, delivered, failed) and page with page and pageSize (50 by default, 100 at most).
curl "https://ace.authoritas.com/api/v1/webhooks/<id>/deliveries?status=failed" \
-H "Authorization: Bearer <your-api-key>"Each entry shows the event_type, the payload that was sent, the number of attempts, the response_status and response_body your endpoint returned, and an error_message when the attempt failed. See the deliveries reference.
Manage subscriptions
- List your subscriptions:
GET /api/v1/webhooks. - Read one:
GET /api/v1/webhooks/{id}. - Update the
url,events,description, oris_active:PUT /api/v1/webhooks/{id}. - Remove a subscription:
DELETE /api/v1/webhooks/{id}. An id that is not one of yours returns404.
To pause deliveries without losing the subscription, set is_active to false with a PUT. There is no endpoint that reads a secret back or replaces it. If you lose a secret, delete the subscription and create a new one.
From an assistant, list_webhooks and delete_webhook do the same over MCP.