---
title: Authentication
description: API keys, capability scopes, and the console session cookie.
---

Every request to Retidal authenticates one of two ways, depending on which API it targets.

## Prerequisites

- A Retidal project and at least one project API key — see [Quickstart](/docs/quickstart) to issue one.
- For Management API calls, a console session (log in at the console) or a key with the `metrics:read`/`metrics:write` scope.

## Ingestion & Decisioning API — API key

Send your project API key in the `X-API-Key` request header:

```bash
curl -X POST https://api.retidal.com/api/v1/t \
  -H "X-API-Key: tk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "eventName": "page_view", "visitorId": "vid_abc123" }'
```

For pixel/beacon contexts where a header cannot be set (`navigator.sendBeacon()`), the key may instead be passed as the `_ak` (or `key`) query parameter. Prefer the header everywhere else — query strings are logged by intermediaries.

### Capability scopes

Keys carry explicit capability scopes. A legacy key that has never been given scopes behaves as `ingest:write` only.

| Scope | Grants |
| --- | --- |
| `ingest:write` / `tracking:ingest` | `POST /api/v1/t`, `POST /api/v1/identify` |
| `decide` / `decision:call` | `POST /api/v1/decide` |
| `profile:write` | `PUT /api/v1/profile` |
| `coupon:redeem` | `POST /api/v1/coupons/redeem` |
| `coupon:read` | `GET /api/v1/coupons/{code}` |
| `metrics:read` / `metrics:write` | read/write metric definitions (Management API) |
| `mcp:events_read`, `mcp:events_write`, `mcp:links_write`, `mcp:attribution_read` | Retidal MCP server tools |

Scoped keys are issued from the console: `POST /api/projects/{projectId}/api-keys` (project role ADMIN+) returns the raw key exactly once — it cannot be retrieved again. `PATCH /api/projects/{projectId}/api-keys/{id}` updates a key's scopes or name and invalidates its cache entry immediately.

A key is bound to exactly one project. Calling another project's resource returns the same response shape as "not found" — resource existence is never leaked across tenants. On a representative route like `GET /api/auth/me`, a revoked or invalid key returns `401`; on a scoped route like `GET /api/admin/users`, a valid key missing the required scope returns `403` (see each endpoint's own page for its exact codes).

`POST /api/v1/t` and `POST /internal/backfill/events` accept any valid project API key with no additional scope gate.

## Management API — session cookie

Most `/api/projects/{projectId}/...` routes authenticate with the console session cookie `trackly_session`, established by the console login flow, and authorize by the caller's **project role** (`OWNER` / `ADMIN` / `MEMBER`, or `VIEWER+` for read routes).

The Management API accepts a scoped `X-API-Key` on only two kinds of route, and each operation states explicitly which:

1. Routes gated by a capability scope (`requireProjectRoleOrApiScope`) — currently the metric-definitions endpoints, requiring `metrics:read` or `metrics:write`.
2. A small number of read-only project endpoints that accept the project's own legacy API key with no scope requirement.

Every other Management route — e.g. `GET /api/tenant/profile` — rejects an API key with `403` and requires the session cookie.

There is also an internal `x-worker-token` scheme (`WorkerToken`) used for worker-to-worker calls — it is not for customer use.

## What this unlocks

Knowing which credential a call needs unlocks calling every endpoint correctly on the first
try. [Sending data](/docs/sending-data/events) needs no scope at all — `POST /api/v1/t` and
`POST /api/v1/identify` declare only `ApiKeyAuth`, so a valid project key is sufficient, and
a scoped key carrying `ingest:write` or `tracking:ingest` works the same way.
[Decisioning](/docs/decisioning/decide) is the opposite: it is scope-gated, and a key
without the right scope is rejected even though it is valid.

## Verify it worked

Confirm your key is valid — `POST /api/v1/t` has no scope gate beyond a valid project key, so this call only proves the key itself is good, not that it carries any particular scope:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.retidal.com/api/v1/t \
  -H "X-API-Key: $RETIDAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"eventName": "page_view", "visitorId": "vid_abc123"}'
# expect: 200 (with ?sync=1) or 202 — either confirms the key is valid;
# a 401 means it is not
```

To confirm a specific scope, call the scoped endpoint itself — e.g. `POST /api/v1/decide` — and expect `403` if the scope is missing — see the scope table above.

## If it doesn't work

If the `POST /api/v1/t` ingestion request above returns `401`, work through [Events not arriving](/docs/troubleshooting/events-not-arriving). If a scoped endpoint like `POST /api/v1/decide` (`decide`, `profile`, coupon routes) returns `403`, the key is valid but missing that scope — reissue or `PATCH` the key with the required scope. If a Management API call or console session fails instead, work through the [health checklist](/docs/troubleshooting/health-checklist).
