Events
An event is a record that something happened. Every event is stored and readable through the API, and delivered to any webhook endpoint subscribed to its type.
Events are what let you stop polling. Training, crawling and tool runs all finish on their own schedule; an event tells you the moment they do.
The payload
Every event has the same envelope, whatever its type:
{
"object": "event",
"id": "evt_4d9a1f2c8b7e4a6d9c0f3b2a1e5d7c9f",
"type": "knowledge.dataset.trained",
"api_version": "2026-08-14",
"created": 1710000000,
"data": {
"object": {
"object": "knowledge_dataset",
"id": 12,
"name": "Refund policy",
"embedding_status": "completed"
}
}
}id— unique per event, prefixedevt_. Deduplicate on this: a retried delivery re-sends the same id, and receiving one twice is normal.type— what happened. Subscribe by type, or use*for everything.created— Unix timestamp of when it was recorded.data.object— the affected resource as it looked at that moment, in the same shape the REST API returns it.api_version— the version that shapeddata.object.
An event you receive describes something that has definitely happened. There is no window where you get told about a change that later gets rolled back.
The catalogue
Generated from the API, so it matches what can actually be delivered.
The ones that matter most
Most events describe something you just did yourself — you called POST /v1/chatbots, you get chatbot.created. Useful for audit, not urgent.
These are different. They tell you about work that finished without you asking, and there is no other way to learn about them except polling:
| Event | Why it matters |
|---|---|
knowledge.dataset.trained | Content is now usable in answers |
knowledge.dataset.failed | It is not, and never will be without intervention |
crawl.completed / crawl.failed | Same, for a whole site |
conversation.handoff.requested | A visitor is waiting for a human, right now |
action.run.failed | Your own system failed mid-conversation |
billing.credits.low | Top up before chat starts refusing |
billing.credits.exhausted | Chat is refusing now |
`knowledge.dataset.trained`, `knowledge.dataset.failed`, `conversation.handoff.requested`, and `billing.credits.low`. Those cover "my content is ready", "my content is broken", "a customer needs a person", and "I am about to run out".
Credit events do not spam
billing.credits.low fires at most once per billing period, and
billing.credits.exhausted likewise. Crossing the threshold does not emit an
event per message — that would make the signal useless. A renewal re-arms
both.
Reading events back
Webhooks are push; this is pull. Both read the same records.
GET/v1/events
curl "https://api.agentency.com/v1/events?limit=20" \
-H "Authorization: Bearer <YOUR_KEY>"Filter by type, page with cursors, and fetch one with
GET /v1/events/{id}. This is the right tool for backfilling after downtime:
rather than replaying deliveries one by one, list what you missed.
GET /v1/event_types returns the catalogue programmatically, which is handy
for building a subscription UI.
Events are retained for a limited window and then pruned. Treat them as a delivery mechanism and a short-term recovery log, not permanent storage — if you need history, store what you receive.
What's next
- Webhooks — receive these, and verify they are real.
- Going live — which to subscribe to before launch.