{
  "openapi": "3.1.0",
  "info": {
    "title": "REVU Behavior Ingest API",
    "version": "1.0.0",
    "summary": "The single endpoint every REVU SDK targets to push captured events.",
    "description": "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.",
    "license": {
      "name": "Apache-2.0"
    }
  },
  "servers": [
    {
      "url": "https://api.revu.ai",
      "description": "Production ingest"
    },
    {
      "url": "https://{host}",
      "description": "First-party ingest through your own domain (reverse proxy). See the web SDK first-party-ingest guide.",
      "variables": {
        "host": {
          "default": "api.revu.ai"
        }
      }
    }
  ],
  "tags": [
    {
      "name": "Ingest",
      "description": "Append captured behavioral events to your organization's log."
    }
  ],
  "paths": {
    "/v1/behavior/events": {
      "post": {
        "tags": [
          "Ingest"
        ],
        "operationId": "ingestBehaviorEvents",
        "summary": "Ingest a batch of behavioral events",
        "description": "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.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IngestBody"
              },
              "examples": {
                "pageview": {
                  "summary": "A single $pageview event",
                  "value": {
                    "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"
                      }
                    ]
                  }
                },
                "autocaptureClick": {
                  "summary": "An $autocapture click with an element fingerprint",
                  "value": {
                    "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": {
          "204": {
            "description": "Batch accepted. Empty body. Already-seen event_ids in the batch are de-duplicated."
          },
          "401": {
            "description": "The api_key is unknown, or its environment is inactive.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or unknown api_key.",
                  "code": "UNAUTHORIZED",
                  "timestamp": "2026-06-21T18:04:05.200Z"
                }
              }
            }
          },
          "403": {
            "description": "The api_key is valid but the request Origin is not in the key's allowed-origins allowlist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Origin not allowed for this key.",
                  "code": "FORBIDDEN",
                  "timestamp": "2026-06-21T18:04:05.200Z"
                }
              }
            }
          },
          "422": {
            "description": "The body failed schema validation (a missing or malformed field, or a batch outside 1 to 200 events).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Validation failed: batch.0.event_id must be a uuid.",
                  "code": "VALIDATION_ERROR",
                  "timestamp": "2026-06-21T18:04:05.200Z"
                }
              }
            }
          },
          "429": {
            "description": "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.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Rate limit exceeded.",
                  "code": "RATE_LIMIT_EXCEEDED",
                  "timestamp": "2026-06-21T18:04:05.200Z"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The SDK backs off and retries; events stay queued client-side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Failed to accept the event batch.",
                  "code": "OPERATION_FAILED",
                  "timestamp": "2026-06-21T18:04:05.200Z"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "IngestBody": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "api_key",
          "batch"
        ],
        "properties": {
          "api_key": {
            "type": "string",
            "pattern": "^revu_pk_[A-Za-z0-9_-]{8,120}$",
            "description": "Public ingest write key (prefix revu_pk_)."
          },
          "batch": {
            "type": "array",
            "minItems": 1,
            "maxItems": 200,
            "description": "Up to 200 events per request.",
            "items": {
              "$ref": "#/components/schemas/BehaviorEvent"
            }
          }
        }
      },
      "BehaviorEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "event_id",
          "anonymous_id",
          "session_id",
          "sequence_no",
          "platform",
          "event_type",
          "screen",
          "properties",
          "device_time"
        ],
        "properties": {
          "event_id": {
            "type": "string",
            "format": "uuid",
            "description": "Client-generated UUID. Unique per organization; the idempotency / dedupe key for retried batches."
          },
          "anonymous_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "First-party anonymous visitor (device) id assigned by the SDK."
          },
          "user_id": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 128,
            "description": "Identified user id, if any. Null when the visitor is not yet identified."
          },
          "session_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "Per-session id."
          },
          "sequence_no": {
            "type": "integer",
            "minimum": 0,
            "description": "Per-page-load monotonic counter starting at 0. Gaps indicate dropped events within a page load."
          },
          "platform": {
            "type": "string",
            "enum": [
              "web",
              "ios",
              "android"
            ],
            "description": "Capture platform."
          },
          "event_type": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "Event type: '$pageview', '$autocapture', or a custom name."
          },
          "screen": {
            "type": "string",
            "maxLength": 2048,
            "description": "Route / path at capture time."
          },
          "fingerprint": {
            "type": [
              "object",
              "null"
            ],
            "description": "Element descriptor, present for $autocapture interactions. Used server-side to name the interacted element.",
            "additionalProperties": true,
            "required": [
              "tag",
              "selector"
            ],
            "properties": {
              "tag": {
                "type": "string",
                "description": "Element tag, e.g. 'button'."
              },
              "text": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Visible text (truncated; masked if sensitive)."
              },
              "role": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "ARIA role / type."
              },
              "id": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Element id, if present."
              },
              "classes": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "string"
                },
                "description": "Class list."
              },
              "selector": {
                "type": "string",
                "description": "Best-effort CSS selector (fragile; a tiebreaker)."
              },
              "ordinal": {
                "type": "number",
                "description": "Position among siblings."
              }
            }
          },
          "properties": {
            "type": "object",
            "additionalProperties": true,
            "description": "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": {
            "type": "object",
            "additionalProperties": true,
            "description": "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": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601 capture timestamp from the client device."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code",
          "timestamp"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable code: UNAUTHORIZED, FORBIDDEN, VALIDATION_ERROR, RATE_LIMIT_EXCEEDED, or OPERATION_FAILED."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601 time the error was produced."
          }
        }
      }
    }
  }
}