# Server-side ingest API

> Send pageviews and custom events from your backend with the server-side ingest API: the endpoint, ingest key authentication, event fields, limits and responses.

Send events from your server when the browser can't or shouldn't: payment
webhooks, signups confirmed by email, API products, backend jobs, or
pages served to clients without JavaScript.

## Endpoint

```http
POST https://privatusanalytics.com/api/events
Authorization: Bearer pik_…
Content-Type: application/json
```

```json
{
  "events": [
    {
      "type": "event",
      "name": "Purchase",
      "url": "https://example.com/checkout",
      "revenue": 49.99,
      "currency": "USD",
      "props": { "plan": "pro" },
      "user_agent": "Mozilla/5.0 (Macintosh; …) Safari/605.1.15",
      "ip": "203.0.113.7"
    }
  ]
}
```

Authenticate with the site's secret **ingest key** (`pik_…`), either as a
Bearer token or in an `X-Ingest-Key` header. Each key belongs to one site.
See [Ingest keys](/docs/server-side/ingest-keys).

## Event fields

| Field | Required | Description |
|---|---|---|
| `type` | Yes | `pageview` or `event` (`custom` is an alias). `engagement`, `vital` and `click` are also accepted but need an existing visit |
| `name` | For events | Event name. Longer names are cut to 120 characters. For `vital`, one of `LCP`, `INP`, `CLS`, `FCP` or `TTFB` |
| `url` | Yes | Full `http(s)` URL of the page. Its hostname must be one of the site's domains (or a subdomain of one) |
| `referrer` | No | Where the visitor came from, which drives sources and channels |
| `props` | No | Up to 30 [properties](/docs/events/properties). More are ignored |
| `revenue` | No | Amount in major units, e.g. `49.99` ([Revenue](/docs/events/revenue)) |
| `currency` | No | ISO 4217 code. Defaults to the site currency |
| `timestamp` | No | ISO 8601. Must be within the last **72 hours**. Otherwise (or if missing or in the future) the time of receipt is used |
| `user_agent` | In practice, yes | The **visitor's** browser User-Agent. Missing or library User-Agents are dropped as bots (see below) |
| `ip` | No | The **visitor's** IP. Used in memory for the daily visit key, the data-center check and IP-range [traffic rules](/docs/privacy/traffic-rules), then dropped. It is not used for location |
| `session_hint` | No | An opaque string that groups your server events into one visit. See [Sessions](/docs/server-side/sessions) |
| `language` | No | e.g. `en-US`. Cut to 16 characters |
| `screen_width` | No | Pixels, stored as a size bucket (`xs`, `sm`, `md`, `lg` or `xl`) |
| `dnt`, `gpc` | No | `true` when the visitor sent Do Not Track or Global Privacy Control. The event is dropped if the site honors that signal ([DNT and GPC](/docs/privacy/dnt-gpc)) |
| `hash_mode` | No | `true` to keep the URL's `#` fragment as part of the page path |
| `engaged_ms` | No | For `engagement`: engaged time in milliseconds, capped at 4 hours |
| `scroll_depth` | No | For `engagement`: scroll depth from 0 to 100, rounded down to a multiple of 10 |
| `value` | No | For `vital`: the measured value |
| `selector` | No | For `click`: the clicked element's selector, cut to 512 characters |

**Country.** The country is read from the request that delivers the batch,
not from the `ip` field. Server-side events therefore carry the country of
the server that sent them, not the visitor's.

The short keys the browser tracker uses (`t`, `n`, `u`, `r`, `p`…) are
accepted too. See [What the tracker sends](/docs/tracker/payload).

## Limits

- Up to **100 events** per request. More returns `422`.
- The request body must be at most **32 KB**. A larger body, or one that
  isn't a JSON object, is read as empty: you get `202` with an empty `data`
  list and nothing is recorded. Split big batches.
- Up to **600 requests per minute** per ingest key. More returns `429`
  with a `Retry-After` header.
- Everything else (property limits, name length, PII scrubbing, path
  masking, traffic rules, bot filtering) works exactly as for browser hits.

## Response

`202 Accepted` with one result per event, in order:

```json
{
  "data": [
    { "index": 0, "status": "accepted", "reason": null },
    { "index": 1, "status": "dropped", "reason": "bot_automation" }
  ]
}
```

`accepted` means the event was queued. It is written in a batch and
normally appears in the dashboard within seconds. `dropped` events aren't stored and don't count toward
usage. Reasons:

| Reason | Meaning |
|---|---|
| `invalid` | A field has the wrong type: a text field (`url`, `name`, `referrer`, `ip`, `user_agent`, `session_hint` and so on) is an object, a list or a boolean, or a number field (`screen_width`) isn't a finite number |
| `bad_type` | `type` isn't one of the accepted values |
| `bad_url` | `url` is missing or not an `http(s)` URL |
| `foreign_host` | The URL's hostname isn't one of the site's domains |
| `privacy_signal` | The event carried DNT/GPC (`dnt`/`gpc` fields) and the site honors it |
| `bot_empty_user_agent` | No `user_agent` |
| `bot_known_bot` | A known crawler User-Agent |
| `bot_automation` | An automation or HTTP-library User-Agent (see [Bot rules](/docs/server-side/bots)) |
| `bot_datacenter` | The `ip` belongs to a cloud or hosting network |
| `bot_referrer_spam` | The referrer is on the referrer-spam list |
| `rule_<list>` | Blocked by a [traffic rule](/docs/privacy/traffic-rules). `<list>` is `hostnames`, `paths`, `referrers`, `countries`, `ip_ranges`, `user_agents` or `events` |
| `vital_sampled` | A Web Vital outside the site's sample rate, or with an unknown name |
| `no_visit` | An engagement, vital or click hit with no visit to attach to |
| `paused` | Collection is paused for the workspace by Privatus Analytics support |
| `limit_reached` | The workspace reached its monthly event limit, so pageviews and custom events are not recorded until the reset or an upgrade (see [Monthly limit](/docs/billing/overage)) |

Errors:

| Status | Body `error.code` | When |
|---|---|---|
| `401` | `unauthorized` | Missing or unknown ingest key |
| `422` | `validation_failed` | More than 100 events, or `events` isn't a list of objects |
| `429` | `rate_limited` | More than 600 requests in a minute with one key |

## Next

- [Examples](/docs/server-side/examples) in curl, Node.js, Python, PHP, Go,
  Ruby and Next.js.
- The browser's [no-JavaScript pixel](/docs/install/pixel) for pages
  without JavaScript.
