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

# Event reference

> The five-key model shared by every resource, and where to find each resource's full payload reference.

# Event reference

All events share the envelope described in **[Getting started](/getting-started)**:
`{ event, id, created, data }`. `id` is always the id of the resource the event is about.

This page explains the shape every resource follows. For a specific resource's exact fields, its
list of `change`/`field`/`activity` values, and full example payloads, go to its own page — linked
in the table below.

## The five keys per resource

Every resource exposes up to five subscribable keys. Instead of a long list of narrow keys, related
changes are grouped and a discriminator inside `data` tells you exactly what happened:

| Key                   | Fires when                                                                                                      | Discriminator               | `data`                                                        |
| --------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------- |
| `<resource>.created`  | a record is created (including via clone/duplicate)                                                             | —                           | the full resource object                                      |
| `<resource>.updated`  | a **significant** change to an existing record                                                                  | `data.change`               | `change` + the full resource object under the resource's name |
| `<resource>.deleted`  | a record is deleted                                                                                             | —                           | `{ "id": <deleted id> }`                                      |
| `<resource>.sundry`   | a **minor** field edit (cosmetic/descriptive)                                                                   | `data.field`                | `field` + the full resource object under the resource's name  |
| `<resource>.activity` | a child record (comment / attachment / note / checklist / milestone / log / reply) is added, updated or deleted | `data.type` + `data.action` | `type`, `action`, `item`                                      |
| `<resource>.imported` | a bulk import finishes — **only on the four importable resources**                                              | —                           | the imported batch (see [Imports](#imports))                  |

Subscribe once to `<resource>.updated` and branch on `data.change` — you do not need a separate
subscription per change type. The same is true for `.sundry` (branch on `data.field`) and
`.activity` (branch on `data.type` + `data.action`).

* **`.updated`/`.sundry`** always carry the **complete, current** resource object — not a diff.
  Whatever single thing changed, `data.<resource>` reflects the record's full state afterwards.
* **`.activity`**'s `data.item` shape depends on `data.type` (`comment`, `attachment`, `note`,
  `checklist`, `checklist_comment`, `milestone`, `task`, `log`, or `reply`) — see the resource's own
  page for which types it produces and their exact fields, including which delete actions carry a
  full item vs. just `{ "id": ... }`.
* **`data.action`** is normally `added`, `updated` or `deleted`. Checklists add one more,
  `status_changed`, for ticking an item off — see [Projects](/projects).

> **Deletion events** (`*.deleted`) fire *after* the record is gone, so `data` is just
> `{ "id": <deleted id> }`.

## Dates

The envelope's top-level `created` field is ISO 8601 with a UTC offset
(`2026-07-13T10:15:00+00:00`). Inside `data`, it depends on the field: any `created`/`updated`
timestamp that names a record's own creation/update — the resource's `dates.created`/`dates.updated`,
**and** the `created` field on `.activity` items (comment/attachment/log/reply) — is ISO 8601 **with
microseconds and a literal `Z`** (`2026-07-13T10:15:00.000000Z`). Every one of these models (`Client`,
`Lead`, `Comment`, `Attachment`, `LeadLog`, `TicketReply`, etc.) recasts its create/update columns to
Carbon instances via Eloquent's `CREATED_AT`/`UPDATED_AT` constants, which is what produces this
format instead of a plain string. Every other date/time field (due dates, sent dates, expiry dates,
a ticket's `last_updated`, etc.) is a raw database value — `Y-m-d H:i:s` for timestamps, `Y-m-d` for
date-only fields — not reformatted at all. Check each resource's own page for exactly which fields
are which; don't assume from the field name alone.

## Resources

| Resource   | Keys                                                  | Details                         |
| ---------- | ----------------------------------------------------- | ------------------------------- |
| Clients    | created, updated, deleted, sundry, activity, imported | **[Clients →](/clients)**       |
| Leads      | created, updated, deleted, sundry, activity, imported | **[Leads →](/leads)**           |
| Projects   | created, updated, deleted, sundry, activity, imported | **[Projects →](/projects)**     |
| Tasks      | created, updated, deleted, sundry, activity           | **[Tasks →](/tasks)**           |
| Estimates  | created, updated, deleted, sundry, activity           | **[Estimates →](/estimates)**   |
| Proposals  | created, updated, deleted, sundry                     | **[Proposals →](/proposals)**   |
| Contracts  | created, updated, deleted, sundry                     | **[Contracts →](/contracts)**   |
| Invoices   | created, updated, deleted, sundry, activity           | **[Invoices →](/invoices)**     |
| Payments   | created, updated, deleted, sundry                     | **[Payments →](/payments)**     |
| Refunds    | created, updated, deleted, sundry                     | **[Refunds →](/refunds)**       |
| Products   | created, updated, deleted, sundry, activity, imported | **[Products →](/products)**     |
| Tickets    | created, updated, deleted, sundry, activity           | **[Tickets →](/tickets)**       |
| Expenses   | created, updated, deleted, sundry, activity           | **[Expenses →](/expenses)**     |
| Timesheets | created, updated, deleted                             | **[Timesheets →](/timesheets)** |
| Team       | created, updated, deleted                             | **[Team →](/team)**             |

Proposals, Contracts, Payments and Refunds have no `.activity` key — none of them has comments,
attachments, or logs in core. Timesheets and Team are lifecycle-only: a time entry has no minor
fields and no child records, and a team member's changes all arrive as `team.updated`.

## Imports

Records created by a **bulk import** do **not** fire `<resource>.created`. Importers write rows
directly rather than going through the normal create path, so a 5,000-row import would otherwise
produce 5,000 separate deliveries. Instead, the four importable resources — Clients, Leads, Projects
and Products — fire a single `<resource>.imported` event carrying the batch:

```json theme={null}
{
  "event": "project.imported",
  "id": "8f3ka92m",
  "created": "2026-07-31T10:00:00+00:00",
  "data": {
    "import_ref": "8f3ka92m",
    "imported_by": 3,
    "batch": 1,
    "batch_count": 3,
    "totals": { "imported": 2400, "skipped": 12, "errors": 3 },
    "records": [
      { "id": 1234, "title": "Acme website rebuild", "client_id": 12, "created": "2026-07-31T10:00:00.000000Z" }
    ]
  }
}
```

| Field           | Type          | Notes                                                                                                          |
| --------------- | ------------- | -------------------------------------------------------------------------------------------------------------- |
| `id` (envelope) | string        | The import reference — **not** a numeric record id. This is the one place the envelope `id` is not an integer. |
| `import_ref`    | string        | Same value as the envelope `id`; identifies this import run.                                                   |
| `imported_by`   | integer\|null | Id of the user who ran the import.                                                                             |
| `batch`         | integer       | 1-based index of this delivery within the import.                                                              |
| `batch_count`   | integer       | Total number of deliveries this import produced.                                                               |
| `totals`        | object        | `imported`, `skipped` and `errors` for the **whole** import, repeated on every batch.                          |
| `records`       | array         | Compact records — see each resource's page for its exact fields.                                               |

**Batching.** Large imports are split across several deliveries of up to 1,000 records each, so a
single request never grows large enough to be rejected by a receiver (nginx, for example, refuses
request bodies over 1 MB by default). Every batch of one import shares the same `import_ref`, and
`batch`/`batch_count` tell you how to reassemble them. Batches are queued in id order and delivered
independently, so treat them as a set that has arrived only once you have seen `batch_count` of them.

**Records are deliberately compact** — enough to identify each row and match it to your own data.
If you need the full record, read it back from the API using its `id`.

Checklist imports do not fire an import event; they create child records rather than resources.
