---
title: "Events and webhooks for a business's AI agent"
url: "https://conecto.chat/docs/events"
markdown: "https://conecto.chat/docs/events.md"
description: "What happens in a business reaches its agent as events. Poll account_events, or send events to Slack, Teams, Telegram or a signed webhook."
section: "Concepts"
published: 2026-10-19
updated: 2026-10-19
---

# Events and webhooks

When someone outside the agent acts, like booking a meeting or signing a document, Conecto records an event. The agent reads it, or a channel gets it as it happens.

## What an event is

An area records an event when something happens that the agent should hear about: a candidate applies, a customer books, a form comes in, a document is signed, an email arrives, the balance runs low. Each event is kept 30 days, and events and their deliveries are free. [Event kinds](https://conecto.chat/docs/event-kinds.md) lists every kind with the fields its data carries.

| Field | What it is |
| --- | --- |
| `event_id` | A number that grows with each event. Poll with `after` set to the last one you saw. |
| `kind` | What happened, like `calendar.booking_created`. |
| `title` | One line about it. |
| `text` | More words, when there are some. |
| `data` | The facts as JSON. Each kind's fields are on [Event kinds](https://conecto.chat/docs/event-kinds.md). |
| `link` | A page about it, when there is one. |
| `occurred_at` | When it happened. |

## Reading events

`account_events` returns what happened, oldest first. Pass `after` with the last `event_id` you saw, and `kinds` to narrow it, like `calendar.*`. A read is free up to the month's 1,000,000 free reads.

account_events, the arguments:

```json
{
  "after": 1520,
  "kinds": ["calendar.*", "forms.*", "esign.*"]
}
```

account_events, the result:

```json
{
  "events": [
    {
      "event_id": 4101,
      "kind": "calendar.booking_created",
      "title": "Rachel Kim booked Bike fitting",
      "text": "Saturday, October 24, 2026 at 11:30 (America/Los Angeles)",
      "data": {
        "reference": "CUT49YHZ85",
        "meeting_type": "bike-fitting",
        "start": "2026-10-24T11:30:00-07:00",
        "end": "2026-10-24T19:15:00+00:00",
        "name": "Rachel Kim",
        "email": "rachel.kim@example.com",
        "phone": null,
        "source": "bot",
        "reason": null
      },
      "link": null,
      "occurred_at": "2026-10-14T08:00:00+00:00"
    }
  ],
  "next_after": 4101,
  "note": null
}
```

`next_after` is the `event_id` to pass the next time. A channel that gets events as they happen saves the polling.

## Sending events to a channel

A channel is a place the business gets messages: Slack, Microsoft Teams, Discord, Google Chat, a Telegram chat, ntfy, Pushover, or a webhook of its own.

1. `notify_add_channel` saves a channel by name, for $0.001. It sends a test message first, and saves nothing when the test fails to arrive. Its tokens are stored encrypted and never shown back.
2. `notify_subscribe` sends chosen events to it, for $0.001: one kind, every event of an area like `hiring.*`, the events bots send like `custom.*`, or `*` for all of them, up to 30 names. It replaces the channel's list.
3. `notify_unsubscribe` stops them. The channel stays for `notify_send`.

notify_subscribe, the arguments:

```json
{
  "channel": "hiring",
  "events": ["hiring.application_received", "hiring.test_completed"]
}
```

A chat app gets the event's title, its text and its link. A webhook gets the whole event as signed JSON.

Deliveries go out within about a minute. Some kinds go out at once, like a chat message and the answer to an approval, and [Event kinds](https://conecto.chat/docs/event-kinds.md) marks them. A delivery that fails is tried again after 5 minutes and after 30 more, 3 tries in all. A channel gets at most 30 messages a minute, and the rest wait a minute. An account keeps up to 50 channels.

## Webhooks

A webhook channel posts each event to the business's own address, which is how Zapier, Make, n8n or the business's own systems connect. `notify_add_channel` with `kind: "webhook"` and a public https `url` answers with a `webhook_secret`, shown once.

The body of a delivery:

```json
{
  "id": "evt_4101",
  "event": "calendar.booking_created",
  "title": "Rachel Kim booked Bike fitting",
  "text": "Saturday, October 24, 2026 at 11:30 (America/Los Angeles)",
  "data": {
    "reference": "CUT49YHZ85",
    "meeting_type": "bike-fitting",
    "start": "2026-10-24T11:30:00-07:00",
    "end": "2026-10-24T19:15:00+00:00",
    "name": "Rachel Kim",
    "email": "rachel.kim@example.com",
    "phone": null,
    "source": "bot",
    "reason": null
  },
  "link": null,
  "account": "Halverson Cycle & Repair",
  "occurred_at": "2026-10-14T08:00:00+00:00"
}
```

| Header | What it holds |
| --- | --- |
| `Content-Type` | `application/json`. |
| `X-Conecto-Signature` | `sha256=` and the HMAC-SHA256 of the raw body, keyed with the channel's `webhook_secret`, as hex. |
| `X-Conecto-Event` | The event's kind, like `calendar.booking_created`. |
| `X-Conecto-Delivery` | `evt_` and the event's `event_id`. A retry sends the same value. |
| `User-Agent` | `Conecto-Notify/1.0`. |

### Checking the signature

Compute the HMAC-SHA256 of the raw body with the `webhook_secret`, as hex, and compare it with the header in constant time. A retry sends the same `X-Conecto-Delivery`, so drop one you have seen.

Python:

```python
import hashlib
import hmac


def is_from_conecto(body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)
```

JavaScript:

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

export function isFromConecto(body, header, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
  return expected.length === header.length && timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
```

A message that `notify_send` posts to a webhook is signed the same way. Its body holds `title`, `message`, `link`, `urgent`, `account` and `sent_at`, with no `X-Conecto-Event` header.

## Custom events

A bot names its own events with `account_emit_event`: `custom.` and a name, like `custom.order_shipped`, with a title and up to 10 KB of data. They go to every channel subscribed to them and to `account_events`, where another team's bot can pick them up. Each costs $0.005 per channel it goes to, and `sent_by` is added with the bot's name and team.

## More in the docs

- Previous: [Approvals](https://conecto.chat/docs/approvals.md)
- Next: [Money](https://conecto.chat/docs/money.md)
- Every page in one file: https://conecto.chat/docs/all.md
- Every page's title and address: https://conecto.chat/docs/index.json
