> ## Documentation Index
> Fetch the complete documentation index at: https://help.ciarem.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Event catalog

> Every webhook event type Ciarem sends, when it fires, and the exact body it carries.

## The envelope

Every event has the same outer shape. Only `data` changes from one type to another.

```json theme={null}
{
  "id": "evt_…",
  "type": "contact.created",
  "api_version": "v1",
  "created_at": "2026-10-09T15:04:05.123456Z",
  "data": { }
}
```

| Field | Type | |
| - | - | - |
| `id` | string | Unique id of the event. It stays the same across retries and when you send a delivery again, so it is the value to deduplicate on. |
| `type` | string | The event type, one of the list below. |
| `api_version` | string | Always `v1` today. |
| `created_at` | string | When the event happened in Ciarem. ISO 8601, UTC, ending in `Z`. |
| `data` | object | The details of the event. |

## All event types

| `type` | Fires when | `data` carries |
| - | - | - |
| `contact.created` | A contact is added, by any means | The full contact |
| `contact.updated` | A contact's fields or custom properties change | The full contact and what changed |
| `contact.deleted` | A contact is deleted | The contact's id |
| `contact.merged` | Two contacts are merged into one | The surviving contact and the id that was merged into it |
| `contact.funnel_stage.changed` | A contact moves to another funnel stage | The full contact and the previous stage |
| `contact.owner.changed` | A contact's owner changes | The full contact and the previous owner |
| `conversation.created` | A new conversation starts | Conversation, contact, and channel ids |
| `conversation.assigned` | A conversation is assigned to a teammate | The same ids, plus who it was assigned to |
| `conversation.closed` | A conversation is resolved | Conversation, contact, and channel ids |
| `messages.inbound` | A customer sends a message | Conversation, contact, and channel ids |
| `whatsapp_flow.completed` | A customer submits a [WhatsApp form](https://help.ciarem.ai/channels/whatsapp-forms.md) | The form, the contact, the answers, and what Ciarem saved |

Contact events carry the **whole contact**, so most integrations never need to call the API back. Conversation and message events carry **ids only**: read the conversation or its messages from the [API](https://help.ciarem.ai/api-reference/overview) when you need the content.

> [!NOTE]
>
> New event types are added over time. An endpoint subscribed to **all events** starts receiving them automatically. Answer `200` to any `type` you don't handle.

## The contact object

Contact events include a `contact` object with the same fields as `GET /v1/contacts/{id}`:

```json theme={null}
{
  "id": "cnt_…",
  "first_name": "Ana",
  "last_name": "García",
  "email": "ana@example.com",
  "phone": "+5215550100",
  "country": "MX",
  "city": "Ciudad de México",
  "instagram": null,
  "messenger": null,
  "whatsapp_username": null,
  "origin": "MANUAL",
  "sub_origin_id": null,
  "whatsapp_status": "verified",
  "marketing_opt_out": false,
  "funnel_stage_id": "stg_…",
  "owner_id": null,
  "properties": {
    "custom_budget": {
      "property_id": "prp_…",
      "kind": "NUMBER",
      "value_text": null,
      "value_number": 5000,
      "value_bool": null,
      "value_date": null,
      "value_json": null
    }
  },
  "last_contacted_at": null,
  "created_at": "2026-01-15T14:30:00Z",
  "updated_at": "2026-01-15T14:30:00Z"
}
```

Three things to know:

* **`properties`** holds your custom properties, keyed by the property's key. Read `kind` to know which `value_*` field holds the value.
* **File properties are left out.** A file's download link is temporary and would expire before a retried delivery arrives. Read the contact from the API to get a fresh link.
* **The contact is a snapshot taken when Ciarem prepares the event**, a moment after the change. If the contact changed twice in quick succession, both events can show the latest state. A retry sends the same snapshot again, not a fresh one.

## Contact events

### contact.created

A contact was added: by a teammate, by an import, by the API, by an integration such as HubSpot, or because a new person wrote in on a channel.

```json theme={null}
{
  "id": "evt_…",
  "type": "contact.created",
  "api_version": "v1",
  "created_at": "2026-10-09T15:04:05.123456Z",
  "data": {
    "contact": { "id": "cnt_…", "first_name": "Ana", "…": "…" }
  }
}
```

### contact.updated

A contact's own fields (name, email, phone, city, and so on) or one of its custom properties changed.

```json theme={null}
{
  "id": "evt_…",
  "type": "contact.updated",
  "api_version": "v1",
  "created_at": "2026-10-09T15:04:05.123456Z",
  "data": {
    "contact": { "id": "cnt_…", "first_name": "Ana María", "…": "…" },
    "changed_fields": ["first_name"]
  }
}
```

`changed_fields` lists what changed:

* A plain field appears by name: `"first_name"`, `"email"`.
* A custom property appears as `properties.<key>`: `"properties.custom_budget"`. Each custom property that changes sends **its own** `contact.updated` event.

Funnel stage and owner changes are **not** reported here. They have their own events, below. One edit that changes a name, the stage, and the owner at once sends three events: `contact.updated`, `contact.funnel_stage.changed`, and `contact.owner.changed`.

### contact.deleted

A contact was deleted. The contact no longer exists, so only its id is sent.

```json theme={null}
{
  "id": "evt_…",
  "type": "contact.deleted",
  "api_version": "v1",
  "created_at": "2026-10-09T15:04:05.123456Z",
  "data": {
    "contact_id": "cnt_…"
  }
}
```

### contact.merged

Two contacts were merged. `contact` is the one that survives, with the merged result. `merged_contact_id` is the contact that was folded into it and no longer exists on its own: update any records on your side that still point at it.

```json theme={null}
{
  "id": "evt_…",
  "type": "contact.merged",
  "api_version": "v1",
  "created_at": "2026-10-09T15:04:05.123456Z",
  "data": {
    "contact": { "id": "cnt_…", "…": "…" },
    "merged_contact_id": "cnt_…"
  }
}
```

### contact.funnel\_stage.changed

A contact moved to another funnel stage, whether a teammate moved it or the AI agent did. The new stage is `contact.funnel_stage_id`; the one it left is `previous_funnel_stage_id`. Resolve stage ids to names with `GET /v1/funnel-stages`.

```json theme={null}
{
  "id": "evt_…",
  "type": "contact.funnel_stage.changed",
  "api_version": "v1",
  "created_at": "2026-10-09T15:04:05.123456Z",
  "data": {
    "contact": { "id": "cnt_…", "funnel_stage_id": "stg_…", "…": "…" },
    "previous_funnel_stage_id": "stg_…"
  }
}
```

### contact.owner.changed

A contact's owner changed. The new owner is `contact.owner_id`; the previous one is `previous_owner_id`. Either can be `null`, meaning the contact has, or had, no owner.

```json theme={null}
{
  "id": "evt_…",
  "type": "contact.owner.changed",
  "api_version": "v1",
  "created_at": "2026-10-09T15:04:05.123456Z",
  "data": {
    "contact": { "id": "cnt_…", "owner_id": "mbr_…", "…": "…" },
    "previous_owner_id": null
  }
}
```

## Conversation and message events

These four events share the same base `data`:

| Field | Type | |
| - | - | - |
| `conversation_id` | string | The conversation. Read it with `GET /v1/conversations/{id}`. |
| `contact_id` | string or null | The contact on the other side. |
| `channel_kind` | string | `whatsapp`, `instagram`, `messenger`, or `webchat`. |
| `channel_id` | string | The connected channel (for example, which WhatsApp number). |

### conversation.created

A new conversation started.

```json theme={null}
{
  "id": "evt_…",
  "type": "conversation.created",
  "api_version": "v1",
  "created_at": "2026-10-09T15:04:05.123456Z",
  "data": {
    "conversation_id": "conv_…",
    "contact_id": "cnt_…",
    "channel_kind": "whatsapp",
    "channel_id": "chn_…"
  }
}
```

> [!NOTE]
>
> Ciarem waits a few seconds for the contact to be linked before sending this event, so `contact_id` is almost always present. In the rare case it is still `null`, read the conversation from the API a little later. This short wait also means the first `messages.inbound` of a conversation can arrive **before** its `conversation.created`.

### conversation.assigned

A conversation was assigned, by a teammate taking it, by an admin, by your automatic assignment rules, or by the AI agent handing it over.

```json theme={null}
{
  "id": "evt_…",
  "type": "conversation.assigned",
  "api_version": "v1",
  "created_at": "2026-10-09T15:04:05.123456Z",
  "data": {
    "conversation_id": "conv_…",
    "contact_id": "cnt_…",
    "channel_kind": "whatsapp",
    "channel_id": "chn_…",
    "assignee_id": "mbr_…",
    "assignee": {
      "id": "mbr_…",
      "name": "Juan Lopez",
      "email": "juan@example.com"
    },
    "team_id": null,
    "strategy": "round_robin",
    "assigned_by": "system",
    "self_assigned": false,
    "url": "https://www.ciarem.ai/dashboard/inbox?c=conv_…"
  }
}
```

`assignee_id` is the teammate, always set. `team_id` is the conversation's team and can be `null`.

| Field | Type | |
| - | - | - |
| `assignee` | object or null | The teammate's `id`, `name`, and `email`, so a no-code tool (a HubSpot workflow, Zapier, Make) can show who got the chat without calling the API. `name` and `email` can be `null`. |
| `strategy` | string | How it was assigned: `manual` (a person chose, or the AI handed it to the teammate a transfer rule names), `round_robin` or `random` (your assignment rules), `owner` (it went to the contact's owner), `reassigned` (the previous advisor didn't reply in time), or `solo_admin` (no advisor was available, so it went to the organization's only member, an admin). |
| `assigned_by` | string | `system` for rules and the AI agent, otherwise the `mbr_…` id of the teammate who assigned it. |
| `self_assigned` | boolean | `true` when the teammate took the conversation themselves, for example by replying to it. Filter these out if you only want to hear about chats handed to someone. |
| `url` | string | Opens the conversation in the Ciarem inbox. |

### conversation.closed

A conversation was resolved, either by a teammate finishing the chat or by the AI agent closing it. The body is the base `data` above.

```json theme={null}
{
  "id": "evt_…",
  "type": "conversation.closed",
  "api_version": "v1",
  "created_at": "2026-10-09T15:04:05.123456Z",
  "data": {
    "conversation_id": "conv_…",
    "contact_id": "cnt_…",
    "channel_kind": "whatsapp",
    "channel_id": "chn_…"
  }
}
```

### messages.inbound

A customer sent a message. The event tells you **which conversation** has something new; it does not contain the message itself.

```json theme={null}
{
  "id": "evt_…",
  "type": "messages.inbound",
  "api_version": "v1",
  "created_at": "2026-10-09T15:04:05.123456Z",
  "data": {
    "conversation_id": "conv_…",
    "contact_id": "cnt_…",
    "channel_kind": "whatsapp",
    "channel_id": "chn_…"
  }
}
```

To read what was said, fetch the conversation's messages:

```bash theme={null}
curl https://api.ciarem.ai/v1/conversations/conv_…/messages \
  -H "Authorization: Bearer oak_YOUR_SECRET"
```

Messages your team or the AI agent **send** do not fire an event.

## WhatsApp form events

### whatsapp\_flow\.completed

A customer submitted a [WhatsApp form](https://help.ciarem.ai/channels/whatsapp-forms.md). It fires once per submission, after Ciarem saved the answers.

```json theme={null}
{
  "id": "evt_…",
  "type": "whatsapp_flow.completed",
  "api_version": "v1",
  "created_at": "2026-10-15T15:04:05.123456Z",
  "data": {
    "session_id": "wafs_…",
    "flow_id": "waflow_…",
    "contact_id": "cnt_…",
    "conversation_id": "conv_…",
    "answers": { "q1": "Ana", "q2": "ana@example.com" },
    "outcome": { "saved": ["contact:first_name", "contact:email"], "errors": [] }
  }
}
```

* `answers` is keyed by the form's internal field names, not by the question text. To read the values by property, use `outcome.saved` (the contact fields and properties the answers were written to) and read the contact.
* `outcome.errors` lists answers that could not be saved, for example an email that belongs to another contact.
* A booking form also carries `booking_id`, `start_at`, `end_at`, `meeting_url`, and `attendee_email` in `outcome`.
* A photo or document question's answer is a list of `{"file_name", "mime_type"}`, and `outcome.files` gives its state: `{"q3": {"status": "pending"}}`. The event fires before the files are stored. When the question saves to a file property, the stored files arrive with the [`contact.updated`](#contact-events) event that follows.
* `conversation_id` can be `null` when the form was sent with a template outside a conversation.

## The test event

A test sent from the dashboard or with `POST /v1/webhook-endpoints/{id}/test` looks like a real event, with two differences:

* Its `id` starts with `evt_test_`, so you can tell it apart and skip any real side effects.
* If you don't choose an event type, its `type` is `test` and its `data` is empty:

```json theme={null}
{
  "id": "evt_test_…",
  "type": "test",
  "api_version": "v1",
  "created_at": "2026-10-09T15:04:05.123456Z",
  "data": {}
}
```

When you do choose a type, the test carries sample data in exactly the shape a real event has. `GET /v1/webhook-event-samples` returns the sample body of every type, which is handy for writing tests of your own handler.

## Compatibility

Webhook bodies follow the same rule as the rest of the API: changes are **additive**. New event types, new fields inside `data`, and new values in string fields can appear at any time. Read the fields you need, ignore the rest, and check the [changelog](https://help.ciarem.ai/api-reference/changelog.md) when something looks different.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.