# REVU Behavior Ingest API

> The single endpoint every REVU SDK targets to push captured events.

REVU SDKs capture behavioral events client-side, batch them, and POST them to this endpoint. The SDK only captures; the server validates, attributes, and stores. Authentication is a public, browser-embeddable write key (`revu_pk_...`) carried in the request body, not a secret header: it is a tenant identifier that grants only append access to one organization's event log. Ingest is idempotent on (organization, event_id), so retried batches are de-duplicated server-side.

This page is generated from the OpenAPI specification. Download it as
[openapi.json](/api/openapi.json) to drive a client generator, a mock
server, or an AI agent integration.

## Servers

- `https://api.revu.ai` - Production ingest
- `https://{host}` - First-party ingest through your own domain (reverse proxy). See the web SDK first-party-ingest guide.

## Authentication

Authentication is a public write key (`revu_pk_...`) sent in the request
body as `api_key`, not an `Authorization` header. The key is a tenant
identifier safe to embed in client bundles; it grants only append access to
one organization's event log. There are no cookies and no session.

## POST /v1/behavior/events

**Ingest a batch of behavioral events**

Accept a batch of captured events from a REVU SDK. The body carries the public `api_key` and a `batch` of 1 to 200 events. Returns 204 with an empty body on success. The SDK sends `Content-Type: application/json`; on page unload it uses `navigator.sendBeacon` with an `application/json` Blob so the terminal batch still parses.

### Request body

Content type: `application/json`

#### IngestBody

| Field | Type | Required | Constraints | Description |
| ----- | ---- | -------- | ----------- | ----------- |
| `api_key` | string | yes | pattern: `^revu_pk_[A-Za-z0-9_-]{8,120}$` | Public ingest write key (prefix revu_pk_). |
| `batch` | array<BehaviorEvent> | yes | items 1-200 | Up to 200 events per request. |

#### BehaviorEvent

| Field | Type | Required | Constraints | Description |
| ----- | ---- | -------- | ----------- | ----------- |
| `event_id` | string | yes | format: uuid | Client-generated UUID. Unique per organization; the idempotency / dedupe key for retried batches. |
| `anonymous_id` | string | yes | length 1-128 | First-party anonymous visitor (device) id assigned by the SDK. |
| `user_id` | string \| null | no | length 0-128 | Identified user id, if any. Null when the visitor is not yet identified. |
| `session_id` | string | yes | length 1-128 | Per-session id. |
| `sequence_no` | integer | yes | min: 0 | Per-page-load monotonic counter starting at 0. Gaps indicate dropped events within a page load. |
| `platform` | enum | yes | one of: web, ios, android | Capture platform. |
| `event_type` | string | yes | length 1-120 | Event type: '$pageview', '$autocapture', or a custom name. |
| `screen` | string | yes | length 0-2048 | Route / path at capture time. |
| `fingerprint` | object \| null | no |  | Element descriptor, present for $autocapture interactions. Used server-side to name the interacted element. |
| `properties` | object | yes |  | Event payload: capture-layer fields (path, url, depth_percent, form structure, ...) plus caller-supplied custom properties. Client-side masked, PII-free, never input values. |
| `context` | object | no |  | Engine environment bucket (unprefixed): user_agent, language, timezone, screen_* / viewport_*, online, connection_*, environment, sdk_version, consent, gpc, sample_rate, and first/last-touch attribution. Optional: a client that omits it still validates. |
| `device_time` | string | yes | format: date-time | ISO-8601 capture timestamp from the client device. |

##### BehaviorEvent.fingerprint

| Field | Type | Required | Constraints | Description |
| ----- | ---- | -------- | ----------- | ----------- |
| `tag` | string | yes |  | Element tag, e.g. 'button'. |
| `text` | string \| null | no |  | Visible text (truncated; masked if sensitive). |
| `role` | string \| null | no |  | ARIA role / type. |
| `id` | string \| null | no |  | Element id, if present. |
| `classes` | array \| null | no |  | Class list. |
| `selector` | string | yes |  | Best-effort CSS selector (fragile; a tiebreaker). |
| `ordinal` | number | no |  | Position among siblings. |

