Guide to Agentset Webhooks: Features and Best Practices
Agentset is a managed platform for retrieval-augmented generation: you upload files or point it at a source, and it parses, chunks and embeds the content so your application can search it. That processing happens in the background, and a large PDF or a crawl can take a while. Agentset webhooks tell your application when each document or ingest job moves between states, so you can show progress, enable search the moment a document is ready, and catch failures without polling the API.
This guide walks through the document and ingest job lifecycle events, how an Agentset webhook is created and tested, how to check the Agentset-Signature header, and how Agentset's retry and disable rules shape the way you run a receiver.
What are Agentset webhooks?
Agentset webhooks belong to an organisation. Each one has a name, a URL, a list of events and, optionally, a list of namespaces. A webhook with no namespaces selected receives events from every namespace in the organisation, which is convenient for a single app and noisy for a multi-tenant one.
Two families of object produce events. A document is a single file or piece of content in a namespace. An ingest job is the unit of work that creates documents, such as an upload batch or a crawl. Both go through the same states: queued, queued for re-sync, queued for deletion, processing, ready, error and deleted. Agentset sends one event per transition, with the full object in the body.
Agentset signs each request with a per-webhook secret. The Agentset-Signature header holds a hex HMAC-SHA256 of the raw body and nothing else, so there is no timestamp to check.
Agentset webhook features
| Feature | Details |
|---|---|
| Configuration | Dashboard, Webhooks settings, Create Webhook |
| Scope | One organisation, optionally limited to chosen namespaces |
| Signature | hex HMAC-SHA256 |
| Signature header | agentset-signature |
| Signed content | The raw request body only |
| Secret | One per webhook, shown in its Update Details tab |
| Body | JSON envelope of id, event, createdAt and data |
| Event routing | Body field event |
| Success | A 2xx within 20 seconds |
| Retries | Up to 10, with exponential backoff capped at 24 hours |
| Auto-disable | On the 20th consecutive failure, after warning emails at 5, 10 and 15 |
| Logs and testing | Webhook Logs tab, and Send test event for any event type |
Agentset publishes no source IP range for webhooks, and its docs name neither the HTTP method nor the Content-Type.
Events and payloads
| Event | Triggered When | Common Use Cases |
|---|---|---|
document.queued | A document is queued for ingestion | Show "pending" in your UI |
document.queued_ | A document is queued for re-sync | Track refreshes |
document.queued_ | A document is queued for deletion | Hide it from results |
document.processing | A document starts processing | Progress indicators |
document.ready | A document is ingested and searchable | Enable search, notify the user |
document.error | Ingestion failed (data.error) | Alert, show the reason, retry |
document.deleted | A document was deleted | Clean up local records |
ingest_job.queued | An ingest job is queued | Track crawls and imports |
ingest_ | An ingest job is queued for re-sync | Track refreshes |
ingest_ | An ingest job is queued for deletion | Track cleanup |
ingest_job.processing | An ingest job starts running | Progress indicators |
ingest_job.ready | An ingest job completed | Mark an import done |
ingest_job.error | An ingest job failed (data.error) | Alert and retry |
ingest_job.deleted | An ingest job was deleted | Clean up local records |
Document events carry the document in data, including its namespaceId, status, source, character, chunk and page counts, and an error field that is null unless processing failed. ingest_job.* events carry the job instead (id, name, namespaceId, organizationId, status, error, createdAt and updatedAt).
Here is the document.deleted sample from the webhook samples library:
{
"id": "evt_stu901vwx234",
"event": "document.deleted",
"createdAt": "2024-08-26T18:05:00.000Z",
"data": {
"id": "doc_k8f2m9x3n4p1",
"name": "product-guide.pdf",
"namespaceId": "ns_j7h3k9m2n5p8",
"organizationId": "org_x9y8z7w6v5u4",
"status": "QUEUED_FOR_DELETION",
"source": {
"type": "FILE",
"fileUrl": "https://example.com/sample.pdf"
},
"totalCharacters": 45230,
"totalChunks": 32,
"totalPages": 15,
"error": null,
"createdAt": "2024-08-26T16:41:52.084Z",
"updatedAt": "2024-08-26T18:05:00.000Z"
}
}
The status inside data is the object's status when the event was built, which is not always the same word as the event name. In this sample the document is deleted but its status still reads QUEUED_FOR_DELETION, so route on event rather than on data.status.
Setting up Agentset webhooks
- In the Agentset dashboard, open the Webhooks settings page and click Create Webhook.
- Give the webhook a name and enter your endpoint URL, for example
https://example.com/webhooks/agentset. - Select the events you want.
document.ready,document.error,ingest_job.readyandingest_job.errorcover most applications. - Optionally pick namespaces. Leave them empty to receive events from every namespace.
- Click Create webhook, then open the webhook's Update Details tab and copy the signing secret into your server's configuration, for example as
AGENTSET_WEBHOOK_SECRET.
To test, open Update Details, choose Send test event from the ⋮ menu, pick an event type and send it. The Webhook Logs tab then shows the request and your response, including any retries.
To receive through Hookdeck, create a source with the Agentset source type, paste the signing secret as its verification secret, and use the Hookdeck source URL as the webhook URL in Agentset. During development you can forward to a local server with the Hookdeck CLI: npx hookdeck-cli listen 3000 agentset --path /webhooks/agentset.
HTTP method
Agentset's docs don't name the HTTP method its webhooks use. Hookdeck's Agentset source pre-selects POST without managing the method, so you can allow other methods on the source if you see them. The receiver below uses app.all for the same reason and verifies whatever method arrives.
Securing Agentset webhooks
Take the agentset-signature header as one value, with no prefix to strip and no list to split. Compute an HMAC-SHA256 of the raw body using the signing secret's UTF-8 bytes as the key, and compare the two digests in constant time after checking that the header is exactly 64 hex characters. Agentset's own samples in the verification documentation do the same.
const crypto = require('node:crypto');
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const signature = headers['agentset-signature'];
if (typeof signature !== 'string') return false;
const key = Buffer.from(secret, 'utf8');
const message = rawBody;
const candidates = [signature];
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
const received = Buffer.from(candidate, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
The receiver keeps the body as raw bytes, verifies it, and only then parses the envelope and routes on event:
const express = require('express');
const app = express();
app.all('/webhooks/agentset', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.AGENTSET_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500);
const body = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
if (!verifySignature(body, req.headers, secret)) return res.sendStatus(401);
if (!body.length) return res.sendStatus(200);
let delivery;
try {
delivery = JSON.parse(body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (!delivery || typeof delivery !== 'object' || Array.isArray(delivery)) return res.sendStatus(400);
try {
const item = delivery.data || {};
switch (delivery.event) {
case 'document.ready':
await markSearchable(item.namespaceId, item.id);
break;
case 'document.error':
case 'ingest_job.error':
await recordFailure(delivery.event, item.id, item.error);
break;
case 'ingest_job.ready':
await markImportComplete(item.id);
break;
default:
// Progress states and events this service doesn't use.
break;
}
} catch {
return res.sendStatus(503);
}
res.sendStatus(200);
});
markSearchable, recordFailure and markImportComplete stand in for your own code. In production, store the verified delivery and do this work after responding, because Agentset counts anything slower than 20 seconds as a failure.
Hookdeck's Agentset source type checks the same header with the same HMAC. Hookdeck accepts the request with a 200 and then records a failed verification on the request, so Agentset never sees the 401 that the sample receiver returns. Because Agentset signs no timestamp, there is no freshness window for Hookdeck or your receiver to apply.
Make Agentset webhooks production-ready. Hookdeck Event Gateway verifies
Agentset-Signature, stores every document and ingest job event, and delivers it to your service with retries and replay you control.
Agentset webhook limitations and pain points
Twenty failures and the webhook switches off
The Problem: Events stop arriving after an outage, and they keep stopping after your service is healthy again.
Why It Happens: Agentset treats anything other than a 2xx within 20 seconds as a failure. It emails the owners after 5, 10 and 15 consecutive failures and disables the webhook on the 20th. A disabled webhook stays off until someone clicks Enable webhook on its details page.
Workarounds:
- Make sure the warning emails reach someone who will act on them, not an unattended inbox.
- Answer quickly and do slow work, such as fetching the document or re-indexing, after responding.
- After any incident, check the webhook's state in the dashboard and re-enable it.
How Hookdeck Can Help: Hookdeck answers Agentset straight away and queues the event, so failures in your own service count against Hookdeck's retry policy rather than Agentset's disable counter.
Nothing to stop a replayed request
The Problem: A captured request still verifies if someone sends it again later.
Why It Happens: The signature covers only the body. Agentset sends no timestamp header and signs nothing else, so the request carries no evidence of when it was made.
Workarounds:
- Record each envelope
id(evt_...) when you process it and skip repeats. - Treat state changes as idempotent: marking a document as ready twice should do no harm.
How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries with the same id before they reach your service.
Thin delivery documentation
The Problem: You have to guess at details that other providers spell out.
Why It Happens: Agentset documents only the Agentset-Signature header. It names no Content-Type, user agent, delivery ID header or HTTP method, and publishes no source IPs, so the signature is the only way to authenticate a request.
Workarounds:
- Accept any method and content type on the webhook route, and rely on signature verification.
- Capture one real delivery with the Send test event menu and check its headers before you write middleware around them.
How Hookdeck Can Help: Hookdeck records the full headers and body of every request it receives, so you can see exactly what Agentset sent without adding logging to your own service.
Status lags behind the event name
The Problem: Code that reads data.status sees an unexpected value.
Why It Happens: The object in data is a snapshot, and its status can trail the event. Agentset's document.deleted example still shows QUEUED_FOR_DELETION.
Workarounds:
- Route on the envelope's
event, which names the transition. - If you need the current state, read the document or job from the Agentset API by ID.
How Hookdeck Can Help: Hookdeck filters can match on the event field, so each transition can go to its own destination without your code inspecting data.status.
Best practices
Subscribe to outcomes, not every step
Most applications need only ready and error. The queued and processing events are useful for progress bars, but every extra event is another request that has to succeed inside 20 seconds.
Scope webhooks with namespaces
If each of your customers has their own namespace, create webhooks per namespace group instead of one for the whole organisation. That keeps unrelated tenants' events away from a receiver that does not need them.
Keep the raw body
The HMAC covers the exact bytes Agentset sent. Use express.raw() in Express, Buffer.from(await request.arrayBuffer()) in a Next.js route handler, or await request.body() in FastAPI, and parse JSON only after the signature matches.
Deduplicate on the event id
Retries resend the same envelope with the same id. Store it with the change it caused, as described in our guide to webhook idempotency.
Acknowledge, then process
Verify the request, persist it, return 200 and then do the work. This keeps you inside Agentset's 20-second window and away from the disable counter. See why to process webhooks asynchronously.
Conclusion
Agentset's webhooks follow documents and ingest jobs through their processing states, and each request carries a plain hex HMAC-SHA256 of the body. The signature is simple to check. Running the receiver well takes more care: answer within 20 seconds, deduplicate on the event id, and watch for the warning emails before the 20th failure turns the webhook off.
Hookdeck Event Gateway sits between Agentset and your service, verifies each signature, and keeps events queued while your service is unavailable.
Get started with Hookdeck for free and handle Agentset webhooks reliably in minutes.