Skip to content

Ingest API reference ​

Everything a client sends us. Generated from the collector's own specification — see the HTTP ingest guide for the prose version.

The collector is the only Tangent service a customer's application talks to directly. It accepts analytics events, session-replay chunks, and serves the experiment definitions the SDKs assign from. Everything else — querying, dashboards, administration — lives behind a different API.

This document is generated from, and kept honest against, the collector's own controllers, EventValidator and ApiKeyGateFilter. Where it differs from prose documentation, this document describes what the running service actually does.

Base URL ​

Self-hosted, the service listens on port 8082 with the context path /collector, so the ingest endpoint is http://<host>:8082/collector/v1/events. On the hosted service you are given a collector endpoint for your workspace; use the server variables below to point tooling at it.

Authentication in one paragraph ​

Every route in this document except the specification itself requires an API key in the X-API-Key header. Ingest fails closed: a missing key, an unrecognised key, a revoked key, a key belonging to a suspended workspace, a secret key presented from a browser, and a public key used from an origin the workspace did not allow, all produce 401. There is no path through this service on which an event is stored without a key that the control plane positively confirmed. If the control plane cannot be reached the answer is 503, never acceptance — an unverified key is not a valid key.

Delivery semantics ​

A 202 means accepted and queued, not stored. Storage is idempotent on messageId, so resending a batch whose response you lost is safe and correct. Generating a fresh messageId per retry attempt is the one thing that turns a lost response into a double count.

Server: {scheme}://{host}{basePath} — Your workspace's collector endpoint.

Operations ​

EndpointWhat it does
POST/v1/eventsSubmit a batch of events
GET/v1/key/validateCheck a key before queueing anything
GET/config/experimentsFetch active experiment definitions
POST/v1/replaySubmit a session-replay chunk
GET/openapi.yamlThis document, as YAML
GET/openapi.jsonThis document, as JSON

Ingest ​

Submitting events. The endpoint every integration starts with.

POST /v1/events ​

Submit a batch of events

Submits one to 1000 events. The batch is validated event by event: one malformed event does not discard the good ones. Losing 999 valid events over one typo is the worst outcome a collector can produce, so it does not happen here.

Outcomes:

  • every event valid → 202 with status: accepted
  • some valid, some not → 202 with status: partially_accepted and a rejected array naming each failure by its index in the array you sent
  • none valid → 400, also carrying the per-event rejected array

Tenant, app and environment are derived from the key. There is no tenantId field; one in the body is ignored.

Rate limiting charges accepted events only, and charges the whole batch before anything is published, so a 429 never leaves part of a batch already in the pipeline.

ParameterInTypeRequiredDescription
apiKeyquerystringnoFallback for navigator.sendBeacon, which cannot set headers. The X-API-Key header is preferred and takes precedence when both are sent.
X-Selica-Sdkheaderstringnolanguage/version, e.g. node/1.2.0. Stored on the event as context.sdk. Used to tell a customer which SDK build is producing bad data.
Content-Encodingheadergzipdeflateno
X-Forwarded-ForheaderstringnoThe first address is taken as the client IP when the event does not carry ipAddress. Falls back to the socket address.
OriginheaderstringnoSet automatically by browsers; never set it by hand. Its presence changes the key policy: a SECRET key with an Origin is refused, and a PUBLIC key is checked against its allow-list. Pre-flight OPTIONS carries no credentials by design and is answered by CORS before the key gate runs.
X-Request-IdheaderstringnoEchoed back in the requestId field of error bodies, so a failure in your logs and a failure in ours can be matched. Note that error bodies produced by the key gate filter (401, 429 and 503 raised before the request reaches application code) do not carry this field at all.

Request body (required)

