# Track events in a batch

`POST /v1/events/batch`

Requires an API key with the `events:write` scope. [About keys and scopes](/docs/api/authentication)

Each entry takes its own `idempotencyKey`, so retrying the whole batch never acts twice on an entry that already went through. [About idempotency](/docs/api/idempotency)

Up to 100 events in one request, each stored exactly as `POST /v1/events/track` would, in order, under its own `idempotencyKey` in place of the header. One event refused does not stop the rest: its entry carries the error a single track would have answered, and a key already used answers the stored event with `replayed: true`. Retry a failed batch whole; the keys stop anything being stored twice. Counts against the rate limit as one request per event.

## Request body

- `events` (object[], required, at least 1 item, at most 100 items)
  - `idempotencyKey` (string, required, 8–255 characters) — As the `Idempotency-Key` header of a single track: one per event you mean.
  - `event` (object, required)
    - One of: **By contact id**
      - `contactId` (string, required, uuid) — Kirora's id for the contact.
      - `eventName` (string, required, 1–128 characters) — Case-sensitive, up to 128 characters; `contact.*` is reserved.
      - `properties` (object, optional, default `{}`) — Free-form details of the event; at most 16 KB, keys up to 64 characters.
      - `occurredAt` (string, optional, date-time) — When it happened; defaults to now. At most 24 hours ahead.
      - `context` (EventContextInput, optional) — Where and on what it happened, beside the properties.
        - `sessionId` (string, optional, up to 128 characters) — The visit, from the SDK.
        - `locale` (string, optional, up to 35 characters) — As BCP 47, `en-GB`.
        - `timezone` (string, optional, up to 64 characters) — As IANA, `Europe/Berlin`.
        - `page` (object, optional) — Web: the page, without its query string or fragment.
        - `campaign` (object, optional) — Web: the `utm_*` parameters of the page.
        - `device` (object, optional)
        - `library` (object, optional)
        - `ip` (string, optional, up to 64 characters)
        - `userAgent` (string, optional, up to 1024 characters)
    - One of: **By external id**
      - `externalId` (string, required, 1–255 characters) — Your own id for the contact, as set with identify.
      - `eventName` (string, required, 1–128 characters) — Case-sensitive, up to 128 characters; `contact.*` is reserved.
      - `properties` (object, optional, default `{}`) — Free-form details of the event; at most 16 KB, keys up to 64 characters.
      - `occurredAt` (string, optional, date-time) — When it happened; defaults to now. At most 24 hours ahead.
      - `context` (EventContextInput, optional) — Where and on what it happened, beside the properties.
        - `sessionId` (string, optional, up to 128 characters) — The visit, from the SDK.
        - `locale` (string, optional, up to 35 characters) — As BCP 47, `en-GB`.
        - `timezone` (string, optional, up to 64 characters) — As IANA, `Europe/Berlin`.
        - `page` (object, optional) — Web: the page, without its query string or fragment.
        - `campaign` (object, optional) — Web: the `utm_*` parameters of the page.
        - `device` (object, optional)
        - `library` (object, optional)
        - `ip` (string, optional, up to 64 characters)
        - `userAgent` (string, optional, up to 1024 characters)
    - One of: **By email**
      - `email` (string, required) — The contact's email address, matched trimmed and without case.
      - `eventName` (string, required, 1–128 characters) — Case-sensitive, up to 128 characters; `contact.*` is reserved.
      - `properties` (object, optional, default `{}`) — Free-form details of the event; at most 16 KB, keys up to 64 characters.
      - `occurredAt` (string, optional, date-time) — When it happened; defaults to now. At most 24 hours ahead.
      - `context` (EventContextInput, optional) — Where and on what it happened, beside the properties.
        - `sessionId` (string, optional, up to 128 characters) — The visit, from the SDK.
        - `locale` (string, optional, up to 35 characters) — As BCP 47, `en-GB`.
        - `timezone` (string, optional, up to 64 characters) — As IANA, `Europe/Berlin`.
        - `page` (object, optional) — Web: the page, without its query string or fragment.
        - `campaign` (object, optional) — Web: the `utm_*` parameters of the page.
        - `device` (object, optional)
        - `library` (object, optional)
        - `ip` (string, optional, up to 64 characters)
        - `userAgent` (string, optional, up to 1024 characters)

## Returns

Returns `200 OK` with a `TrackEventsResult` in `data`. Otherwise returns an error; [Errors](/docs/api/errors) lists every code and which to retry.

