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

# Events & goals

> Record what contacts did, including conversions with a value, and start journeys that listen for an event name.

An **event** is something a contact did, with a name such as `order.completed`. Give it a **`goalType`** and it becomes a conversion (a goal), which can also carry money.

| Endpoint                 | Scope          | Limit                                        |
| ------------------------ | -------------- | -------------------------------------------- |
| `POST /v1/events`        | `events:write` | 300 requests/minute                          |
| `POST /v1/events/import` | `events:write` | 60 requests/minute, up to 50 events per call |

The browser version of the same call is [`POST /public/events`](/api-reference/website-tracking).

## Recording a purchase

```bash theme={null}
curl -X POST https://instantcampaign.ai/api/v1/events \
  -H "Authorization: Bearer ic_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "order.completed",
    "email": "jane@example.com",
    "goalType": "purchase",
    "value": 42.50,
    "currency": "GBP",
    "externalId": "order-10422",
    "properties": { "sku": "MUG-1" }
  }'
```

The response is `201 { id, contactId, deduplicated: false, status: "recorded" }`. Sending the same `externalId` again answers `200` with `status: "duplicate"`.

## Rules

* `event` must be lowercase letters, digits, `.`, `_`, `/` or `-`, starting with a letter or digit, up to 100 characters.
* Identify the contact with `email` or `contactId`, and send one of the two.
* `goalType` is one of `purchase`, `subscription`, `lead`, `signup`, `booking`, `trial` or `other`. `purchase` and `subscription` require a `value`. A `value` is only accepted together with a `goalType`, and a negative value records a refund.
* `currency` is a 3-letter ISO code.
* `occurredAt` is an ISO date-time and defaults to now.

<Note>
  If the email or ID matches no contact, the event is still stored with `contactId: null`. That is not an error, and no contact is created. An event with no contact starts no journey and fires no webhook. Create the contact first if you need it.
</Note>

## What an event does

When the event matches a contact:

* It starts every active journey whose **API Event** trigger listens for that event name, and every website-event journey that listens for it. See [Journeys](/api-reference/journeys).
* It fires the `EVENT_RECORDED` webhook.
* It updates the contact's last-activity time.
* It feeds goal-based segment rules, such as lifetime value, purchase count and last purchase.

## Batch import

`POST /v1/events/import` takes `{ "events": [ … ] }` with 1 to 50 events. The whole body is checked against the event schema first, so one malformed row fails the entire call with `400 Invalid input`. After that, each row is recorded on its own and reported by `index`. A row that is missing `email`/`contactId`, or that breaks a goal rule, gets an `error` without affecting the others. The call answers `400` only when every row failed.

```json theme={null}
{ "recorded": 48, "duplicates": 1, "failed": 1, "results": [ { "index": 0, "id": "…", "contactId": "…", "deduplicated": false } ] }
```