json
{
  "events": [
    {
      "messageId": "8f14e45f-ceea-467a-9d31-1c9ebc1ad4a1",
      "eventType": "report_exported",
      "deviceId": "3b6a1f42-9c77-4c1e-8a02-6d0f2b5e77aa",
      "properties": {
        "format": "csv",
        "rows": 1240
      }
    }
  ]
}
ResponseMeaning
202Accepted and queued. status distinguishes a wholly valid batch (accepted) from one where some events were refused (partially_accepted). Both are 202 — check status and rejectedCount, not the HTTP code alone.
400Nothing was stored. Two distinct bodies share this code: * every event was invalid — the body carries rejected, exactly as a partial acceptance would, so you can report the same way in both cases * the request itself was wrong — empty events, unparseable JSON, or more than 1000 events. No rejected array. Every 400 here is deterministic. Do not retry; fix the payload.
401The key was missing, unrecognised, not ACTIVE, or not permitted for this caller. Nothing was stored. Do: stop sending and surface the failure loudly. A client that retries a 401 in a loop burns battery and fills logs without ever succeeding. Retry only after a human changes the key. Two body shapes exist, differing only in whether requestId is present: refusals from the key gate filter omit it, refusals from the key policy include it (as null unless you sent X-Request-Id). Do not key your handling on that field.
413The declared Content-Length exceeded the 1 MB body ceiling. Nothing was read or stored. Do: split the batch in half and retry both halves. A single event over the limit can never be sent — drop it and report it. Measure serialised size as you build the batch rather than discovering this on the wire.
415The body was not sent as application/json. Set the header; nothing else is accepted, and there is no compression negotiation on the request body.
429Either the per-key event budget was exceeded (60 000 events per minute by default; your key may carry its own limit), or the workspace is over its plan quota. The collector reports both as 429 rather than 401 on purpose: over quota is not a bad key, and an SDK must back off rather than disable itself. Do: retry with exponential backoff and jitter, and keep the events queued — they are still valid. The collector does not currently send a Retry-After header. Do not wait for one; use your own backoff. The budget is per collector instance, so behind N nodes the effective ceiling is N × the limit.
503The control plane was unreachable or unhealthy, so the key was never verified. The collector fails closed: an unverified key is not a valid key. Nothing was stored, and nothing was lost. Do: retry with backoff and keep the events queued. Do not treat this as an auth failure and do not disable the client.

202

json
{
  "status": "accepted",
  "acceptedCount": 1,
  "rejectedCount": 0,
  "rejected": [],
  "requestId": "c9dbbbcf-22bb-4149-88b9-ad06d24fe656",
  "serverReceivedAt": "2026-09-07T01:42:46.842364Z"
}

400

json
{
  "timestamp": "2026-09-07T01:42:48.089927Z",
  "status": 400,
  "error": "Bad Request",
  "message": "No event in the batch was valid",
  "rejected": [
    {
      "index": 0,
      "messageId": "doc-bad-0001",
      "errors": [
        "eventType is required",
        "userId must be at least 5 characters"
      ]
    }
  ],
  "path": "/collector/v1/events"
}

401

json
{
  "timestamp": "2026-09-07T01:42:45.236175Z",
  "status": 401,
  "error": "Unauthorized",
  "message": "Missing API key header: X-API-Key",
  "path": "/collector/v1/events"
}

429

json
{
  "timestamp": "2026-09-07T01:43:13.568789Z",
  "status": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded",
  "path": "/collector/v1/events",
  "requestId": null
}

Keys ​

Checking a key before an SDK queues anything.

GET /v1/key/validate ​

Check a key before queueing anything

Lets an SDK verify its key at initialisation and fail loudly, rather than silently queueing events against a key that will never be accepted.

The answer here is exactly the answer the first batch would get: the same resolver and the same policy checks (type, status, origin binding) run. It returns nothing a caller could not already learn from a 401 on ingest — the key's type, environment and app id — plus the features the workspace is entitled to, which SDKs read to skip capture work that would be discarded anyway (session replay, autocapture).

There is no valid: false body. An invalid key is a 401.

ParameterInTypeRequiredDescription
apiKeyquerystringnoFallback for navigator.sendBeacon, which cannot set headers. The X-API-Key header is preferred and takes precedence when both are sent.
OriginheaderstringnoSet automatically by browsers; never set it by hand. Its presence changes the key policy: a SECRET key with an Origin is refused, and a PUBLIC key is checked against its allow-list. Pre-flight OPTIONS carries no credentials by design and is answered by CORS before the key gate runs.
ResponseMeaning
200The key is valid and may be used from this caller.
401The key was missing, unrecognised, not ACTIVE, or not permitted for this caller. Nothing was stored. Do: stop sending and surface the failure loudly. A client that retries a 401 in a loop burns battery and fills logs without ever succeeding. Retry only after a human changes the key. Two body shapes exist, differing only in whether requestId is present: refusals from the key gate filter omit it, refusals from the key policy include it (as null unless you sent X-Request-Id). Do not key your handling on that field.
503The control plane was unreachable or unhealthy, so the key was never verified. The collector fails closed: an unverified key is not a valid key. Nothing was stored, and nothing was lost. Do: retry with backoff and keep the events queued. Do not treat this as an auth failure and do not disable the client.

