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

# Webhook Events

> Every event a webhook can subscribe to, grouped by object, with the scope it needs and the payload it carries.

Every delivery uses the [envelope](https://docs.useorbitcrm.ai/developers/webhooks/overview.md#the-envelope); this page documents `data` for each object. Updates carry `changes`, an object keyed by field with the old and new value:

```json theme={null}
"changes": {
  "amount": { "from": 8000, "to": 12000 },
  "owner_user_id": { "from": null, "to": "d4e5f6a7-..." }
}
```

Bookkeeping columns (`updated_at`, activity counters such as a contact's `submission_count`, a deal's board `position`) never count as a change on their own.

`external_ids` on contacts, companies and deals holds the record's id in other systems, one key per system (`{ "hubspot": "301", "halo": "u_8812" }`), as `external_id` + `external_source` set them through the API: an app that links records this way finds its own record straight from the event. `custom_fields` carries each value in its field type's shape (a list for multi-select, a member's user ID for a teammate field, an ISO 8601 UTC instant for a date-time; see [Custom fields](https://docs.useorbitcrm.ai/developers/api/crm.md#custom-fields)). Changing a field's type in a way that rewrites stored values (Dropdown to Multi-select and back) sends an `*.updated` event for every record that held a value.

To subscribe with an API key or an OAuth app, the credential needs `webhooks:write` plus the object's read scope listed with each group. Signed-in members creating webhooks in the dashboard are not scoped.

## Submissions

Scope: `submissions:read`. The only events a webhook's `form_id` filter applies to.

| Event | When |
| - | - |
| `submission.created` | A form received a completed submission |
| `submission.qualified` | A workflow's AI qualification marked the submission's contact as qualified. `data` adds `contact_id`, `qualification_status` and `workflow_run_id` |
| `submission.updated` | The answers or status of a submission changed |
| `submission.deleted` | A submission was deleted |

```json theme={null}
"data": {
  "form_id": "550e8400-e29b-41d4-a716-446655440000",
  "form_title": "Contact Form",
  "form_slug": "contact-form",
  "submission_id": "660e8400-e29b-41d4-a716-446655440001",
  "submission_data": { "email": "user@example.com", "name": "Jane Doe" },
  "submitted_at": "2026-10-27T14:30:00Z",
  "metadata": {
    "referrer": "https://example.com",
    "user_agent": "Mozilla/5.0...",
    "device_type": "desktop",
    "country": "US",
    "utm_source": "google",
    "utm_medium": "cpc",
    "utm_campaign": "spring_sale"
  }
}
```

The submitter's IP address is never sent.

## Contacts

Scope: `contacts:read`

| Event | When |
| - | - |
| `contact.created` | A contact was created: by a form, an import, the CRM, the API or a workflow |
| `contact.updated` | A field on the contact changed (`changes` lists them), including its `lifecycle_stage` or `lead_status` (keys; a deal can move the stage forward, and a lead status that carries a qualification changes `qualification_status` in the same event), or its read-only `lead_score` when Orbit recalculates it. Adding or removing one of the contact's other email addresses sends it with `emails` (every address, the primary first) in `data` and `changes.emails` (`old` and `new` lists); making another address primary is a change of `email` |
| `contact.archived` | The contact's status became `archived` (sent alongside `contact.updated`) |
| `contact.deleted` | The contact was deleted, permanently or to Recently deleted; `contact` is the record as it was |
| `contact.restored` | The contact came back from Recently deleted (restored by an admin, or revived by a new submission, import or API create) |
| `contact.tag_added` | A tag was added; `data.tag` is `{ id, name, slug, color }` |
| `contact.tag_removed` | A tag was removed; `data.tag` names it |
| `contact.association_changed` | A company was linked to or unlinked from the contact, or the link's labels or primary flag changed. `data` adds `change` (`added`, `updated` or `removed`), `association` and, for an update, `previous` (`{ is_primary, labels }`). Making another company primary sends one for each of the two links, plus `contact.updated` for `company_id` |
| `contact.job_changed` | Enrichment found the contact at another employer, or with another title at the same one, when it re-checked them (only in workspaces that turn on re-checks under Settings › Integrations › your enrichment provider). `data.job_change` is `{ signal_id, change, previous_company, company, previous_title, title, provider, detected_at }`; `change` is `company` (a new employer; the title may have changed too) or `title`. Orbit never overwrites the contact's fields for it: a title someone typed stays, and the new one is offered as a suggestion |

```json theme={null}
"data": {
  "contact_id": "7c1d2e3f-...",
  "contact": {
    "id": "7c1d2e3f-...",
    "email": "jane@acme.com",
    "first_name": "Jane",
    "last_name": "Doe",
    "phone": "+14155550100",
    "company": "Acme",
    "company_id": "3a4b5c6d-...",
    "job_title": "VP Sales",
    "time_zone": "America/Chicago",
    "status": "active",
    "qualification_status": "qualified",
    "lifecycle_stage": "sales_qualified_lead",
    "lead_status": "qualified",
    "lead_score": 42,
    "owner_user_id": "d4e5f6a7-...",
    "do_not_call": false,
    "custom_fields": { "plan": "enterprise" },
    "external_ids": { "halo": "u_8812" },
    "submission_count": 2,
    "meeting_count": 1,
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-10-27T14:30:05Z"
  },
  "changes": {
    "lead_status": { "from": "connected", "to": "qualified" },
    "qualification_status": { "from": null, "to": "qualified" }
  }
}
```

A contact can be linked to several companies; `contact.company_id` is its primary company. A `contact.association_changed` delivery:

```json theme={null}
"data": {
  "contact_id": "7c1d2e3f-...",
  "contact": { "id": "7c1d2e3f-...", "email": "jane@acme.com", "company_id": "3a4b5c6d-...", "...": "..." },
  "change": "updated",
  "association": {
    "type": "contact_company",
    "contact_id": "7c1d2e3f-...",
    "company_id": "5e6f7a8b-...",
    "company": { "id": "5e6f7a8b-...", "name": "Globex", "domain": "globex.com" },
    "is_primary": false,
    "labels": [{ "key": "billing_contact", "label": "Billing contact" }]
  },
  "previous": { "is_primary": false, "labels": [] }
}
```

Label keys never change (labels are renamed, never re-keyed); `GET /api/v1/association-labels` lists them.

Linking a contact's first company sends `added` (`is_primary: false`) and then `updated` (it became the primary), in that order; removing the primary sends `removed`, then `updated` for the company that took over. A contact or deal created with a company sends no association event for it: `contact.created` / `deal.created` already carry `company_id`.

A `contact.job_changed` delivery:

```json theme={null}
"data": {
  "contact_id": "7c1d2e3f-...",
  "contact": { "id": "7c1d2e3f-...", "email": "jane@acme.com", "job_title": "VP Sales", "...": "..." },
  "job_change": {
    "signal_id": "0f1e2d3c-...",
    "change": "company",
    "previous_company": "Acme",
    "company": "Globex",
    "previous_title": "VP Sales",
    "title": "Chief Revenue Officer",
    "provider": "moltsets",
    "detected_at": "2026-11-02T09:14:00Z"
  }
}
```

## Companies

Scope: `companies:read`

| Event | When |
| - | - |
| `company.created` | A company was created, including one created automatically from a new contact's email domain |
| `company.updated` | A field on the company changed, including its parent company (`parent_company_id`) and its `lifecycle_stage` |
| `company.deleted` | The company was deleted, permanently or to Recently deleted; `company` is the record as it was. Its contacts, deals and child companies stay, without the company, until it is restored |
| `company.restored` | The company came back from Recently deleted, with its links to contacts, deals, child companies, tags, tasks, notes and files |
| `company.merged` | Another company was merged into this one. `company` is the survivor; `data` adds `merged_company_id`, `merged_company` (the merged company as it was) and `moved` (counts by kind: `contacts`, `deals`, `child_companies`, `notes`, `tasks`, `activities`, `files`, `tags`, `followers`). The merged company went to Recently deleted, which sent `company.deleted` just before; contacts and deals that moved send their own `*.updated` |
| `company.tag_added` | A tag was added to the company; `data.tag` is `{ id, name, slug, color }` |
| `company.tag_removed` | A tag was removed from the company; `data.tag` names it (only `{ id }` when the tag itself was deleted). Deleting a company sends `company.deleted`, not a removal per tag |

```json theme={null}
"data": {
  "company_id": "3a4b5c6d-...",
  "company": {
    "id": "3a4b5c6d-...",
    "name": "Acme",
    "domain": "acme.com",
    "industry": "Software",
    "employee_count": 250,
    "annual_revenue": 42000000,
    "phone": null,
    "location": "Austin, TX",
    "description": null,
    "owner_user_id": "d4e5f6a7-...",
    "parent_company_id": "1f2e3d4c-...",
    "custom_fields": {},
    "external_ids": {},
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-10-27T14:30:05Z"
  },
  "changes": { "employee_count": { "from": 200, "to": 250 } }
}
```

A `company.merged` delivery:

```json theme={null}
"data": {
  "company_id": "3a4b5c6d-...",
  "company": { "id": "3a4b5c6d-...", "name": "Acme", "domain": "acme.com", "external_ids": { "hubspot": "1201" }, "...": "..." },
  "merged_company_id": "7e8f9a0b-...",
  "merged_company": { "id": "7e8f9a0b-...", "name": "ACME Inc.", "domain": "acme.io", "external_ids": { "hubspot": "1202" }, "...": "..." },
  "moved": { "contacts": 3, "deals": 2, "child_companies": 0, "notes": 1, "tasks": 0, "activities": 14, "files": 0, "tags": 1, "followers": 1 }
}
```

## Deals

Scope: `deals:read`

| Event | When |
| - | - |
| `deal.created` | A deal was created |
| `deal.updated` | A field on the deal changed: amount, currency, close date, owner, company, description, custom fields, stage, status, lost reason, deal type, recurring amount or interval, renewal date. `amount_home` (the amount in the workspace currency; see [Currencies](https://docs.useorbitcrm.ai/developers/api/crm.md#currencies-and-exchange-rates)) is in every deal payload, but an exchange-rate change alone sends no event |
| `deal.stage_changed` | The deal moved to another stage (sent alongside `deal.updated`). `data` adds `from_stage` and `to_stage`, each `{ id, name, kind, pipeline_id }`; `kind` is `open`, `won` or `lost`, and `deal.status` says whether the deal is now open, won or lost |
| `deal.won` | The deal became won: it moved to a won stage, was created in one, or the stage it sits in became a won stage. Sent once per win (a move between two won stages sends nothing), alongside `deal.updated` or `deal.created`. `data` adds `to_stage` and, when the deal moved to get there, `from_stage` (`null` otherwise) |
| `deal.lost` | The deal became lost, the same way. `deal.lost_reason` carries the reason when one was given |
| `deal.reopened` | A won or lost deal moved back to an open stage: `deal.status` is `open` again and `closed_at` is `null`. Subscribe to it with `deal.won` to retract a win you have already counted |
| `deal.deleted` | The deal was deleted, permanently or to Recently deleted; `deal` is the record as it was |
| `deal.restored` | The deal came back from Recently deleted, with its contacts, stage history, tags, tasks, notes and files. Its stage may differ from the one it was deleted in: deleting a stage moves the deleted deals in it to the pipeline's first stage |
| `deal.merged` | Another deal was merged into this one, which kept its own pipeline, stage and status. `data` adds `merged_deal_id`, `merged_deal` (the merged deal as it was) and `moved` (counts: `contacts`, `companies`, `stage_changes`, `notes`, `tasks`, `activities`, `files`, `tags`, `followers`). The merged deal went to Recently deleted, which sent `deal.deleted` just before |
| `deal.tag_added` | A tag was added to the deal; `data.tag` is `{ id, name, slug, color }` |
| `deal.tag_removed` | A tag was removed from the deal; `data.tag` names it (only `{ id }` when the tag itself was deleted). Deleting a deal sends `deal.deleted`, not a removal per tag |
| `deal.association_changed` | A contact or company was added to or removed from the deal, or the association's labels or primary flag changed. `data` adds `change` (`added`, `updated` or `removed`), `association` (`type` is `deal_contact` with `contact_id` and `contact`, or `deal_company` with `company_id` and `company`; plus `is_primary` and `labels`, each `{ key, label }`) and, for an update, `previous` (`{ is_primary, labels }`). Deleting the deal sends `deal.deleted`, not a removal per association |
| `deal.line_item_added` | A product, or a one-off line, was added to the deal. `data` adds `line_item`: `{ id, deal_id, product_id, name, sku, description, quantity, unit_price, discount_type, discount, recurring_interval, amount, position }`; `amount` is quantity × unit price less the discount, in the deal's currency |
| `deal.line_item_updated` | A line item on the deal changed. `data` adds `line_item`, and `changes` lists the line's own changed fields. When the deal amount follows its line items, the new amount arrives as a separate `deal.updated` |
| `deal.line_item_removed` | A line item was removed from the deal; `line_item` is the line as it was. Deleting the deal sends `deal.deleted`, not a removal per line |

To count closed-won deals, subscribe to `deal.won` (and `deal.reopened`) rather than `deal.updated`: you receive one delivery per win instead of every edit, and `deal.closed_at` is the moment it closed. `GET /api/v1/deals?status=won&closed_after=...` lists the same deals for a backfill or a reconciliation.

A `deal.won` delivery (the `deal.stage_changed` and `deal.updated` sent for the same move carry the same `deal` and `changes`):

```json theme={null}
"data": {
  "deal_id": "9a1d3c4e-...",
  "deal": {
    "id": "9a1d3c4e-...",
    "name": "Acme renewal",
    "pipeline_id": "e1f2a3b4-...",
    "stage_id": "2a3b4c5d-...",
    "status": "won",
    "amount": 12000,
    "currency": "USD",
    "amount_home": 12000,
    "amount_home_rate": 1,
    "amount_home_rate_at": "2026-10-27T14:30:05Z",
    "close_date": "2026-11-15",
    "closed_at": "2026-10-27T14:30:05Z",
    "owner_user_id": "d4e5f6a7-...",
    "company_id": "3a4b5c6d-...",
    "lost_reason": null,
    "custom_fields": {},
    "source": "manual",
    "stage_changed_at": "2026-10-27T14:30:05Z",
    "created_at": "2026-09-20T10:00:00Z",
    "updated_at": "2026-10-27T14:30:05Z"
  },
  "changes": { "stage_id": { "from": "1f2e3d4c-...", "to": "2a3b4c5d-..." }, "status": { "from": "open", "to": "won" } },
  "from_stage": { "id": "1f2e3d4c-...", "name": "Proposal", "kind": "open", "pipeline_id": "e1f2a3b4-..." },
  "to_stage": { "id": "2a3b4c5d-...", "name": "Closed won", "kind": "won", "pipeline_id": "e1f2a3b4-..." }
}
```

The contacts linked to a deal are not embedded; fetch them with `GET /api/v1/deals/:id`.

## Products

Scope: `deals:read`

| Event | When |
| - | - |
| `product.created` | A product was added to the product library: by hand, by an import or through the API |
| `product.updated` | A product's name, SKU, description, unit price, currency or recurring interval changed |
| `product.archived` | The product was archived: it can no longer be added to deals. Line items that already have it keep their copy |
| `product.restored` | An archived product was restored |

```json theme={null}
"data": {
  "product_id": "5b6c7d8e-...",
  "product": {
    "id": "5b6c7d8e-...",
    "name": "Pro plan",
    "sku": "PLAN-PRO",
    "description": "Up to 10 seats",
    "unit_price": 99,
    "currency": "USD",
    "recurring_interval": "month",
    "archived_at": null,
    "created_at": "2026-11-01T09:00:00Z",
    "updated_at": "2026-11-01T09:00:00Z"
  }
}
```

## Quotes

Scope: `deals:read`

| Event | When |
| - | - |
| `quote.created` | A quote was made from a deal: a draft, or shared at once (then `quote.published` follows) |
| `quote.updated` | A draft's title, terms, expiry, payment link choice or lines changed; `changes` lists them |
| `quote.published` | A draft was shared: its public page works from now on (`GET /api/v1/quotes/:id` returns `public_url`), with `payment_link_url` when it has a Stripe payment link |
| `quote.viewed` | Someone opened the shared quote. Counted at most once an hour per viewer (by IP), so reloads send nothing new; `quote.view_count` and `quote.first_viewed_at` tell a first view from a return visit |
| `quote.accepted` | The buyer accepted the quote on its page; `quote` carries `accepted_name`, `accepted_email` and `accepted_at` |
| `quote.voided` | A shared or accepted quote was voided: its page says it is no longer available and its payment link was switched off |
| `quote.deleted` | A draft was deleted, or the quote's deal was deleted permanently |

`data` is `{ quote_id, quote }`, plus `changes` on `quote.updated`. `quote` is the stored quote: `number` (shown as Q-1001), `deal_id`, `title`, `status` (`draft`, `open`, `accepted` or `void`; an open quote past `expires_at` can no longer be accepted), `currency`, `items` (a copy of the deal's line items when the quote was made), `subtotal`, `discount_total`, `total`, `terms`, `expires_at`, `recipient` and `sender` (`{ name, email, company }`), `include_payment_link`, `payment_link_url`, `published_at`, `first_viewed_at`, `last_viewed_at`, `view_count`, `accepted_at`, `accepted_name`, `accepted_email`, `voided_at`. The page's link itself is never sent in a webhook.

## Tasks

Scope: `tasks:read`

| Event | When |
| - | - |
| `task.created` | A task was created: by a person, a workflow, the API, a "Call back" call outcome, or as the next occurrence of a repeating task that was completed (`recurrence_parent_id` names that task) |
| `task.updated` | A field on the task changed |
| `task.completed` | The task was marked done (sent alongside `task.updated`) |
| `task.deleted` | The task was deleted, permanently or to Recently deleted; `task` is the record as it was |
| `task.restored` | The task came back from Recently deleted |

```json theme={null}
"data": {
  "task_id": "b1c2d3e4-...",
  "task": {
    "id": "b1c2d3e4-...",
    "title": "Send proposal",
    "description": null,
    "status": "done",
    "priority": "high",
    "task_type": "email",
    "due_at": "2026-10-28T10:00:00Z",
    "completed_at": "2026-10-27T14:30:05Z",
    "repeat_unit": "week",
    "repeat_interval": 1,
    "repeat_time_zone": "America/New_York",
    "reminder_minutes": 60,
    "remind_at": "2026-10-28T09:00:00Z",
    "recurrence_parent_id": null,
    "callback_call_id": null,
    "assignee_user_id": "d4e5f6a7-...",
    "contact_id": "7c1d2e3f-...",
    "company_id": null,
    "deal_id": "9a1d3c4e-...",
    "source": "manual",
    "custom_fields": { "outcome": "Signed", "follow_up_on": "2026-11-03" },
    "created_by": "d4e5f6a7-...",
    "created_at": "2026-10-20T10:00:00Z",
    "updated_at": "2026-10-27T14:30:05Z"
  },
  "changes": { "status": { "from": "open", "to": "done" }, "completed_at": { "from": null, "to": "2026-10-27T14:30:05Z" } }
}
```

`task_type` is `call`, `email`, `todo` or a workspace's own `custom_*` key ([task types](https://docs.useorbitcrm.ai/developers/api/crm.md#task-types-and-repeating-tasks)). A repeating task has `repeat_unit` (`day`, `week`, `month`), `repeat_interval` and `repeat_time_zone`; completing it sends `task.completed` for it and `task.created` for the next one. `callback_call_id` is the dialer call whose "Call back" outcome created the task. `reminder_minutes` reminds the assignee that many minutes before `due_at` (`0` at the due time, `null` none), and `remind_at` is when. The payload also carries `repeat_anchor_at`, the date the series counts from. `repeat_anchor_at` and `remind_at` move with `due_at` and `reminder_minutes`, so they never appear in `changes` on their own.

## Meetings

Scope: `meetings:read`

| Event | When |
| - | - |
| `meeting.booked` | A meeting was booked through a scheduling page, the API or an assistant |
| `meeting.rescheduled` | A meeting was moved. `meeting` carries the new time plus `old_start_time`, `old_end_time` and `original_meeting_id` |
| `meeting.cancelled` | A meeting was cancelled; `meeting.cancellation_reason` holds the reason when one was given |

```json theme={null}
"data": {
  "meeting_id": "c3d4e5f6-...",
  "meeting": {
    "id": "c3d4e5f6-...",
    "title": "Intro call",
    "status": "confirmed",
    "start_time": "2026-10-29T15:00:00Z",
    "end_time": "2026-10-29T15:30:00Z",
    "timezone": "America/New_York",
    "duration_minutes": 30,
    "meeting_link": "https://meet.google.com/abc-defg-hij",
    "notes": "Interested in the enterprise plan",
    "is_first_meeting": true,
    "host": { "user_id": "d4e5f6a7-...", "name": "Sam Rivera", "email": "sam@yourcompany.com" },
    "attendee": { "name": "Jane Doe", "email": "jane@acme.com" },
    "guests": [],
    "scheduling_page": { "id": "a9b8c7d6-...", "title": "Book a demo" },
    "event_type_id": "f5e4d3c2-...",
    "lead_capture_fields": { "Company size": "50-200" }
  },
  "contact": {
    "id": "7c1d2e3f-...",
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane@acme.com",
    "company": "Acme",
    "job_title": "VP Sales",
    "phone": "+14155550100",
    "status": "active",
    "qualification_status": "qualified",
    "owner_user_id": "d4e5f6a7-...",
    "submission_count": 2
  }
}
```

## Calls

Scope: `dialer:read`

| Event | When |
| - | - |
| `call.completed` | A dialer call ended: answered, voicemail, busy, no answer, failed or cancelled. Calls people make to the workspace's numbers fire it too, with `direction: "inbound"` and `inbound_result` (`answered`, `forwarded`, `voicemail` or `missed`); for those `from_number` is the caller ("anonymous" when withheld) and `user_id` the rep the call was for. It fires as the call ends, before the rep picks an outcome, so `call.outcome` is the automatic one for calls nobody answered (`no_answer`, `busy`, `voicemail`, …) and usually `null` after a conversation. The rep's outcome (a built-in key or one of the workspace's `custom_*` keys; `GET /api/v1/dialer/outcomes` has their labels) is on the call from then on: read it with `GET /api/v1/dialer/calls/:id`, or from `call.transcribed` / `call.analyzed`, which carry the call as it is when they are sent |
| `call.transcribed` | The call's transcript is ready. `data.insights` carries its status, language and length; fetch the text with `GET /api/v1/dialer/calls/:id/insights` |
| `call.analyzed` | AI feedback for the call is ready; `data.insights.feedback` carries it |

```json theme={null}
"data": {
  "call_id": "e7f8a9b0-...",
  "call": {
    "id": "e7f8a9b0-...",
    "contact_id": "7c1d2e3f-...",
    "user_id": "d4e5f6a7-...",
    "session_id": "1a2b3c4d-...",
    "direction": "outbound",
    "inbound_result": null,
    "from_number": "+14155550123",
    "to_number": "+14155550100",
    "status": "completed",
    "answered_by": "human",
    "outcome": "connected",
    "notes": "Wants a quote by Friday",
    "started_at": "2026-10-27T14:20:00Z",
    "answered_at": "2026-10-27T14:20:08Z",
    "ended_at": "2026-10-27T14:26:40Z",
    "duration_seconds": 392,
    "recording_status": "completed",
    "recording_duration_seconds": 390,
    "voicemail_dropped": false
  },
  "insights": {
    "call_id": "e7f8a9b0-...",
    "transcript_status": "completed",
    "transcript_language": "en",
    "transcript_minutes": 7,
    "transcript_provider": "twilio",
    "feedback_status": "completed",
    "feedback_model": "claude-opus-5-5",
    "script_id": null,
    "feedback": {
      "summary": "Jane confirmed budget and asked for a quote by Friday.",
      "score": 8,
      "went_well": ["Confirmed the decision timeline"],
      "improve": ["Ask who else signs off"],
      "objections": [],
      "next_steps": ["Send the quote by Friday"],
      "script_adherence": { "followed": "mostly", "notes": "Skipped the discovery questions" },
      "suggested_outcome": "connected"
    }
  }
}
```

`call.completed` carries `call` only. The recording URL and the transcript text are never sent in a webhook; use `GET /api/v1/dialer/calls/:id/recording` and `GET /api/v1/dialer/calls/:id/insights`.

## Comments

Scope: `notes:read`

Comments are threads under notes and logged activities (calls, emails, meetings and messages). A comment mentions a member with a token in its text, `@[Sam Lee](user:<user id>)`; `mentioned_user_ids` lists the members it mentions.

| Event | When |
| - | - |
| `comment.created` | Someone commented on a note or logged activity |
| `comment.updated` | A comment's text was edited. `changes` has the old and new `body` (and `mentioned_user_ids` when the mentions changed) |
| `comment.deleted` | A comment was deleted. Deleting a comment is permanent; `data.comment` is its last state. Comments are also deleted with their note or activity when that is deleted for good (a note moved to Recently deleted keeps its comments, hidden, and sends nothing) |

```json theme={null}
"data": {
  "comment_id": "3c4d5e6f-...",
  "comment": {
    "id": "3c4d5e6f-...",
    "note_id": "9a8b7c6d-...",
    "activity_id": null,
    "body": "Can you send the quote today, @[Sam Lee](user:d4e5f6a7-...)?",
    "mentioned_user_ids": ["d4e5f6a7-..."],
    "created_by": "b1c2d3e4-...",
    "created_at": "2026-10-30T14:20:00Z",
    "updated_at": "2026-10-30T14:20:00Z",
    "edited_at": null,
    "contact_id": null,
    "company_id": null,
    "deal_id": "5e6f7a8b-..."
  }
}
```

`contact_id`, `company_id` and `deal_id` are the records the comment's note or activity is on when the event happened (`null` on `comment.deleted` when the note or activity was deleted with it). Read a thread with `GET /api/v1/comments?note_id=` or `?activity_id=`.

## Lists

Scope: `crm:read`

[Static lists](https://docs.useorbitcrm.ai/developers/api/crm.md#static-lists) hold contacts or companies that people, imports, the API or workflows put in them. The envelope's `object` is `list` and `object_id` the list; `data` names the list and carries the record, with the same fields as the contact and company events.

| Event | When |
| - | - |
| `list.member_added` | A contact or company was added to a list: by hand, by an import (once the workspace sends imports to integrations), through the API, by a workflow, or by a merge (the record that's kept joins the merged record's lists) |
| `list.member_removed` | A contact or company was taken out of a list. Deleting the record (`contact.deleted` / `company.deleted`) or the list sends no removal, and a record in Recently deleted sends nothing |

```json theme={null}
"data": {
  "list_id": "6a7b8c9d-...",
  "list": { "id": "6a7b8c9d-...", "name": "Webinar attendees", "object": "contacts" },
  "record_type": "contact",
  "record_id": "1a2b3c4d-...",
  "contact_id": "1a2b3c4d-...",
  "contact": { "id": "1a2b3c4d-...", "email": "ana@acme.com", "first_name": "Ana", "...": "..." }
}
```

A list of companies sends `record_type: "company"` with `company_id` and `company` instead.

## Inbox conversations

Scope: `emails:read`

A conversation is an email thread with a contact in the team Inbox: the mail forwarded or synced from members' mailboxes, mail to the workspace's team inbox address, emails sent from Orbit and the replies to them. Each is assigned to a member (or nobody) and is open or closed. `reason` says who made the change: `manual` (a member, the API or MCP; the envelope's `actor` names them), `reply` (a new email from the contact reopened it) or `round_robin` (the team inbox address handed a new conversation to the next member).

| Event | When |
| - | - |
| `conversation.assigned` | A conversation was assigned, handed to someone else or unassigned. `changes.assigned_to` has the previous and new member's user ID (`null` is nobody) |
| `conversation.closed` | A conversation was marked closed. `changes.status` is `open` to `closed` |
| `conversation.reopened` | A closed conversation was reopened, by a member or by a new reply (`reason: "reply"`; automatic replies such as out-of-office notes don't reopen it) |

```json theme={null}
"data": {
  "conversation_id": "6f7a8b9c-...",
  "conversation": {
    "id": "6f7a8b9c-...",
    "thread_key": "<CAF1x2y3@mail.gmail.com>",
    "subject": "Pricing for 40 seats",
    "contact_id": "a3f1c2d4-...",
    "channel": "shared",
    "mailbox_user_id": null,
    "assigned_to": "d4e5f6a7-...",
    "assigned_at": "2026-10-31T15:02:00Z",
    "assigned_by": "b1c2d3e4-...",
    "status": "open",
    "closed_at": null,
    "closed_by": null,
    "last_message_at": "2026-10-31T14:58:00Z",
    "last_inbound_at": "2026-10-31T14:58:00Z",
    "created_at": "2026-10-31T14:58:01Z",
    "updated_at": "2026-10-31T15:02:00Z"
  },
  "changes": { "assigned_to": { "from": null, "to": "d4e5f6a7-..." } },
  "reason": "manual"
}
```

`channel` is `shared` for mail that arrived on the team inbox address (`mailbox_user_id` is then `null`) and `personal` for a member's own mail. Read the messages with `GET /api/v1/conversations/:id`.

[Next: Webhook Security →](https://docs.useorbitcrm.ai/developers/webhooks/security.md)


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