# Events

Events record what happened in a project: uploads and moderation decisions, manual reviews, processing failures, usage limits, new members and failing connectors. The events feed returns them oldest first after a cursor: call it without `after` to get a cursor for the current time, pass the returned `cursor` back to receive only newer events, and request again straight away while `hasMore` is `true`.

Guide: [Webhooks & Connectors](/docs/guides/webhooks), which push the same events to you as they happen.

## Feed and webhook envelopes

| Source | Shape |
| --- | --- |
| Events API | Event fields include `severity`, `asset` and `facts` directly on the event. |
| Webhook | The same fields are inside `data`; `id`, `type`, `createdAt` and `projectId` remain at the top level. |

Use the event ID to deduplicate deliveries. The asset snapshot records the decision when the event happened.

## Endpoints

| Endpoint | Description |
| --- | --- |
| [`GET /projects/:projectId/events`](#list-events) | List project events, oldest first, after a cursor. |

## The event object

Asset events include the `asset` decision snapshot, and each event's `facts` is typed by its `type`. A [webhook connector](/docs/guides/webhooks#webhook-payload) delivers the same event wrapped as `{ id, type, createdAt, projectId, data }`, where `data` holds `severity`, `title`, `description`, `assetId`, `asset` and `facts`.

| Field | Type | Presence | Description |
| --- | --- | --- | --- |
| `id` | `string` | Required | Unique event ID |
| `projectId` | `string` | Required | ID of the project the event belongs to |
| `userId` | `string` | Optional, nullable | ID of the user who caused the event, if any |
| `type` | `string` | Required | Event type. See [Event types](/docs/api/events#event-types). |
| `severity` | `"low" \| "high"` | Required | Default severity of the event type |
| `title` | `string` | Required | Short human-readable summary |
| `description` | `string` | Optional, nullable | Longer human-readable explanation |
| `assetId` | `string` | Optional, nullable | ID of the asset the event is about, for asset events |
| `asset` | `object` | Optional, nullable | Snapshot of the asset's moderation decision when the event was created, for asset events |
| `asset.id` | `string` | Required | Asset ID |
| `asset.externalId` | `string` | Optional, nullable | The asset's `externalId`, if you set one |
| `asset.status` | `"uploading" \| "pending" \| "approved" \| "rejected" \| "error"` | Required | Asset status when the event was created |
| `asset.flagged` | `boolean` | Optional, nullable | The report's `flagged` value; `null` until the asset has been classified |
| `asset.moderationAction` | `"ignore" \| "alert" \| "hide" \| "purge"` | Optional, nullable | Moderation action applied to the asset, if any |
| `facts` | `object[]` | Required | Registered event facts. See [Event facts](/docs/api/events#event-facts). |
| `notify` | `boolean` | Optional, nullable | Whether the event triggers in-app notifications |
| `createdAt` | `integer` | Required | When the event was created, in milliseconds since the Unix epoch |
| `updatedAt` | `integer` | Optional | When the event was last updated, in milliseconds since the Unix epoch |
| `readAt` | `number` | Optional, nullable | When the event was marked as read in the portal, in milliseconds since the Unix epoch |

### Event types

| Event | Category | Severity | Meaning |
| --- | --- | --- | --- |
| `asset.uploaded` | Assets | `low` | A new asset was uploaded to the project. |
| `asset.classified` | Assets | `low` | Moderation finished and the asset has a decision. |
| `asset.flagged` | Moderation | `high` | Automated moderation flagged the asset for a policy violation. |
| `asset.approved` | Moderation | `low` | A reviewer approved a rejected asset in the portal. |
| `asset.rejected` | Moderation | `high` | A reviewer rejected an asset in the portal. |
| `asset.failed` | Assets | `high` | The asset could not be processed and is in the error state. |
| `usage.limit-reached` | Usage | `high` | The project owner used their free upload allowance; new uploads are blocked. |
| `project.member-joined` | Team | `low` | Someone accepted an invitation and joined the project. |
| `connector.failing` | Connectors | `high` | Deliveries to a connector kept failing, so it was turned off. |

### Event facts

| Event | Facts |
| --- | --- |
| `asset.uploaded` | `asset-name`: The asset's name when the event was created.<br />`asset-type`: The asset's type, such as `image` or `video`.<br />`file-format`: The uploaded file's format, such as `jpg`. |
| `asset.classified` | `asset-name`: The asset's name when the event was created.<br />`asset-type`: The asset's type, such as `image` or `video`.<br />`status`: The asset's status after classification: `approved`, `rejected` or `error`.<br />`moderation-action`: The action applied to flagged content (`alert`, `hide` or `purge`), or `null`. |
| `asset.flagged` | `asset-name`: The asset's name when the event was created.<br />`asset-type`: The asset's type, such as `image` or `video`.<br />`flagged-categories`: The matched parent policy categories. Inspect the asset's moderation report for qualifier detail.<br />`moderation-action`: The action applied to the flagged content (`alert`, `hide` or `purge`). |
| `asset.approved` | `asset-name`: The asset's name when the event was created.<br />`asset-type`: The asset's type, such as `image` or `video`.<br />`reviewer-user-id`: ID of the user who made the decision, or `null` when an API key made it.<br />`reviewer-name`: Name or email of the user who made the decision, when known.<br />`note`: The reviewer's note, or `null`. |
| `asset.rejected` | `asset-name`: The asset's name when the event was created.<br />`asset-type`: The asset's type, such as `image` or `video`.<br />`reviewer-user-id`: ID of the user who made the decision, or `null` when an API key made it.<br />`reviewer-name`: Name or email of the user who made the decision, when known.<br />`note`: The reviewer's reason for rejecting the asset, or `null`. |
| `asset.failed` | `asset-name`: The asset's name when the event was created.<br />`asset-type`: The asset's type, such as `image` or `video`.<br />`previous-status`: The asset's status before it failed, such as `pending`. |
| `usage.limit-reached` | `limit`: Number of free uploads in the allowance.<br />`used`: Number of free uploads used.<br />`owner-user-id`: ID of the project owner whose allowance was used. |
| `project.member-joined` | `member-user-id`: User ID of the new member.<br />`member-name`: The new member's name or email.<br />`role`: The member's role: `admin`, `member`, `viewer` or `custom`.<br />`invited-by-user-id`: User ID of the person who sent the invitation. |
| `connector.failing` | `connector-id`: ID of the failing connector.<br />`connector-name`: The connector's name.<br />`connector-type`: The connector's type, such as `webhook`, `slack` or `email`.<br />`consecutive-failures`: Number of deliveries in a row that failed after their retries.<br />`last-error`: Summary of the most recent failure, or `null`. |

## Endpoint details

### List Events

List project events, oldest first, after a cursor. A webhook connector delivers each event as `{ id, type, createdAt, projectId, data }`, where `data` holds the remaining event fields. Each event's `facts` are `{ key, value, url? }` entries registered for its `type` (see the event types in the webhooks guide). Omit `after` to get a cursor for the current time without any events, then pass each response's `cursor` as `after` until `hasMore` is false. Polling receives new events without a public webhook URL, for example during local development.

**Endpoint:** `GET https://api.calmlens.com/v1/projects/:projectId/events`

**Authentication:** API Key

#### Example request

```bash
curl -X GET "https://api.calmlens.com/v1/projects/{projectId}/events?limit=50" \
  -H "X-Api-Key: your-api-key"
```

#### Query parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `after` | `string` | No | The `cursor` from a previous response. Omit it to get a cursor for the current time without any events. |
| `limit` | `integer` | Yes | Maximum number of events to return |

#### Response

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `events` | `array<Event>` | Yes |  |
| `cursor` | `string` | Yes |  |
| `hasMore` | `boolean` | Yes |  |

#### Error codes

| Status | Code | Description |
| --- | --- | --- |
| 401 | <a id="get-events-error-auth-missing-token" />`auth/missing-token` | Authentication required. Send your project API key in the X-Api-Key header. |
| 401 | <a id="get-events-error-auth-invalid-api-key" />`auth/invalid-api-key` | The API key is invalid, revoked or expired. Create a key under Developer → API Keys. |
| 403 | <a id="get-events-error-auth-project-mismatch" />`auth/project-mismatch` | This API key belongs to a different project. Use a key created for the project that owns this resource. |
| 404 | <a id="get-events-error-project-not-found" />`project/not-found` | Project not found |
| 400 | <a id="get-events-error-events-invalid-cursor" />`events/invalid-cursor` | The events cursor is invalid. Use the cursor from a previous response, or omit it to start from now. |
