# Events API

Record something that happened in your own app, such as a purchase or a signup, so an automation can react to it. Events are attached to a contact. All routes need a bearer API key (see [API Overview](/docs/api/overview)).

## Record an event

```text
POST /api/v1/events
```

```json
{
  "name": "order_placed",
  "email": "ada@example.com",
  "properties": { "order_id": "1042", "items": 3 },
  "value": 59.9,
  "currency": "USD",
  "externalId": "order-1042"
}
```

| Field        | Notes                                                                                                          |
| ------------ | -------------------------------------------------------------------------------------------------------------- |
| `name`       | Required. See name rules below                                                                                 |
| `email`      | The contact's email. Provide `email` or `contactId`                                                            |
| `contactId`  | The contact's id. Used instead of `email` when both are given                                                  |
| `properties` | Optional object. At most 50 keys and 8 KB when serialized                                                      |
| `value`      | Optional money amount in major units (for example dollars). Stored as whole minor units, so 19.99 becomes 1999 |
| `valueCents` | Optional money amount as a whole number of minor units. Wins over `value` if both are sent                     |
| `currency`   | Optional three-letter code, stored uppercase. Anything else is ignored                                         |
| `externalId` | Optional. Your own unique id for the event, used to make retries safe. Truncated to 200 characters             |

### Event names

Names are normalized to lowercase snake_case and cut to 64 characters, so `Order Placed`, `order-placed` and `orderPlaced` all become `order_placed`. An automation trigger name is normalized the same way, so it still matches.

### Contacts

- With `contactId`, the contact must be in your workspace or you get `404`.
- With `email`, an existing contact is used. If there is none, a new contact is created with status `PENDING`, never `SUBSCRIBED`. An event is not a marketing opt-in. See [Double Opt-In](/docs/concepts/double-opt-in).

### Retries

If you send an `externalId` that already exists in your workspace, nothing new is recorded and no automation runs again. You get `200` with `{ "id": "...", "duplicate": true, "createdAt": "..." }`.

### Response

A new event returns `201`:

```json
{ "data": { "id": "clx...", "name": "order_placed", "createdAt": "2026-10-04T12:00:00.000Z" } }
```

Recording the event also starts any live automation with a custom event trigger of that name for the contact, and passes the event to your lead scoring rules. If starting the automation fails, the event is still recorded and the request still succeeds.

Errors: `400` if the body is not valid JSON, `422` for a missing name, a missing email and contactId, an invalid email, bad `properties`, or a bad money value.

## List events

```text
GET /api/v1/events
```

| Query       | Meaning                                   |
| ----------- | ----------------------------------------- |
| `limit`     | Page size, default 20, maximum 100        |
| `after`     | Cursor from the previous page             |
| `name`      | Only events with exactly this stored name |
| `contactId` | Only events for this contact              |

Items have `id`, `name`, `properties`, `valueCents`, `currency`, `contactId` and `createdAt`, newest first.

Because `name` is matched exactly, filter with the normalized form, for example `order_placed`.
