> ## 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.

# Timesheets

> Payload reference for timesheet.created, timesheet.updated and timesheet.deleted webhooks.

# Timesheets

> New to webhooks? Start with **[Getting started](/getting-started)** (envelope, delivery, retries)
> and **[Verification](/verification)** (confirming a request came from Grow CRM). Those conventions
> apply to every event below and are not repeated here.

Timesheets expose three keys: `timesheet.created`, `timesheet.updated`, `timesheet.deleted`. There
is no `.sundry` or `.activity` — a time entry has no minor fields and no child records.

## Manually-recorded time only

These events cover time entered through the **Timesheets** section. Time captured with the built-in
task **timer** (start/stop on a task) does **not** fire them.

That is deliberate: a running timer is not yet a completed record, and stop/start churn would
produce a large volume of low-value deliveries. If you need timer-tracked hours, read them from the
API rather than expecting a webhook.

## The timesheet object

`.created` and `.updated` carry this shape — flat for `.created`, nested under `timesheet` for
`.updated`:

```json theme={null}
{
  "id": 904,
  "status": "stopped",
  "seconds": 5400,
  "hours": 1.5,
  "task": { "id": 611, "title": "Build the export screen" },
  "project_id": 305,
  "client_id": 12,
  "user": { "id": 7, "name": "Dana" },
  "recorded_by": 3,
  "billing": { "status": "not_invoiced", "invoice_id": null },
  "dates": { "created": "2026-07-31T09:00:00" }
}
```

| Field         | Type          | Notes                                                                                                                                                    |
| ------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`          | integer       | `timer_id`.                                                                                                                                              |
| `status`      | string        | `stopped` for a recorded timesheet. `running` only appears if you fetch a live timer through the API — these webhooks never fire for one.                |
| `seconds`     | integer       | Recorded time in whole seconds, as stored.                                                                                                               |
| `hours`       | number        | `seconds / 3600`, rounded to 2 decimal places. A convenience — `seconds` is authoritative.                                                               |
| `task`        | object        | `{ id, title }` — the task the time was logged against.                                                                                                  |
| `project_id`  | integer\|null | Denormalised from the task.                                                                                                                              |
| `client_id`   | integer\|null | Denormalised from the task.                                                                                                                              |
| `user`        | object        | `{ id, name }` — who the time is **for**.                                                                                                                |
| `recorded_by` | integer\|null | Who **entered** it. Differs from `user.id` when a manager logs time on someone else's behalf.                                                            |
| `billing`     | object        | `status` is `invoiced` or `not_invoiced`; `invoice_id` is set once billed.                                                                               |
| `dates`       | object        | Raw database value (`Timer` does not recast its timestamps), so `created` is `Y-m-d H:i:s`, not the ISO-with-microseconds format used by most resources. |

## `timesheet.created`

Fires when time is recorded in the Timesheets section.

**One submission can create several timesheets** — the form accepts a row per assigned task. Each
recorded row delivers its own `timesheet.created` event; there is no combined event.

```json theme={null}
{
  "event": "timesheet.created",
  "id": 904,
  "created": "2026-07-31T09:00:00+00:00",
  "data": {
    "id": 904,
    "status": "stopped",
    "seconds": 5400,
    "hours": 1.5,
    "task": { "id": 611, "title": "Build the export screen" },
    "project_id": 305,
    "client_id": 12,
    "user": { "id": 7, "name": "Dana" },
    "recorded_by": 3,
    "billing": { "status": "not_invoiced", "invoice_id": null },
    "dates": { "created": "2026-07-31 09:00:00" }
  }
}
```

## `timesheet.updated`

Fires on a significant change. `data.change` tells you which; `data.timesheet` is the full object.

| `change` | Fires when                                                                |
| -------- | ------------------------------------------------------------------------- |
| `edited` | The timesheet edit form is saved. Only the recorded duration is editable. |

```json theme={null}
{
  "event": "timesheet.updated",
  "id": 904,
  "created": "2026-07-31T09:30:00+00:00",
  "data": {
    "change": "edited",
    "timesheet": { "id": 904, "seconds": 7200, "hours": 2, "...": "..." }
  }
}
```

## `timesheet.deleted`

Fires after the timesheet is deleted. `data` is just the id — the record no longer exists to
describe.

Deleting is a checkbox selection, so removing several timesheets at once delivers one event **per
timesheet**, not one event containing a list.

```json theme={null}
{
  "event": "timesheet.deleted",
  "id": 904,
  "created": "2026-07-31T09:40:00+00:00",
  "data": { "id": 904 }
}
```