200

json
{
  "valid": true,
  "appId": "668528e0-88b2-4a52-b176-10fb15412a9d",
  "environment": "production",
  "keyType": "SECRET",
  "features": {
    "session_replay": true,
    "experiments": true,
    "heatmaps": true,
    "funnels": true
  }
}

401

json
{
  "timestamp": "2026-09-07T01:42:45.236175Z",
  "status": 401,
  "error": "Unauthorized",
  "message": "Missing API key header: X-API-Key",
  "path": "/collector/v1/events"
}

Experiments ​

The definition feed SDKs poll to assign variants client-side.

GET /config/experiments ​

Fetch active experiment definitions

The definition feed client-side SDKs poll to assign variants locally, without a network call per assignment. Definitions are scoped to the project the key belongs to.

This route is authenticated by exactly the same mechanism as ingest — same header, same resolver, same origin and key-type policy — and fails closed the same way. A bad key is a 401, never an empty-but-successful body that an SDK would cache as "this workspace runs no experiments".

The response carries pollIntervalSeconds, and the same value is set as Cache-Control: max-age, private, so a shared proxy in front of a customer's site cannot hold a definition longer than the SDK itself would.

The payload deliberately omits the experiment's name, hypothesis and goal event: it is served to every browser on the customer's site, and those are the workspace's internal business. Only what assignment needs is here.

ParameterInTypeRequiredDescription
apiKeyquerystringnoFallback for navigator.sendBeacon, which cannot set headers. The X-API-Key header is preferred and takes precedence when both are sent.
OriginheaderstringnoSet automatically by browsers; never set it by hand. Its presence changes the key policy: a SECRET key with an Origin is refused, and a PUBLIC key is checked against its allow-list. Pre-flight OPTIONS carries no credentials by design and is answered by CORS before the key gate runs.
ResponseMeaning
200The active definitions for this key's project.
401The key was missing, unrecognised, not ACTIVE, or not permitted for this caller. Nothing was stored. Do: stop sending and surface the failure loudly. A client that retries a 401 in a loop burns battery and fills logs without ever succeeding. Retry only after a human changes the key. Two body shapes exist, differing only in whether requestId is present: refusals from the key gate filter omit it, refusals from the key policy include it (as null unless you sent X-Request-Id). Do not key your handling on that field.
503The control plane was unreachable or unhealthy, so the key was never verified. The collector fails closed: an unverified key is not a valid key. Nothing was stored, and nothing was lost. Do: retry with backoff and keep the events queued. Do not treat this as an auth failure and do not disable the client.

200

json
{
  "fetchedAt": "2026-09-07T01:42:49.818441Z",
  "pollIntervalSeconds": 300,
  "experiments": [
    {
      "key": "onboarding-checklist",
      "salt": "",
      "trafficAllocation": 100,
      "targeting": [
        {
          "property": "plan",
          "operator": "in",
          "values": [
            "pro"
          ]
        }
      ],
      "variants": [
        {
          "key": "control",
          "weight": 50,
          "payload": {
            "checklist": false
          }
        },
        {
          "key": "treatment",
          "weight": 50,
          "payload": {
            "checklist": true
          }
        }
      ]
    }
  ]
}

401

json
{
  "timestamp": "2026-09-07T01:42:45.236175Z",
  "status": 401,
  "error": "Unauthorized",
  "message": "Missing API key header: X-API-Key",
  "path": "/collector/v1/events"
}

Replay ​

Session-replay recorder chunks.

POST /v1/replay ​

Submit a session-replay chunk

Accepts one chunk of a browser recording. Same key contract as events — header or query parameter, resolved and origin-checked identically, same 1 MB body ceiling — but a separate pipeline and no per-event validation: the recorder payload is opaque to the collector.

