# Events API

The Events API takes events from your own backends and apps, beside the website tag.
One endpoint takes one event or a batch, says which it stored, and lets you retry safely.

If you are building a Flutter app, the [Flutter SDK](/docs/developers/flutter-sdk) sends through this API for you.

## The smallest call

```bash
curl https://konta.la/v1/events \
  -H "Authorization: Bearer $KONTALA_SERVER_KEY" \
  -d '{"event":"subscription_renewed"}'
```

```json
{"request_id": "0192f2c4-...", "accepted": 1, "rejected": [], "warnings": []}
```

Only `event` is required.
Use the endpoint shown on your property's **Events API** card in Markalit.

Keep a server key where your backend keeps its other secrets.
Never put it in code, or in a command line your shell history keeps.

## Keys

Send a key in the `Authorization` header as `Bearer <key>`.
A request with a key in the URL is refused, since URLs are kept in request logs.

| Key | Looks like | For | Notes |
| --- | --- | --- | --- |
| Write key | `pk_live_...` | An app | Public, as it is on every page that loads the tag. Every event must name its `device` and `session`. |
| Server key | `ksk_live_...` | A backend | Secret, shown once when it is made. Only it may set the visitor's IP, and a custom event sent with it may name no visitor. |

Admins make, label and revoke server keys on the property's **Events API** card.
A revoked key is refused within a minute.
A server key sent from a browser is refused, and Markalit tells you so that you can rotate it.

## Events

`event` is the name of what happened:

| `event` | What it is | Needs |
| --- | --- | --- |
| `page_view` | A web page where no tag runs, or an app's screen | `page` or `screen`, and a `device` and `session` |
| `click` | A click or a tap | `element`, a `device` and `session`, and clicks on for the property |
| Any other name | A custom event, such as `subscription_renewed` | The Events API in the property's capture rules |

Some names are Markalit's own and are refused as custom events, including `purchase`, `cart_add`, `checkout_started` and other Shopify and data layer names.
Names are compared ignoring case and punctuation, so `Page View` is refused and told to use `page_view`.

Other optional fields include `id`, `timestamp`, `device`, `session`, `user`, `org`, `consent`, `properties`, `page_type`, `page`, `screen` and `element`.

### Send a batch

```json
{
  "context": {"user_agent": "..."},
  "events": [
    {"event": "subscription_renewed", "user": "customer-42"},
    {"event": "invoice_paid", "user": "customer-42", "properties": {"amount": 49}}
  ]
}
```

A batch's `context` is the default for every event, and an event's own `context` overrides it field by field.

## Context

| Field | Is |
| --- | --- |
| `ip` | The visitor's IP address, server key only. Used for location, then dropped. |
| `user_agent` | The visitor's user agent. Without one, browser, operating system and device are unknown. |
| `page.url` | The page a click or custom event happened on. |
| `app` | `{id, version, build, screen}` for an app's events. |
| `locale` | A language tag, such as `nl-NL`. |
| `timezone` | An IANA zone, such as `Europe/Amsterdam`. |
| `display` | `{width, height}` in points. |
| `library` | `{name, version}` of what sent the event. |
| `consent` | The default consent for every event. |

## Join a browser visit

To attach a server event to the visit a browser is in, send the `pt_did` cookie's value as `device`, and the part of `pt_sid` before the dot as `session`.

## Responses

A 200 means the request was read.
Each event is stored or refused on its own.
`rejected` and `warnings` name each event by its `index` and `id`, with a `code` and a sentence.

| Status | Means | Do |
| --- | --- | --- |
| 200 | Read; `accepted` events were stored | Fix the events in `rejected`, if any. Events for a capture the property has off are refused by name, such as `clicks_off` or `source_not_captured` |
| 400 | `invalid_json`, `invalid_field`, `unknown_field`, `no_events`, `too_many_events`, `key_in_url` | Fix the request |
| 401 | `missing_key`, or `invalid_key` (unknown, revoked or another region's) | Check the key and endpoint |
| 403 | `secret_key_in_browser` | Rotate the key, and send from a server |
| 413 | `body_too_large`: over 1 MiB, decompressed | Send fewer events at once |
| 415 | `unsupported_encoding` | Send plain JSON, or gzip |
| 429 | `rate_limited` | Retry after `Retry-After` seconds |
| 503 | `unavailable`: the events were not stored | Retry after `Retry-After` seconds |

Retry a 429 or 503 with backoff, sending the same events with the same `id`s.
An event with its own `id` is stored once however often it arrives; one without may be stored twice.

## Limits

- 200 events and 1 MiB per request, decompressed. Send `Content-Encoding: gzip` for large batches.
- 1,000 events a second per key, with bursts of 10,000.
- Timestamps from 30 days ago to 24 hours ahead.
- 32 KB of properties per event.
