{"openapi":"3.1.0","info":{"title":"reopt data API","version":"1.0.0","summary":"Product analytics ingestion, export, realtime streaming, and documentation search.","description":"reopt data is a product analytics platform: you send product events to it, and it turns them\ninto funnels, retention curves, paths, segments, dashboards, and condition-based alerts.\n\nThis document covers the endpoints that are callable from outside the dashboard.\n\n- `POST /api/track` is the ingestion entrypoint. It is the only endpoint the official SDK\n  (`@reopt-ai/data-sdk`) calls, and the only one that accepts API-key style credentials.\n- `GET /api/export/events` and `GET /api/realtime/{projectId}` read project data back out and\n  require an authenticated dashboard session, because they expose collected data.\n- `GET /api/health` and `GET /api/docs/search` are unauthenticated.\n\nEvery endpoint returns `application/json` on error — HTML error pages are never served from\n`/api/*`. Ingestion errors additionally repeat the status code inside the body.","contact":{"name":"reopt data support","email":"support@reopt.ai","url":"https://data.reopt.ai/contact"},"termsOfService":"https://data.reopt.ai/terms"},"servers":[{"url":"https://data.reopt.ai","description":"Production"}],"externalDocs":{"description":"Product documentation","url":"https://data.reopt.ai/docs"},"tags":[{"name":"Ingestion","description":"Write product events into a project."},{"name":"Analytics","description":"Computed analytics — time series, funnels, retention — read with a server credential."},{"name":"Export","description":"Read collected events back out in bulk."},{"name":"Realtime","description":"Server-sent stream of live events and rolling statistics."},{"name":"Documentation","description":"Search the public product documentation."},{"name":"Operations","description":"Service health for uptime monitors."}],"paths":{"/api/track":{"post":{"tags":["Ingestion"],"operationId":"trackEvents","summary":"Ingest one or more product events","description":"Accepts a single event envelope or an array of envelopes and writes them to the project\nbound to the presented credential. The request is acknowledged as soon as the raw events\nare durably stored; enrichment (session, device, profile, analytics tables) and realtime\nfan-out happen asynchronously.\n\nDeduplication is by `eventId`: re-sending an envelope that was already accepted increases\n`duplicates` instead of `accepted`, which makes client-side retries safe.\n\nThe credential decides the ingest mode, and the two behave differently:\n\n- `reopt-write-key` selects **browser** mode: repeated identical event names from one device\n  within 5 seconds collapse into one (double-click protection), sessions are tracked per\n  device, and one malformed envelope fails the whole batch.\n- `reopt-client-id` + `reopt-client-secret` selects **server** mode: no click dedup, no\n  sessions, device identity taken from each event's `profileId`, and a malformed envelope is\n  reported in `rejected[]` while the rest of the batch proceeds. A server batch legitimately\n  repeats one event name many times per second, and a forwarder replaying a log must not stall\n  forever on a single bad row.\n\nEvery 2xx response satisfies `accepted + duplicates + rejected.length === envelopes sent`.\n\nThe request body must not exceed 512,000 bytes. `type: \"alias\"` is not supported.","security":[{"writeKey":[]},{"clientId":[],"clientSecret":[]}],"x-access-level":"project:write","parameters":[{"name":"reopt-device-id","in":"header","required":false,"description":"Stable device identifier. Generated server-side and returned to the caller when omitted.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/EventEnvelope"},{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/EventEnvelope"},"description":"Batch form. The official SDK always sends this form."}]},"examples":{"singleTrack":{"summary":"One product event","value":{"type":"track","eventId":"0f4c2c8a-6d1a-4a3c-9d3f-7f4a1e5c2b90","timestamp":1735689600000,"payload":{"name":"checkout_completed","profileId":"user_1024","properties":{"plan":"growth","amount":49000,"currency":"KRW"}}}},"batch":{"summary":"Batch of an identify and a track event","value":[{"type":"identify","eventId":"5b0c1f18-2a44-4d61-8a2e-1f9f6c3d7c11","timestamp":1735689600000,"payload":{"profileId":"user_1024","email":"user@example.com","firstName":"Jiwoo"}},{"type":"track","eventId":"9c1d0f22-7d3b-4f10-8f6a-3c2b5e9a0d44","timestamp":1735689601000,"payload":{"name":"checkout_completed","profileId":"user_1024"}}]}}}}},"responses":{"200":{"description":"Events were accepted and stored.","headers":{"x-request-id":{"description":"Correlation id for this request.","schema":{"type":"string"}},"x-bot-detected":{"description":"Present and `true` when the user agent matched a known bot pattern.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackAccepted"}}}},"202":{"description":"Events were stored but the enrichment task could not be queued. They will be replayed by the `replay-unmaterialized` job; no client retry is needed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackDeferred"}}}},"400":{"description":"Malformed request: payload over 512KB, unparseable JSON, empty batch, unsupported `alias` type, or schema validation failure.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorResponse"},{"$ref":"#/components/schemas/ValidationErrorResponse"}]}}}},"401":{"description":"Missing or invalid client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Either the per-project rate limit (1000 requests / 60s) or the organization's monthly data-point quota was exceeded. `error` distinguishes the two.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error. Safe to retry with the same `eventId`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/query/events/timeseries":{"post":{"tags":["Analytics"],"operationId":"queryEventsTimeseries","summary":"Bucketed event counts over a window","description":"Counts events per time bucket, optionally for one event name and optionally broken down by\na property key. Reads are POSTs because the inputs nest; consumers cache on their own side,\nso HTTP cache semantics would buy nothing.\n\nRequires a server credential carrying the `query` scope, scoped to the project being asked\nabout. Write keys are refused outright — they ship inside browser bundles.","security":[{"clientId":[],"clientSecret":[]}],"x-access-level":"project:read","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsTimeseriesRequest"},"examples":{"daily":{"summary":"Daily counts for one event","value":{"projectId":"prj_1","startDate":"2026-08-01","endDate":"2026-08-23","granularity":"day","eventName":"checkout_completed"}},"brokenDown":{"summary":"Daily counts split by a property value","value":{"projectId":"prj_1","startDate":"2026-08-01","endDate":"2026-08-23","granularity":"day","eventName":"checkout_completed","breakdown":{"kind":"property","key":"plan","topN":5}}}}}}},"responses":{"200":{"description":"The series, plus staleness metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsTimeseriesResponse"}}}},"400":{"description":"Schema validation failed, or `breakdown` was combined with `segmentId`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"401":{"description":"Missing or invalid client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"403":{"description":"The credential lacks the `query` scope, or is scoped to a different project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"429":{"description":"Per-credential query rate limit (60 requests / 60s).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"504":{"description":"The analytics backend exceeded its statement timeout. Narrow the window and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}}}}},"/api/v1/query/traffic/overview":{"post":{"tags":["Analytics"],"operationId":"queryTrafficOverview","summary":"Sessions, pageviews and new-vs-returning visitors over a window","description":"Session-level traffic for a brand site, per bucket and in total, served from the hourly\nrollups where the window allows it. `newVisitors` counts sessions whose device was first seen\ninside them; `returningVisitors` is the rest.\n\nRequires a server credential carrying the `query` scope, scoped to the project being asked about.","security":[{"clientId":[],"clientSecret":[]}],"x-access-level":"project:read","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrafficOverviewRequest"},"examples":{"default":{"summary":"Last 30 days","value":{"projectId":"prj_1","startDate":"2026-07-26","endDate":"2026-08-25","granularity":"day","timezone":"Asia/Seoul"}}}}}},"responses":{"200":{"description":"The rows, plus staleness metadata. `meta.source` names the path the numbers took.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrafficOverviewResponse"}}}},"400":{"description":"Schema validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"401":{"description":"Missing or invalid client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"403":{"description":"The credential lacks the `query` scope, or is scoped to a different project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"429":{"description":"Per-credential query rate limit (60 requests / 60s).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"504":{"description":"The analytics backend exceeded its statement timeout. Narrow the window and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}}}}},"/api/v1/query/traffic/sources":{"post":{"tags":["Analytics"],"operationId":"queryTrafficSources","summary":"Top acquisition sources by sessions","description":"The top channels, referrers or UTM combinations for a window, ordered by sessions. One\ndimension per call; the row carries the dimension's own columns plus the traffic totals.\n\nRequires a server credential carrying the `query` scope, scoped to the project being asked about.","security":[{"clientId":[],"clientSecret":[]}],"x-access-level":"project:read","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrafficSourcesRequest"},"examples":{"default":{"summary":"Last 30 days","value":{"projectId":"prj_1","startDate":"2026-07-26","endDate":"2026-08-25","dimension":"referrer","limit":10,"timezone":"Asia/Seoul"}}}}}},"responses":{"200":{"description":"The rows, plus staleness metadata. `meta.source` names the path the numbers took.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrafficSourcesResponse"}}}},"400":{"description":"Schema validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"401":{"description":"Missing or invalid client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"403":{"description":"The credential lacks the `query` scope, or is scoped to a different project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"429":{"description":"Per-credential query rate limit (60 requests / 60s).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"504":{"description":"The analytics backend exceeded its statement timeout. Narrow the window and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}}}}},"/api/v1/query/funnel":{"post":{"tags":["Analytics"],"operationId":"queryFunnel","summary":"Step-by-step conversion through an ordered sequence of events","description":"Counts how many distinct subjects reached each step of an ordered event sequence within\n`windowSeconds` of entering the funnel.\n\nSubjects are counted by device identity. For a project fed by server credentials that\nidentity is the event's `profileId`, so these are customer counts rather than browser counts.","security":[{"clientId":[],"clientSecret":[]}],"x-access-level":"project:read","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FunnelRequest"},"examples":{"signup":{"summary":"Two-step signup funnel over three weeks","value":{"projectId":"prj_1","steps":["signup_started","signup_completed"],"startDate":"2026-08-01","endDate":"2026-08-23","windowSeconds":86400}}}}}},"responses":{"200":{"description":"The funnel, plus staleness metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FunnelResponse"}}}},"400":{"description":"Schema validation failed (fewer than 2 or more than 10 steps, bad window).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"401":{"description":"Missing or invalid client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"403":{"description":"The credential lacks the `query` scope, or is scoped to a different project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"429":{"description":"Per-credential query rate limit (60 requests / 60s).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"504":{"description":"The analytics backend exceeded its statement timeout.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}}}}},"/api/v1/query/retention":{"post":{"tags":["Analytics"],"operationId":"queryRetention","summary":"Cohort retention matrix","description":"Groups subjects into cohorts by when they first appeared, then reports how many came back in\neach subsequent period.\n\nUnder the default `cohortMode: \"first-ever\"` the cohort is read from the first-seen\naggregate, and `startEvent` / `segmentId` / `channelFilter` constrain only the returning\nside. Use `first-in-window` to apply those filters to cohort selection too, at the cost of a\nfull event scan.","security":[{"clientId":[],"clientSecret":[]}],"x-access-level":"project:read","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionRequest"},"examples":{"weekly":{"summary":"Eight weekly periods","value":{"projectId":"prj_1","startDate":"2026-06-01","endDate":"2026-08-23","interval":"week","periods":8}}}}}},"responses":{"200":{"description":"The matrix, plus staleness metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetentionResponse"}}}},"400":{"description":"Schema validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"401":{"description":"Missing or invalid client credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"403":{"description":"The credential lacks the `query` scope, or is scoped to a different project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"429":{"description":"Per-credential query rate limit (60 requests / 60s).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}},"504":{"description":"The analytics backend exceeded its statement timeout.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryErrorResponse"}}}}}}},"/api/export/events":{"get":{"tags":["Export"],"operationId":"exportEvents","summary":"Export collected events as CSV or JSON Lines","description":"Streams the raw events of one project back to the caller. Requires a dashboard session\ncookie and membership of the organization that owns the project — ingestion credentials\ncannot read data back out.\n\nRate limited to 10 requests per minute per user.","security":[{"sessionCookie":[]}],"x-access-level":"organization:member","parameters":[{"name":"projectId","in":"query","required":true,"description":"Project to export from.","schema":{"type":"string"}},{"name":"startDate","in":"query","required":false,"description":"Inclusive lower bound, `YYYY-MM-DD`.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"endDate","in":"query","required":false,"description":"Inclusive upper bound, `YYYY-MM-DD`. Expanded to end-of-day UTC.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"limit","in":"query","required":false,"description":"Maximum rows. Defaults to 10,000 and is capped by the deployment row limit.","schema":{"type":"integer","minimum":1}},{"name":"format","in":"query","required":false,"description":"`jsonl` returns newline-delimited JSON. Any other value returns CSV.","schema":{"type":"string","enum":["csv","jsonl"],"default":"csv"}}],"responses":{"200":{"description":"Export stream. Columns are stable across both formats.","headers":{"Content-Disposition":{"description":"Attachment filename.","schema":{"type":"string"}}},"content":{"text/csv":{"schema":{"type":"string"}},"application/x-ndjson":{"schema":{"$ref":"#/components/schemas/ExportedEvent"}}}},"400":{"description":"Missing `projectId`, or a date that is not `YYYY-MM-DD`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"}}}},"401":{"description":"No valid session cookie.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"}}}},"404":{"description":"Project does not exist, or the caller is not a member of its organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"}}}},"413":{"description":"Requested `limit` exceeds the deployment row limit.","content":{"application/json":{"schema":{"type":"object","required":["error","maxRows"],"properties":{"error":{"type":"string"},"maxRows":{"type":"integer"}}}}}},"429":{"description":"Export rate limit exceeded (10 / 60s).","headers":{"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"}}}},"500":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"}}}}}}},"/api/realtime/{projectId}":{"get":{"tags":["Realtime"],"operationId":"streamRealtimeEvents","summary":"Subscribe to a project's live event stream","description":"Server-Sent Events stream. Emits a `ready` frame once when the subscription is armed,\nan `events` frame whenever new events land, a `stats`\nframe immediately and then every 30 seconds, and a `: heartbeat` comment on the same\ncadence. Send `Last-Event-ID` to resume from a previously received frame id.\n\nAn event whose properties exceed the inline size bound arrives with `properties: null`\nand `truncated: true`; read the full row from the events API instead.\n\nThe stream closes with a `close` frame once the deployment's maximum stream duration is\nreached (290 seconds on Vercel); clients are expected to reconnect.\n\nRate limited to 30 connections per minute per user and project.","security":[{"sessionCookie":[]}],"x-access-level":"organization:member","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}},{"name":"Last-Event-ID","in":"header","required":false,"description":"Frame id to resume from. Defaults to only new events. Sent automatically by EventSource on reconnect, which is what makes a reconnect lossless — clients should not re-run a backfill for one.","schema":{"type":"string"}},{"name":"f","in":"query","required":false,"description":"Server-side filter, as base64url-encoded JSON: `{ \"q\"?: string, \"eventNames\"?: string[], \"propertyFilters\"?: { key, operator: \"eq\"|\"neq\"|\"contains\"|\"not_contains\", value }[] }`. Matching mirrors the events query exactly — `contains` is case-insensitive and a missing property compares as the empty string. Anything malformed is ignored rather than rejected, yielding an unfiltered stream.","schema":{"type":"string"}}],"responses":{"200":{"description":"Event stream.","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/RealtimeFrame"}}}},"400":{"description":"The `f` filter could not be parsed. Rejected rather than ignored: a stream that silently ignored the filter would bill by the minute for volume the caller never asked for.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"}}}},"401":{"description":"No valid session cookie.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"}}}},"404":{"description":"Project does not exist, or the caller is not a member of its organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"}}}},"429":{"description":"Realtime connection rate limit exceeded (30 / 60s).","headers":{"Retry-After":{"schema":{"type":"integer"}}},"content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/docs/search":{"get":{"tags":["Documentation"],"operationId":"searchDocs","summary":"Full-text search across the public documentation","description":"Unauthenticated. Indexes the same Korean-tokenized content served at `/docs`. An empty or missing `query` returns an empty array rather than an error.","security":[],"x-access-level":"public","parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string"}},{"name":"tag","in":"query","required":false,"description":"Comma-separated tag filter.","schema":{"type":"string"}},{"name":"locale","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Ranked matches. Page, heading, and text nodes are returned as separate rows.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DocsSearchResult"}}}}}}}},"/api/health":{"get":{"tags":["Operations"],"operationId":"getHealth","summary":"Service health report","description":"Unauthenticated liveness and dependency check. Returns `200` when every dependency is reachable and `503` otherwise, so it can back an uptime monitor directly.","security":[],"x-access-level":"public","responses":{"200":{"description":"All dependencies healthy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthReport"}}}},"503":{"description":"One or more dependencies are failing. The body names which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthReport"}}}}}}}},"components":{"securitySchemes":{"writeKey":{"type":"apiKey","in":"header","name":"reopt-write-key","description":"Public, browser-safe project write key. Grants ingestion into exactly one project and\nnothing else — it cannot read events, list projects, or reach the dashboard API. Rejected\nif `reopt-client-id` or `reopt-client-secret` is also present.\n\nGenerate and rotate it in the dashboard under project settings."},"clientId":{"type":"apiKey","in":"header","name":"reopt-client-id","description":"Server-side client identifier. Must be sent together with `reopt-client-secret`. Usable from\ntrusted backends where the credential is not exposed.\n\nOn `POST /api/track` it selects server ingest mode. On `/api/v1/query/*` it is the only\naccepted credential — and only when the client carries the `query` scope, which is not\ngranted by default. A write key is never accepted there, since it ships inside browser\nbundles."},"clientSecret":{"type":"apiKey","in":"header","name":"reopt-client-secret","description":"Secret half of the server-side credential. Never expose it in client code."},"sessionCookie":{"type":"apiKey","in":"cookie","name":"reopt_session","description":"Dashboard session, established through Reopt OAuth. Required by every endpoint that reads\ncollected data. Authorization is resolved per request from the caller's organization\nmembership role (`owner` > `admin` > `member`); read endpoints require `member` or above."}},"schemas":{"EventEnvelope":{"oneOf":[{"$ref":"#/components/schemas/TrackEnvelope"},{"$ref":"#/components/schemas/IdentifyEnvelope"},{"$ref":"#/components/schemas/IncrementEnvelope"},{"$ref":"#/components/schemas/DecrementEnvelope"}],"discriminator":{"propertyName":"type","mapping":{"track":"#/components/schemas/TrackEnvelope","identify":"#/components/schemas/IdentifyEnvelope","increment":"#/components/schemas/IncrementEnvelope","decrement":"#/components/schemas/DecrementEnvelope"}}},"TrackEnvelope":{"type":"object","description":"A product event that happened.","required":["type","eventId","timestamp","payload"],"properties":{"type":{"type":"string","const":"track"},"eventId":{"type":"string","format":"uuid","description":"Client-generated UUID. Doubles as the idempotency key — replaying the same eventId is counted as a duplicate, not a new event."},"timestamp":{"type":"integer","minimum":0,"description":"Client-side event time as a Unix epoch value in milliseconds."},"payload":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"Event name. Reserved names emitted by the platform itself are rejected: session_start, session_end, screen_view.","example":"checkout_completed"},"properties":{"type":"object","additionalProperties":true,"description":"Arbitrary event properties. Used as breakdown dimensions in analysis."},"profileId":{"oneOf":[{"type":"string","minLength":1,"maxLength":500},{"type":"number"}],"description":"Stable identifier of the end user this event belongs to."}},"additionalProperties":false}},"additionalProperties":false},"IdentifyEnvelope":{"type":"object","description":"Attaches or updates profile attributes for a user.","required":["type","eventId","timestamp","payload"],"properties":{"type":{"type":"string","const":"identify"},"eventId":{"type":"string","format":"uuid","description":"Client-generated UUID. Doubles as the idempotency key — replaying the same eventId is counted as a duplicate, not a new event."},"timestamp":{"type":"integer","minimum":0,"description":"Client-side event time as a Unix epoch value in milliseconds."},"payload":{"type":"object","required":["profileId"],"properties":{"profileId":{"oneOf":[{"type":"string","minLength":1,"maxLength":500},{"type":"number"}],"description":"Stable identifier of the end user this event belongs to."},"firstName":{"type":"string","maxLength":200},"lastName":{"type":"string","maxLength":200},"email":{"type":"string","format":"email","maxLength":320},"avatar":{"type":"string","format":"uri","maxLength":2000},"properties":{"type":"object","additionalProperties":true}},"additionalProperties":false}},"additionalProperties":false},"IncrementEnvelope":{"type":"object","description":"Increases a numeric profile property.","required":["type","eventId","timestamp","payload"],"properties":{"type":{"type":"string","const":"increment"},"eventId":{"type":"string","format":"uuid","description":"Client-generated UUID. Doubles as the idempotency key — replaying the same eventId is counted as a duplicate, not a new event."},"timestamp":{"type":"integer","minimum":0,"description":"Client-side event time as a Unix epoch value in milliseconds."},"payload":{"type":"object","required":["profileId","property"],"properties":{"profileId":{"oneOf":[{"type":"string","minLength":1,"maxLength":500},{"type":"number"}],"description":"Stable identifier of the end user this event belongs to."},"property":{"type":"string","minLength":1,"maxLength":200},"value":{"type":"number","exclusiveMinimum":0,"description":"Defaults to 1 when omitted."}},"additionalProperties":false}},"additionalProperties":false},"DecrementEnvelope":{"type":"object","description":"Decreases a numeric profile property.","required":["type","eventId","timestamp","payload"],"properties":{"type":{"type":"string","const":"decrement"},"eventId":{"type":"string","format":"uuid","description":"Client-generated UUID. Doubles as the idempotency key — replaying the same eventId is counted as a duplicate, not a new event."},"timestamp":{"type":"integer","minimum":0,"description":"Client-side event time as a Unix epoch value in milliseconds."},"payload":{"type":"object","required":["profileId","property"],"properties":{"profileId":{"oneOf":[{"type":"string","minLength":1,"maxLength":500},{"type":"number"}],"description":"Stable identifier of the end user this event belongs to."},"property":{"type":"string","minLength":1,"maxLength":200},"value":{"type":"number","exclusiveMinimum":0,"description":"Defaults to 1 when omitted."}},"additionalProperties":false}},"additionalProperties":false},"TrackAccepted":{"type":"object","required":["status","requestId","mode","accepted","duplicates","rejected","queued"],"description":"Every 2xx satisfies `accepted + duplicates + rejected.length === envelopes sent`. A caller that checks this identity cannot lose events silently.","properties":{"status":{"type":"string","const":"ok"},"requestId":{"type":"string","description":"Matches the `x-request-id` response header."},"mode":{"type":"string","enum":["browser","server"],"description":"Which ingest mode the presented credential selected."},"accepted":{"type":"integer","description":"Envelopes stored as new events."},"duplicates":{"type":"integer","description":"Envelopes skipped because their `eventId` was already seen. In `browser` mode this also counts the 5-second click dedup."},"rejected":{"type":"array","description":"Envelopes permanently refused. Always empty in `browser` mode, where a bad envelope fails the whole batch instead. A rejection is permanent — resending the same envelope produces the same rejection — so count it as skipped and advance.","items":{"$ref":"#/components/schemas/TrackRejection"}},"queued":{"type":"boolean","description":"Whether asynchronous enrichment was scheduled."}}},"TrackRejection":{"type":"object","required":["eventId","index","reason"],"properties":{"eventId":{"type":"string","description":"May be empty or malformed when the envelope failed validation on that very field."},"index":{"type":"integer","description":"Position in the submitted batch — the only reliable handle when `eventId` itself is bad."},"reason":{"type":"string","enum":["validation_failed","reserved_name","duplicate_in_batch"]},"message":{"type":"string"}}},"TrackDeferred":{"allOf":[{"$ref":"#/components/schemas/TrackAccepted"},{"type":"object","properties":{"status":{"type":"string","const":"accepted"},"backgroundQueued":{"type":"boolean","const":false},"replay":{"type":"string","const":"unmaterialized-raw-events"}}}]},"QueryMeta":{"type":"object","required":["projectId","computedAt","cacheAgeSeconds","ingestLagSeconds","ingestLagStatus","ingestDeadLetterCount","cached","timezone","source","requestId"],"description":"Staleness is reported as two independent numbers rather than one, because they have different remedies: a stale cache resolves itself at the next TTL, while ingest lag means the read model is genuinely behind.","properties":{"projectId":{"type":"string"},"computedAt":{"type":"string","format":"date-time","description":"When the underlying computation ran. On a cache hit, when the cached value was computed."},"cacheAgeSeconds":{"type":"integer","description":"Age of the returned computation. `0` on a cache miss."},"ingestLagSeconds":{"type":["integer","null"],"description":"How far behind live materialization was when the computation ran. `0` means no live event is pending; `null` means freshness could not be measured."},"ingestLagStatus":{"type":"string","enum":["known","unknown"],"description":"Whether ingest freshness was measured successfully."},"ingestDeadLetterCount":{"type":["integer","null"],"description":"Pending events older than the live replay window. Separate from live lag so old failures remain visible without pegging the aggregating indicator."},"cached":{"type":"boolean"},"timezone":{"type":"string","description":"The zone actually used to place bucket and window boundaries — the resolved value, not the requested one. `UTC` when nothing named a zone.","example":"Asia/Seoul"},"source":{"type":"string","enum":["rollup","raw","mixed"],"description":"Where the numbers came from: pre-aggregated hourly rollups, a raw scan, or finished hours from the rollup plus the in-progress hour from raw. Diagnostic — the values agree either way."},"requestId":{"type":"string"}}},"TimeseriesPoint":{"type":"object","required":["date","count"],"properties":{"date":{"type":"string","description":"Bucket start, formatted for the requested granularity."},"count":{"type":"integer"}}},"EventsTimeseriesRequest":{"type":"object","required":["projectId","startDate","endDate"],"properties":{"projectId":{"type":"string","description":"Must equal the project the presented credential is scoped to."},"startDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-08-01"},"endDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-08-23"},"timezone":{"type":"string","description":"IANA zone deciding where a calendar day starts, e.g. `Asia/Seoul`. Omit to use the project's configured zone, then UTC. The zone actually applied comes back in `meta.timezone` — read it rather than assuming the request was honoured.","example":"Asia/Seoul"},"granularity":{"type":"string","enum":["hour","day","week","month"],"default":"day"},"eventName":{"type":"string","description":"Omit to count every event in the window."},"breakdown":{"type":"object","required":["kind","key"],"description":"v1 breaks down by `properties` JSON keys only. Built-in columns have a totals breakdown but no time-series variant, and this plane adds no new computations.","properties":{"kind":{"type":"string","const":"property"},"key":{"type":"string"},"topN":{"type":"integer","minimum":1,"maximum":10,"default":5}}},"segmentId":{"type":"string","description":"Cannot be combined with `breakdown`; sending both is refused as `unsupported_combination`."},"includeInternal":{"type":"boolean","default":false,"description":"Count events whose catalogue status is `internal` (operational probes). Left out by default."}}},"TrafficTotals":{"type":"object","required":["sessions","pageviews","newVisitors","returningVisitors","bounceRate"],"properties":{"sessions":{"type":"integer"},"pageviews":{"type":"integer"},"newVisitors":{"type":"integer","description":"Sessions whose device was first seen inside them."},"returningVisitors":{"type":"integer","description":"`sessions - newVisitors`."},"bounceRate":{"type":"number","description":"Percent of sessions that bounced, two decimals."}}},"TrafficOverviewRequest":{"type":"object","required":["projectId","startDate","endDate"],"properties":{"projectId":{"type":"string","description":"Must equal the project the presented credential is scoped to."},"startDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-08-01"},"endDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-08-23"},"timezone":{"type":"string","description":"IANA zone deciding where a calendar day starts, e.g. `Asia/Seoul`. Omit to use the project's configured zone, then UTC. The zone actually applied comes back in `meta.timezone` — read it rather than assuming the request was honoured.","example":"Asia/Seoul"},"granularity":{"type":"string","enum":["hour","day","week","month"],"default":"day"}}},"TrafficOverviewResponse":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","required":["granularity","timezone","totals","series"],"properties":{"granularity":{"type":"string","enum":["hour","day","week","month"]},"timezone":{"type":"string"},"totals":{"$ref":"#/components/schemas/TrafficTotals"},"series":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/TrafficTotals"},{"type":"object","required":["date"],"properties":{"date":{"type":"string","description":"Bucket start, `YYYY-MM-DD HH:MM:SS` in `meta.timezone`."}}}]}}}},"meta":{"$ref":"#/components/schemas/QueryMeta"}}},"TrafficSourcesRequest":{"type":"object","required":["projectId","startDate","endDate","dimension"],"properties":{"projectId":{"type":"string","description":"Must equal the project the presented credential is scoped to."},"startDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-08-01"},"endDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-08-23"},"timezone":{"type":"string","description":"IANA zone deciding where a calendar day starts, e.g. `Asia/Seoul`. Omit to use the project's configured zone, then UTC. The zone actually applied comes back in `meta.timezone` — read it rather than assuming the request was honoured.","example":"Asia/Seoul"},"dimension":{"type":"string","enum":["channel","referrer","utm"],"description":"`channel`: channel type · `referrer`: referrer name and type · `utm`: source / medium / campaign."},"limit":{"type":"integer","minimum":1,"maximum":100,"default":10}}},"TrafficSourcesResponse":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","required":["dimension","timezone","rows"],"properties":{"dimension":{"type":"string","enum":["channel","referrer","utm"]},"timezone":{"type":"string"},"rows":{"type":"array","description":"Ordered by sessions, descending.","items":{"allOf":[{"$ref":"#/components/schemas/TrafficTotals"},{"type":"object","required":["key"],"properties":{"key":{"type":"string","description":"Display key for the row."},"channelType":{"type":"string"},"referrerName":{"type":"string"},"referrerType":{"type":"string"},"utmSource":{"type":"string"},"utmMedium":{"type":"string"},"utmCampaign":{"type":"string"}}}]}}}},"meta":{"$ref":"#/components/schemas/QueryMeta"}}},"EventsTimeseriesResponse":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","required":["granularity","series","timezone"],"properties":{"granularity":{"type":"string","enum":["hour","day","week","month"]},"timezone":{"type":"string","description":"Zone the buckets were cut in. Mirrors `meta.timezone`."},"series":{"type":"array","items":{"$ref":"#/components/schemas/TimeseriesPoint"},"description":"Total per bucket. Always present, with or without a breakdown."},"breakdown":{"type":"array","description":"Present only when `breakdown` was requested.","items":{"type":"object","required":["value","points"],"properties":{"value":{"type":"string"},"points":{"type":"array","items":{"$ref":"#/components/schemas/TimeseriesPoint"}}}}}}},"meta":{"$ref":"#/components/schemas/QueryMeta"}}},"FunnelRequest":{"type":"object","required":["projectId","steps","startDate","endDate"],"properties":{"projectId":{"type":"string","description":"Must equal the project the presented credential is scoped to."},"startDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-08-01"},"endDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-08-23"},"timezone":{"type":"string","description":"IANA zone deciding where a calendar day starts, e.g. `Asia/Seoul`. Omit to use the project's configured zone, then UTC. The zone actually applied comes back in `meta.timezone` — read it rather than assuming the request was honoured.","example":"Asia/Seoul"},"steps":{"type":"array","minItems":2,"maxItems":10,"items":{"type":"string"},"description":"Ordered event names. A subject counts at step k if it reached at least step k."},"windowSeconds":{"type":"integer","minimum":60,"maximum":2592000,"default":86400,"description":"Maximum elapsed time from first to last step for a conversion to count."},"segmentId":{"type":"string"},"channelFilter":{"$ref":"#/components/schemas/ChannelFilter"}}},"FunnelResponse":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","required":["steps","totalUsers","overallConversionRate"],"properties":{"steps":{"type":"array","items":{"type":"object","required":["step","name","users","conversionRate","dropoffRate","dropoffUsers"],"properties":{"step":{"type":"integer","minimum":1},"name":{"type":"string"},"users":{"type":"integer"},"conversionRate":{"type":"number","description":"Percent of the previous step that reached this one. `100` for step 1."},"dropoffRate":{"type":"number","description":"Percent of the previous step that did not."},"dropoffUsers":{"type":"integer"}}}},"totalUsers":{"type":"integer"},"overallConversionRate":{"type":"number","description":"Percent of step-1 subjects that reached the last step."}}},"meta":{"$ref":"#/components/schemas/QueryMeta"}}},"RetentionRequest":{"type":"object","required":["projectId","startDate","endDate"],"properties":{"projectId":{"type":"string","description":"Must equal the project the presented credential is scoped to."},"startDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-08-01"},"endDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-08-23"},"timezone":{"type":"string","description":"IANA zone deciding where a calendar day starts, e.g. `Asia/Seoul`. Omit to use the project's configured zone, then UTC. The zone actually applied comes back in `meta.timezone` — read it rather than assuming the request was honoured.","example":"Asia/Seoul"},"interval":{"type":"string","enum":["day","week","month"],"default":"day"},"periods":{"type":"integer","minimum":1,"maximum":30,"default":7},"startEvent":{"type":"string"},"returnEvent":{"type":"string"},"retentionType":{"type":"string","enum":["bounded","unbounded"],"default":"bounded","description":"`bounded`: returned in exactly period N. `unbounded`: returned in period N or later."},"cohortMode":{"type":"string","enum":["first-ever","first-in-window"],"default":"first-ever","description":"A non-UTC `timezone` makes `first-ever` read raw events instead of its daily rollup — the rollup is cut on UTC days and cannot be re-cut afterwards — so expect a slower query in exchange for cohorts that match the reader's calendar."},"segmentId":{"type":"string"},"channelFilter":{"$ref":"#/components/schemas/ChannelFilter"}}},"RetentionResponse":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","required":["matrix","averageRetention","interval","periods","retentionType","timezone"],"properties":{"matrix":{"type":"array","items":{"type":"object","required":["cohortDate","cohortSize","retention"],"properties":{"cohortDate":{"type":"string"},"cohortSize":{"type":"integer"},"retention":{"type":"array","description":"Length is `periods + 1`, index-aligned to `period`.","items":{"type":"object","required":["period","users","rate"],"properties":{"period":{"type":"integer","description":"`0` is the cohort's own period."},"users":{"type":"integer"},"rate":{"type":"number","description":"Percent of `cohortSize`."}}}}}}},"averageRetention":{"type":"array","description":"Unweighted mean rate per period across cohorts.","items":{"type":"object","required":["period","rate"],"properties":{"period":{"type":"integer"},"rate":{"type":"number"}}}},"interval":{"type":"string","enum":["day","week","month"]},"periods":{"type":"integer"},"retentionType":{"type":"string","enum":["bounded","unbounded"]},"timezone":{"type":"string","description":"Zone the cohort days were cut in. Mirrors `meta.timezone`."}}},"meta":{"$ref":"#/components/schemas/QueryMeta"}}},"ErrorResponse":{"type":"object","description":"Structured error body returned by every ingestion endpoint.","required":["status","code","error"],"properties":{"status":{"type":"integer","description":"HTTP status code, repeated in the body.","example":401},"code":{"type":"string","description":"Stable machine-readable failure code. Branch on this, never on `error` or `message`, which are prose and may be reworded. Note that the two 429 codes mean different things: `rate_limited` clears on its own, `quota_exceeded` needs the monthly limit raised.","enum":["unauthorized","invalid_json","payload_too_large","empty_batch","alias_unsupported","project_without_organization","validation_failed","rate_limited","quota_exceeded","internal_error"],"example":"unauthorized"},"error":{"type":"string","description":"Short human-readable label.","example":"Unauthorized"},"message":{"type":"string","description":"Human-readable explanation. Absent on 500 responses."},"requestId":{"type":"string","description":"Matches the `x-request-id` response header."}}},"QueryErrorResponse":{"type":"object","description":"Structured error body returned by every query endpoint.","required":["status","code","error"],"properties":{"status":{"type":"integer","example":403},"code":{"type":"string","enum":["unauthorized","project_scope_mismatch","query_scope_missing","validation_failed","unsupported_combination","rate_limited","query_timeout","internal_error"],"example":"project_scope_mismatch"},"error":{"type":"string","example":"Forbidden"},"message":{"type":"string"},"errors":{"type":"array","items":{},"description":"Validation issues, when `code` is `validation_failed`."},"requestId":{"type":"string"}}},"ChannelFilter":{"type":"object","description":"Acquisition-channel filter. Each field is a set: an event matches when its value is in the set, and omitted fields do not constrain.","properties":{"referrerTypes":{"type":"array","items":{"type":"string"}},"channelTypes":{"type":"array","items":{"type":"string"}},"utmSources":{"type":"array","items":{"type":"string"}},"utmMediums":{"type":"array","items":{"type":"string"}},"utmCampaigns":{"type":"array","items":{"type":"string"}}}},"ValidationErrorResponse":{"allOf":[{"$ref":"#/components/schemas/ErrorResponse"},{"type":"object","properties":{"errors":{"type":"array","description":"Zod issues, one per rejected field.","items":{"type":"object","properties":{"path":{"type":"array","items":{"oneOf":[{"type":"string"},{"type":"integer"}]}},"message":{"type":"string"},"code":{"type":"string"}}}}}}]},"SimpleError":{"type":"object","description":"Structured error body returned by the export and realtime endpoints.","required":["error"],"properties":{"error":{"type":"string","example":"Unauthorized"}}},"ExportedEvent":{"type":"object","description":"One exported event row. CSV uses these field names as its header.","properties":{"id":{"type":"string"},"name":{"type":"string"},"device_id":{"type":"string"},"session_id":{"type":"string"},"profile_id":{"type":"string"},"properties":{"type":"string","description":"JSON-encoded property bag."},"path":{"type":"string"},"country":{"type":"string"},"city":{"type":"string"},"os":{"type":"string"},"browser":{"type":"string"},"device":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"RealtimeEvent":{"type":"object","description":"One live event. Carries the same fields the event list renders so a live row and a stored row are interchangeable without a second request.","required":["id","name","path","country","city","os","browser","device","device_id","session_id","profile_id","properties","created_at"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"country":{"type":"string"},"city":{"type":"string"},"os":{"type":"string"},"browser":{"type":"string"},"device":{"type":"string"},"device_id":{"type":"string"},"session_id":{"type":"string"},"profile_id":{"type":"string"},"properties":{"type":["string","null"],"description":"JSON-encoded property bag, or null when it exceeded the inline size bound."},"truncated":{"type":"boolean","description":"Present and true when `properties` was dropped for size. Fetch the full row separately."},"created_at":{"type":"string","description":"UTC timestamp without a zone designator ('YYYY-MM-DD HH:MM:SS.mmm')."}}},"RealtimeFrame":{"oneOf":[{"type":"object","required":["type","cursor"],"description":"Sent once per connection, before any events. `cursor` is the stream entry the subscription resumes after; every event published later is guaranteed to arrive on this connection. Its leading component is an epoch-millisecond timestamp, which is what lets a client that just read historical events know exactly which window it still has to backfill.","properties":{"type":{"type":"string","const":"ready"},"cursor":{"type":"string","description":"Redis stream id (\"<ms>-<seq>\"), or \"0-0\" for an empty stream."}}},{"type":"object","required":["type","events"],"properties":{"type":{"type":"string","const":"events"},"events":{"type":"array","items":{"$ref":"#/components/schemas/RealtimeEvent"}}}},{"type":"object","required":["type"],"properties":{"type":{"type":"string","const":"stats"},"eventsLastMinute":{"type":"integer"},"eventsLastHour":{"type":"integer"},"eventsLast24h":{"type":"integer"},"activeUsers":{"type":"integer"},"topEvents":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"count":{"type":"integer"}}}}}},{"type":"object","required":["type","message"],"properties":{"type":{"type":"string","const":"error"},"message":{"type":"string"}}},{"type":"object","required":["type","reason"],"properties":{"type":{"type":"string","const":"close"},"reason":{"type":"string","const":"max_duration"}}}]},"DocsSearchResult":{"type":"object","required":["id","url","type","content"],"properties":{"id":{"type":"string"},"url":{"type":"string","description":"Documentation path, relative to the site root."},"type":{"type":"string","enum":["page","heading","text"]},"content":{"type":"string"},"breadcrumbs":{"type":"array","items":{"type":"string"}}}},"HealthReport":{"type":"object","required":["status","timestamp","uptime","checks"],"properties":{"status":{"type":"string","enum":["healthy","degraded","unhealthy"]},"timestamp":{"type":"string","format":"date-time"},"uptime":{"type":"integer","description":"Process uptime in seconds."},"checks":{"type":"object","properties":{"postgresql":{"$ref":"#/components/schemas/HealthCheck"},"redis":{"$ref":"#/components/schemas/HealthCheck"},"analytics":{"allOf":[{"$ref":"#/components/schemas/HealthCheck"},{"type":"object","properties":{"backend":{"type":"string","enum":["clickhouse","motherduck"]},"error":{"type":"string"}}}]},"ingest":{"allOf":[{"$ref":"#/components/schemas/HealthCheck"},{"type":"object","properties":{"provider":{"type":"string","enum":["redis","vercel"]},"queue":{"type":"object","properties":{"queued":{"type":"integer"},"processing":{"type":"integer"},"deadLettered":{"type":"integer"}}},"worker":{"type":"object","properties":{"status":{"type":"string","enum":["online","offline","managed"]},"updatedAt":{"type":"string","format":"date-time"},"workerId":{"type":"string"}}}}}]}}}}},"HealthCheck":{"type":"object","required":["status","latencyMs"],"properties":{"status":{"type":"string","enum":["ok","error"]},"latencyMs":{"type":"integer"}}}}}}