Chunks are ordered within a session by seq. The browser SDK splits at its configured replay.chunkBytes (400 KB by default), well under the body limit.

If the workspace is not entitled to session replay the answer is 403. The SDK also checks entitlement before it starts recording; this is the backstop for stale bundles and forged clients.

ParameterInTypeRequiredDescription
apiKeyquerystringnoFallback for navigator.sendBeacon, which cannot set headers. The X-API-Key header is preferred and takes precedence when both are sent.
OriginheaderstringnoSet automatically by browsers; never set it by hand. Its presence changes the key policy: a SECRET key with an Origin is refused, and a PUBLIC key is checked against its allow-list. Pre-flight OPTIONS carries no credentials by design and is answered by CORS before the key gate runs.

Request body (required)

json
{
  "sessionId": "sess-88213",
  "userId": "user-10294",
  "deviceId": "3b6a1f42-9c77-4c1e-8a02-6d0f2b5e77aa",
  "seq": 0,
  "url": "https://app.example.com/reports",
  "page": "/reports",
  "startTs": 1725000000000,
  "endTs": 1725000005000,
  "events": [
    {
      "type": 4,
      "data": {}
    }
  ]
}
ResponseMeaning
202The chunk was accepted and queued.
400The body did not parse, or a required field (sessionId, events) was missing.
401The key was missing, unrecognised, not ACTIVE, or not permitted for this caller. Nothing was stored. Do: stop sending and surface the failure loudly. A client that retries a 401 in a loop burns battery and fills logs without ever succeeding. Retry only after a human changes the key. Two body shapes exist, differing only in whether requestId is present: refusals from the key gate filter omit it, refusals from the key policy include it (as null unless you sent X-Request-Id). Do not key your handling on that field.
403The workspace is not entitled to session replay. This is not an authentication failure and retrying will not help; stop recording.
413The declared Content-Length exceeded the 1 MB body ceiling. Nothing was read or stored. Do: split the batch in half and retry both halves. A single event over the limit can never be sent — drop it and report it. Measure serialised size as you build the batch rather than discovering this on the wire.
415The body was not sent as application/json. Set the header; nothing else is accepted, and there is no compression negotiation on the request body.
429Either the per-key event budget was exceeded (60 000 events per minute by default; your key may carry its own limit), or the workspace is over its plan quota. The collector reports both as 429 rather than 401 on purpose: over quota is not a bad key, and an SDK must back off rather than disable itself. Do: retry with exponential backoff and jitter, and keep the events queued — they are still valid. The collector does not currently send a Retry-After header. Do not wait for one; use your own backoff. The budget is per collector instance, so behind N nodes the effective ceiling is N × the limit.
503The control plane was unreachable or unhealthy, so the key was never verified. The collector fails closed: an unverified key is not a valid key. Nothing was stored, and nothing was lost. Do: retry with backoff and keep the events queued. Do not treat this as an auth failure and do not disable the client.

202

json
{
  "status": "accepted",
  "seq": 0,
  "events": 1
}

400

json
{
  "timestamp": "2026-09-07T01:42:48.697830Z",
  "status": 400,
  "error": "Bad Request",
  "message": "sessionId must not be blank",
  "path": "/collector/v1/replay",
  "requestId": null
}

401

json
{
  "timestamp": "2026-09-07T01:42:45.236175Z",
  "status": 401,
  "error": "Unauthorized",
  "message": "Missing API key header: X-API-Key",
  "path": "/collector/v1/events"
}

429

json
{
  "timestamp": "2026-09-07T01:43:13.568789Z",
  "status": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded",
  "path": "/collector/v1/events",
  "requestId": null
}

Specification ​

This document, fetched at runtime.

GET /openapi.yaml ​

This document, as YAML

Unauthenticated on purpose: a specification nobody can fetch is useless, and it describes only the four public routes above. No internal or control-plane route appears in it, and it contains no credentials.

ResponseMeaning
200The specification.

GET /openapi.json ​

This document, as JSON

The same specification, converted to JSON for tools that will not read YAML.

ResponseMeaning
200The specification.

Tangent — product analytics.