- `results` (object[], required)
  - One of: **Event recorded**
    - `idempotencyKey` (string, required)
    - `replayed` (boolean, required) — True when the key was used before and the stored event is returned.
    - `event` (TrackedEvent, required)
      - `id` (string, required, uuid)
      - `occurredAt` (string, required, date-time)
  - One of: **Event refused**
    - `idempotencyKey` (string, required)
    - `error` (object, required)
      - `code` (string, required, one of `BAD_REQUEST`, `ALREADY_EXISTS`, `CONFLICT`, `IN_USE`, `FORBIDDEN`, `INTERNAL_ERROR`, `NOT_FOUND`, `RATE_LIMITED`, `REVISION_CONFLICT`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED`, `VALIDATION_ERROR`, `API_KEY_IP_NOT_ALLOWED`, `AGENT_CONFIRMATION_REQUIRED`, `EMBED_ORIGIN_NOT_ALLOWED`, `WORKSPACE_SUSPENDED`, `WORKSPACE_DELETION_PENDING`, `CONTACT_SEND_IN_PROGRESS`, `EXPORT_UNAVAILABLE`, `NOTHING_TO_EXPORT`, `EVENT_NAME_LIMIT_REACHED`, `PLAN_LIMIT_REACHED`, `PLAN_FEATURE_UNAVAILABLE`, `CAMPAIGN_AUDIENCE_CONFLICT`, `EMPTY_AUDIENCE`, `INVALID_CAMPAIGN_CONTENT`, `DRAFT_INCOMPLETE`, `INVALID_SCHEDULE`, `UNSUPPORTED_CHANNEL`, `POSTAL_ADDRESS_MISSING`, `CONSENT_MISSING`, `SUPPRESSED`, `SIGNUP_BLOCKED`, `INVALID_SENDER_ADDRESS`, `SENDING_DOMAIN_DMARC_MISSING`, `SENDING_DOMAIN_IDENTITY_CONFLICT`, `SENDING_DOMAIN_UNVERIFIED`, `IDEMPOTENCY_KEY_REUSED`, `CONTENT_NOT_RETAINED`, `RECIPIENT_EMAIL_MISSING`, `TEMPLATE_NOT_PUBLISHED`, `TEMPLATE_ENVELOPE_INCOMPLETE`, `PUSH_APP_NOT_READY`, `PUSH_DESTINATION_MISSING`)
      - `message` (string, required)
      - `details` (object, optional)
      - `requestId` (string, optional)

## Example: Two events in one request

```bash
curl -X POST https://api.kirora.io/v1/events/batch \
  -H "Authorization: Bearer $KIRORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "idempotencyKey": "order-1001-completed",
        "event": {
          "externalId": "customer-4821",
          "eventName": "Order Completed",
          "properties": {
            "order_id": "1001",
            "total": 45.5,
            "currency": "USD"
          },
          "occurredAt": "2026-10-01T09:30:00Z"
        }
      },
      {
        "idempotencyKey": "order-1002-completed",
        "event": {
          "externalId": "customer-5307",
          "eventName": "Order Completed",
          "properties": {
            "order_id": "1002",
            "total": 18.5,
            "currency": "USD"
          },
          "occurredAt": "2026-10-01T09:30:00Z"
        }
      }
    ]
  }'
```

Response `200`:

```json
{
  "data": {
    "results": [
      {
        "idempotencyKey": "order-1001-completed",
        "replayed": false,
        "event": {
          "id": "0192f3c0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
          "occurredAt": "2026-10-01T09:30:00.000Z"
        }
      },
      {
        "idempotencyKey": "order-1002-completed",
        "replayed": false,
        "event": {
          "id": "0192f3c0-2b3c-7d4e-9f50-6b7c8d9e0f1a",
          "occurredAt": "2026-10-01T09:30:00.000Z"
        }
      }
    ]
  }
}
```

## Example: One event fails, the other is recorded

```bash
curl -X POST https://api.kirora.io/v1/events/batch \
  -H "Authorization: Bearer $KIRORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "idempotencyKey": "order-1003-completed",
        "event": {
          "contactId": "0192f3a1-7c2e-7d4b-9a10-3e5f6a7b8c9d",
          "eventName": "Order Completed",
          "occurredAt": "2026-10-01T09:30:00Z"
        }
      },
      {
        "idempotencyKey": "order-1004-completed",
        "event": {
          "email": "nobody@example.com",
          "eventName": "Order Completed",
          "occurredAt": "2026-10-01T09:30:00Z"
        }
      }
    ]
  }'
```

Response `200`:

```json
{
  "data": {
    "results": [
      {
        "idempotencyKey": "order-1003-completed",
        "replayed": false,
        "event": {
          "id": "0192f3c0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
          "occurredAt": "2026-10-01T09:30:00.000Z"
        }
      },
      {
        "idempotencyKey": "order-1004-completed",
        "error": {
          "code": "NOT_FOUND",
          "message": "Contact not found"
        }
      }
    ]
  }
}
```
