Skip to main content
Rezolve Ai
Guides

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

EventSent when
enrichment.job.succeededA rules, content, pipeline, or lookup job finished and every item succeeded.
enrichment.job.partialA job finished with some items succeeded and some failed.
enrichment.job.failedA job failed.
article.completedAn article job started from a connected store's app or plugin finished.
article.failedAn article job started from a connected store's app or plugin failed.
content.approvedA 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.rejectedA review item was rejected.
content.moved_to_approvalNew content arrived in Review and is waiting for approval. One event for the batch, with the ids of the items in it.
content.generatedA generation run started in the app finished, when in-app notifications were on for that run.
content.publishedAn article was reported live by a connected store, or a published guide was refreshed after a product change was approved.
feed.ingestedA product feed finished importing.
feed.source.refreshedA scheduled or requested pull of a feed's source URL finished, including when the file had not changed.
feed.autopilot.completedAn Autopilot run for a feed finished: its content is written and the quality check has run.
feed.autopilot.failedAn Autopilot run for a feed found products to work on but could not start.
alert.triggeredAn 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:

Eventdata 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.approvedreview_item_id, product_id, content_type
content.rejectedreview_item_id, and product_id and content_type when the item was rejected from the review queue
content.moved_to_approvalreview_item_ids, content_types, count
content.generatedjob_id, completed_count
feed.ingestedfeed_id, product_count, filename
content.publishedbriefId, title, destination, and either url for a newly published article or reason and changeSummary for a refreshed guide
feed.source.refreshedfeedId, kind, and jobId, via, or reason depending on kind
feed.autopilot.*feedId, runId, trigger, cause, productsEvaluated, productsQueued, skippedApproved, truncated, jobIds, error, resultsUrl
alert.triggeredalert_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:

HeaderValue
Content-Typeapplication/json
X-Webhook-SignatureThe signature of the body. See below.
X-Webhook-EventThe event name, the same as event in the body.
X-Webhook-TimestampWhen the event happened, the same as timestamp in the body.
User-AgentPDP-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 then delivered or failed. A delivery that reaches failed is not sent again.
  • If you set a subscription inactive while deliveries are waiting, they are marked failed rather than sent.

To keep deliveries healthy:

  • Respond quickly with a 2xx status, 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 event together with data.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

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.

On this page