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 ingest
  • https://{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 as api_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_... or revu_sk_dev_...), sent as Authorization: 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 an Origin header 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#

FieldTypeRequiredConstraintsDescription
api_keystringyespattern: ^revu_pk_[A-Za-z0-9_-]{8,120}$Public ingest write key (prefix revu_pk_).
batcharray<BehaviorEvent>yesitems 1-200Up to 200 events per request.

BehaviorEvent#

FieldTypeRequiredConstraintsDescription
event_idstringyesformat: uuidClient-generated UUID. Unique per organization; the idempotency / dedupe key for retried batches.
anonymous_idstringyeslength 1-128First-party anonymous visitor (device) id assigned by the SDK.
user_idstring | nullnolength 0-128Identified user id, if any. Null when the visitor is not yet identified.
session_idstringyeslength 1-128Per-session id.
sequence_nointegeryesmin: 0Per-page-load monotonic counter starting at 0. Gaps indicate dropped events within a page load.
platformenumyesone of: web, ios, androidCapture platform.
event_typestringyeslength 1-120Event type: '$pageview', '$autocapture', or a custom name.
screenstringyeslength 0-2048Route / path at capture time.
fingerprintobject | nullnoElement descriptor, present for $autocapture interactions. Used server-side to name the interacted element.
propertiesobjectyesEvent payload: capture-layer fields (path, url, depth_percent, form structure, ...) plus caller-supplied custom properties. Client-side masked, PII-free, never input values.
contextobjectnoEngine 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_timestringyesformat: date-timeISO-8601 capture timestamp from the client device.
BehaviorEvent.fingerprint#
FieldTypeRequiredConstraintsDescription
tagstringyesElement tag, e.g. 'button'.
textstring | nullnoVisible text (truncated; masked if sensitive).
rolestring | nullnoARIA role / type.
idstring | nullnoElement id, if present.
classesarray | nullnoClass list.
selectorstringyesBest-effort CSS selector (fragile; a tiebreaker).
ordinalnumbernoPosition 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#

StatusMeaning
204Batch accepted. Empty body. Already-seen event_ids in the batch are de-duplicated.
401The api_key is unknown, or its environment is inactive.
403The 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).
422The body failed schema validation (a missing or malformed field, or a batch outside 1 to 200 events).
429The 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.
500Unexpected 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#

FieldTypeRequiredConstraintsDescription
sdkobjectnoOptional sender identity, for example { "name": "@revu-ai/server", "version": "0.1.0" }.
sent_atstringnoformat: date-timeOptional. The sender's clock at send time. Every event's timestamp is corrected by the difference from REVU's clock.
eventsarray<CrawlEvent>yesitems 1-500Up to 500 hits per request (more answers 413). Each is validated on its own.
ServerEventsBody.sdk#
FieldTypeRequiredConstraintsDescription
namestringnolength 0-40Sender name.
versionstringnolength 0-40Sender version.

CrawlEvent#

FieldTypeRequiredConstraintsDescription
event_typeenumyesone of: $crawlAlways $crawl.
event_idstringyesformat: uuidClient-generated UUID, unique per organization. A repeat, within the batch or already stored, counts as a duplicate and is stored once.
timestampstringnoformat: date-timeWhen 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.
hoststringyeslength 0-255The 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.
pathstringyespattern: ^/, length 0-1024The request path. Include only query parameters you have allowlisted, never personal data.
methodstringnopattern: ^[A-Za-z]{1,10}$HTTP method. Defaults to GET. Only a GET counts toward crawled pages.
statusinteger | nullnomin: 100, max: 599The final response status, or null when it is not known.
user_agentstringyeslength 1-512The 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.
ipstring | nullnoThe 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_hoststring | nullnolength 0-255The 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#

StatusMeaning
202Accepted. The body counts the hits: accepted + duplicates + rejected always equals the number of events sent.
401The 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.
403The 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.
413More than 500 events or more than 1 MB in one request. Send smaller batches.
422The 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.
429The 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.
500Unexpected server error. Retry once, then drop the batch.

The 202 response carries this body:

ServerEventsResult#

FieldTypeRequiredConstraintsDescription
acceptedintegeryesmin: 0New hits stored.
duplicatesintegeryesmin: 0Hits already stored, or repeated within the batch.
rejectedobjectyesDropped hits, by reason.
ServerEventsResult.rejected#
FieldTypeRequiredConstraintsDescription
invalidintegeryesmin: 0Malformed, or more than 7 days old.
unknown_hostintegeryesmin: 0The host is not the key's environment's domain.
not_crawlerintegeryesmin: 0The user agent does not classify as a crawler.

Errors#

Every error response carries this body:

Error#

FieldTypeRequiredConstraintsDescription
errorstringyesHuman-readable error message.
codestringyesStable machine-readable code: UNAUTHORIZED, FORBIDDEN, VALIDATION_ERROR, RATE_LIMIT_EXCEEDED, or OPERATION_FAILED.
timestampstringyesformat: date-timeISO-8601 time the error was produced.