Gareth Wilson Gareth Wilson

Guide to Agentset Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationDashboard, Webhooks settings, Create Webhook
ScopeOne organisation, optionally limited to chosen namespaces
Signaturehex HMAC-SHA256
Signature headeragentset-signature
Signed contentThe raw request body only
SecretOne per webhook, shown in its Update Details tab
BodyJSON envelope of id, event, createdAt and data
Event routingBody field event
SuccessA 2xx within 20 seconds
RetriesUp to 10, with exponential backoff capped at 24 hours
Auto-disableOn the 20th consecutive failure, after warning emails at 5, 10 and 15
Logs and testingWebhook 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

EventTriggered WhenCommon Use Cases
document.queuedA document is queued for ingestionShow "pending" in your UI
document.queued_for_resyncA document is queued for re-syncTrack refreshes
document.queued_for_deletionA document is queued for deletionHide it from results
document.processingA document starts processingProgress indicators
document.readyA document is ingested and searchableEnable search, notify the user
document.errorIngestion failed (data.error)Alert, show the reason, retry
document.deletedA document was deletedClean up local records
ingest_job.queuedAn ingest job is queuedTrack crawls and imports
ingest_job.queued_for_resyncAn ingest job is queued for re-syncTrack refreshes
ingest_job.queued_for_deletionAn ingest job is queued for deletionTrack cleanup
ingest_job.processingAn ingest job starts runningProgress indicators
ingest_job.readyAn ingest job completedMark an import done
ingest_job.errorAn ingest job failed (data.error)Alert and retry
ingest_job.deletedAn ingest job was deletedClean 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

  1. In the Agentset dashboard, open the Webhooks settings page and click Create Webhook.
  2. Give the webhook a name and enter your endpoint URL, for example https://example.com/webhooks/agentset.
  3. Select the events you want. document.ready, document.error, ingest_job.ready and ingest_job.error cover most applications.
  4. Optionally pick namespaces. Leave them empty to receive events from every namespace.
  5. 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.