REVU Behavior Ingest API#
The endpoints REVU SDKs target to push captured events and crawler hits.
REVU SDKs capture, batch and POST. The server validates, attributes and stores. POST /v1/behavior/events takes behavioral events from the Web SDK, authenticated by a public write key (revu_pk_...) in the request body. POST /v1/behavior/server-events takes crawler hits from the customer's own web server, authenticated by a secret server key (revu_sk_...) in the Authorization header. Both are 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 to drive a client generator, a mock server, or an AI agent integration.
Servers#
https://api.revu.ai- Production ingesthttps://{host}- First-party ingest through your own domain (reverse proxy). See the web SDK first-party-ingest guide.
Authentication#
Each endpoint names the key it takes. There are no cookies and no session.
- Public write key (
revu_pk_...), sent in the request body asapi_key. A tenant identifier safe to embed in client bundles, it grants only append access to one organization's event log. - Secret server key (
revu_sk_prod_...,revu_sk_stg_...orrevu_sk_dev_...), sent asAuthorization: Bearer <key>. Each key belongs to one environment and writes only that environment. Create it in the REVU app under Settings > Touchpoints > your website > the environment's tab > Server capture. It is shown once. Send it from your server only: a request that carries anOriginheader is refused with 403.
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.
Authentication: the public write key in the request body (api_key).
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):
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#
{
"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#
{
"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's Origin is not allowed: it is not in the key's allowed-origins list, or it is not the touchpoint's domain for the key's environment (the development key also accepts localhost). |
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. |
POST /v1/behavior/server-events#
Report crawler hits from your server
Accept a batch of $crawl hits: crawler requests seen by the customer's own web server, including crawlers that never run JavaScript. Send it from your server only. Each hit is validated on its own, and the 202 response counts what was stored and what was dropped, by reason. Every valid hit is stored, and a page counts as crawled when the crawler got it: a GET answered with 2xx, 304 or an unknown status. robots.txt, llms.txt and sitemap reads are reported as site files, not pages.
Authentication: a secret server key in the Authorization: Bearer header.
Request body#
Content type: application/json
ServerEventsBody#
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
sdk | object | no | Optional sender identity, for example { "name": "@revu-ai/server", "version": "0.1.0" }. | |
sent_at | string | no | format: date-time | Optional. The sender's clock at send time. Every event's timestamp is corrected by the difference from REVU's clock. |
events | array<CrawlEvent> | yes | items 1-500 | Up to 500 hits per request (more answers 413). Each is validated on its own. |
ServerEventsBody.sdk#
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
name | string | no | length 0-40 | Sender name. |
version | string | no | length 0-40 | Sender version. |
CrawlEvent#
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
event_type | enum | yes | one of: $crawl | Always $crawl. |
event_id | string | yes | format: uuid | Client-generated UUID, unique per organization. A repeat, within the batch or already stored, counts as a duplicate and is stored once. |
timestamp | string | no | format: date-time | When the request reached your server, corrected by sent_at. Defaults to the receive time. More than 7 days old is rejected as invalid. More than 5 minutes ahead is replaced by the receive time. |
host | string | yes | length 0-255 | The host the client asked for. A port is ignored. It must be the key's environment's domain or a subdomain (the development key also accepts localhost). When domains overlap, the most specific one decides. Any other host is rejected as unknown_host. |
path | string | yes | pattern: ^/, length 0-1024 | The request path. Include only query parameters you have allowlisted, never personal data. |
method | string | no | pattern: ^[A-Za-z]{1,10}$ | HTTP method. Defaults to GET. Only a GET counts toward crawled pages. |
status | integer | null | no | min: 100, max: 599 | The final response status, or null when it is not known. |
user_agent | string | yes | length 1-512 | The request's User-Agent (longer values are truncated). REVU classifies it: a user agent that is not a crawler is rejected as not_crawler, and its IP is not stored. |
ip | string | null | no | The crawler's IP address, kept for crawler verification and cleared after 30 days. REVU checks it with the crawler vendor's documented method (its published IP ranges, or reverse DNS confirmed by a forward lookup). A hit whose address fails the check is a spoofed user agent: it is stored but left out of AI visibility. An invalid value is dropped, not the hit. | |
referer_host | string | null | no | length 0-255 | The host of the Referer header, never the full URL. |
Example requests#
A curl call (the SDK does this for you, batched and retried):
curl -X POST https://api.revu.ai/v1/behavior/server-events \
-H "Authorization: Bearer revu_sk_..." \
-H "Content-Type: application/json" \
-d '{"sdk":{"name":"@revu-ai/server","version":"0.1.0"},"sent_at":"2026-09-15T12:00:00.000Z","events":[{"event_type":"$crawl","event_id":"0b6d9f0e-6c1d-4a51-9a4e-2b7f1c0d8e11","timestamp":"2026-09-15T11:59:58.120Z","host":"www.acme.com","path":"/pricing","method":"GET","status":200,"user_agent":"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.1; +https://openai.com/gptbot","ip":"203.0.113.7","referer_host":null}]}'
One GPTBot page fetch#
{
"sdk": {
"name": "@revu-ai/server",
"version": "0.1.0"
},
"sent_at": "2026-09-15T12:00:00.000Z",
"events": [
{
"event_type": "$crawl",
"event_id": "0b6d9f0e-6c1d-4a51-9a4e-2b7f1c0d8e11",
"timestamp": "2026-09-15T11:59:58.120Z",
"host": "www.acme.com",
"path": "/pricing",
"method": "GET",
"status": 200,
"user_agent": "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.1; +https://openai.com/gptbot",
"ip": "203.0.113.7",
"referer_host": null
}
]
}
Responses#
| Status | Meaning |
|---|---|
202 | Accepted. The body counts the hits: accepted + duplicates + rejected always equals the number of events sent. |
401 | The server key is missing, malformed or unknown, a public write key (revu_pk_) was sent instead, or the key's environment is turned off on the touchpoint. Stop sending until the key or the environment is fixed. |
403 | The key was revoked, its touchpoint is archived or is not a website, or the request came from a browser (it carries an Origin header). Stop sending. |
413 | More than 500 events or more than 1 MB in one request. Send smaller batches. |
422 | The envelope failed validation: events is missing, empty or not an array. A malformed single event is not a 422: it is dropped and counted as invalid. |
429 | The key's rate limit (3,000 requests a minute) is exhausted, or this address sent too many unknown keys and is blocked for a minute. Carries a Retry-After header (seconds). Keep the batch and retry after it. |
500 | Unexpected server error. Retry once, then drop the batch. |
The 202 response carries this body:
ServerEventsResult#
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
accepted | integer | yes | min: 0 | New hits stored. |
duplicates | integer | yes | min: 0 | Hits already stored, or repeated within the batch. |
rejected | object | yes | Dropped hits, by reason. |
ServerEventsResult.rejected#
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
invalid | integer | yes | min: 0 | Malformed, or more than 7 days old. |
unknown_host | integer | yes | min: 0 | The host is not the key's environment's domain. |
not_crawler | integer | yes | min: 0 | The user agent does not classify as a crawler. |
Errors#
Every error 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. |