### Example requests

A `curl` call (the SDK does this for you, batched and retried):

```bash
curl -X POST https://api.revu.ai/v1/behavior/events \
  -H "Content-Type: application/json" \
  -d '{"api_key":"revu_pk_example12345678","batch":[{"event_id":"f1d2c3b4-a5e6-4789-9abc-de0123456789","anonymous_id":"a0e1d2c3-b4a5-4677-8899-aabbccddeeff","user_id":null,"session_id":"11112222-3333-4444-5555-666677778888","sequence_no":0,"platform":"web","event_type":"$pageview","screen":"/pricing","properties":{"path":"/pricing","url":"https://acme.com/pricing"},"context":{"user_agent":"Mozilla/5.0 ...","language":"en-US","timezone":"Europe/Berlin","environment":"production","sdk_version":"0.1.0"},"device_time":"2026-06-21T18:04:05.123Z"}]}'
```

#### A single $pageview event

```json
{
  "api_key": "revu_pk_example12345678",
  "batch": [
    {
      "event_id": "f1d2c3b4-a5e6-4789-9abc-de0123456789",
      "anonymous_id": "a0e1d2c3-b4a5-4677-8899-aabbccddeeff",
      "user_id": null,
      "session_id": "11112222-3333-4444-5555-666677778888",
      "sequence_no": 0,
      "platform": "web",
      "event_type": "$pageview",
      "screen": "/pricing",
      "properties": {
        "path": "/pricing",
        "url": "https://acme.com/pricing"
      },
      "context": {
        "user_agent": "Mozilla/5.0 ...",
        "language": "en-US",
        "timezone": "Europe/Berlin",
        "environment": "production",
        "sdk_version": "0.1.0"
      },
      "device_time": "2026-06-21T18:04:05.123Z"
    }
  ]
}
```

#### An $autocapture click with an element fingerprint

```json
{
  "api_key": "revu_pk_example12345678",
  "batch": [
    {
      "event_id": "0a1b2c3d-4e5f-4061-8273-8495a6b7c8d9",
      "anonymous_id": "a0e1d2c3-b4a5-4677-8899-aabbccddeeff",
      "user_id": "user_8675309",
      "session_id": "11112222-3333-4444-5555-666677778888",
      "sequence_no": 4,
      "platform": "web",
      "event_type": "$autocapture",
      "screen": "/pricing",
      "fingerprint": {
        "tag": "button",
        "text": "Start free trial",
        "role": "button",
        "selector": "main > section.cta button.primary",
        "ordinal": 0
      },
      "properties": {
        "path": "/pricing"
      },
      "context": {
        "environment": "production",
        "sdk_version": "0.1.0"
      },
      "device_time": "2026-06-21T18:04:11.880Z"
    }
  ]
}
```

### Responses

| Status | Meaning |
| ------ | ------- |
| `204` | Batch accepted. Empty body. Already-seen event_ids in the batch are de-duplicated. |
| `401` | The api_key is unknown, or its environment is inactive. |
| `403` | The api_key is valid but the request Origin is not in the key's allowed-origins allowlist. |
| `422` | The body failed schema validation (a missing or malformed field, or a batch outside 1 to 200 events). |
| `429` | The api_key's per-minute rate limit is exhausted. Carries a Retry-After header (seconds). SDK transport treats this as a backoff-and-retry signal; events stay queued. |
| `500` | Unexpected server error. The SDK backs off and retries; events stay queued client-side. |

Every non-204 response carries this body:

#### Error

| Field | Type | Required | Constraints | Description |
| ----- | ---- | -------- | ----------- | ----------- |
| `error` | string | yes |  | Human-readable error message. |
| `code` | string | yes |  | Stable machine-readable code: UNAUTHORIZED, FORBIDDEN, VALIDATION_ERROR, RATE_LIMIT_EXCEEDED, or OPERATION_FAILED. |
| `timestamp` | string | yes | format: date-time | ISO-8601 time the error was produced. |
