# Admin API

The admin and data endpoints let you manage projects and pull data programmatically. Everything under /api/* is authenticated with your account API key - find it in the dashboard under Settings. This page covers project management, event and user data, and stats; analytical queries are on the Queries page, and feature flag management is on the Feature Flags API page.

The API key grants read and admin access to all your projects. Keep it
server-side - never ship it in client code. For client-side event tracking
use the project token instead (see [Ingestion](/api/ingestion)).

## Authentication

Send `Authorization: Bearer $API_KEY` on every `/api/*` request - either
your superadmin key or a Firebase ID token. A missing or invalid credential
returns `401`.

```bash
curl https://cohorly-service.velloalabs.com/api/projects \
  -H "Authorization: Bearer $API_KEY"
```

## Projects

GET /api/projects List projects. A superadmin credential sees every project across every org; a user credential sees only their own org's.

```bash
curl https://cohorly-service.velloalabs.com/api/projects \
  -H "Authorization: Bearer $API_KEY"
```

```js
await fetch("https://cohorly-service.velloalabs.com/api/projects", {
  headers: { Authorization: `Bearer ${API_KEY}` },
});
```

```json
[
  {
    "id": 1,
    "name": "Production",
    "token": "1c9e2f2a-6a3e-4a54-9b7d-6a2a9d1a7b21",
    "created_at": 1753900800000,
    "org_id": 1,
    "event_count": 128340
  }
]
```

```json
{ "error": "unauthorized" }
```

```json
{ "error": "no organization" }
```

POST /api/projects Create a project. Creates in the caller's org (superadmin creates in the bootstrap org).

name string Must be unique within the org.

```bash
curl -X POST https://cohorly-service.velloalabs.com/api/projects \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "mobile-app" }'
```

```js
await fetch("https://cohorly-service.velloalabs.com/api/projects", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "mobile-app" }),
});
```

```json
{
  "id": 4,
  "name": "mobile-app",
  "token": "9d1a7b21-6a3e-4a54-9b7d-1c9e2f2a6a3e",
  "created_at": 1753900800000
}
```

```json
{ "error": "name required" }
```

```json
{ "error": "unauthorized" }
```

```json
{ "error": "name taken" }
```

Two independent conditions return `409`, each with its own message: a name
already used within the org gives `"error": "name taken"`, and hitting the
org's project quota gives `"error": "project limit reached"`.

DELETE /api/projects/{id} Delete a project and all of its data. Requires the org owner role (or superadmin).

id integer Path parameter - the project id.

```bash
curl -X DELETE https://cohorly-service.velloalabs.com/api/projects/4 \
  -H "Authorization: Bearer $API_KEY"
```

```js
await fetch("https://cohorly-service.velloalabs.com/api/projects/4", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${API_KEY}` },
});
```

```json
{ "ok": true }
```

```json
{ "error": "unauthorized" }
```

```json
{ "error": "forbidden" }
```

```json
{ "error": "cannot delete last project" }
```

## Event metadata

These GETs accept an optional `projectId` query param; omitting it targets
your first project, so pass it explicitly when you track more than one app.

GET /api/events Raw event stream, newest first.

projectId string Target project id. Defaults to the org's oldest project (or the legacy default project for a superadmin credential).

limit integer 100 Max 1000.

event string Filter to one event name. Applies lexicon merge aliasing (matches the canonical event plus its merged children).

distinct\_id string Filter to one user.

from string (date) YYYY-MM-DD, inclusive.

to string (date) YYYY-MM-DD, inclusive.

search string Case-insensitive substring match on the event name.

filters string JSON-encoded PropertyFilter\[] (same shape as segmentation). Invalid JSON or shape returns 400.

```bash
curl "https://cohorly-service.velloalabs.com/api/events?projectId=1&limit=50&event=Signed+Up" \
  -H "Authorization: Bearer $API_KEY"
```

```js
await fetch(
  "https://cohorly-service.velloalabs.com/api/events?projectId=1&limit=50&event=Signed+Up",
  { headers: { Authorization: `Bearer ${API_KEY}` } },
);
```

```json
{
  "events": [
    {
      "id": 10245,
      "event": "Signed Up",
      "distinct_id": "user-42",
      "time": 1753900800000,
      "properties": { "plan": "free", "$city": "Lisbon" }
    }
  ],
  "total": 8213
}
```

```json
{ "error": "invalid filters" }
```

```json
{ "error": "unauthorized" }
```

```json
{ "error": "forbidden" }
```

GET /api/events/names Distinct event names observed in the project - useful for autocomplete.

projectId string Target project id.

raw "1" Bypass lexicon governance (merge collapsing, hidden exclusion) and return observed names untouched.

```bash
curl "https://cohorly-service.velloalabs.com/api/events/names?projectId=1" \
  -H "Authorization: Bearer $API_KEY"
```

```js
await fetch("https://cohorly-service.velloalabs.com/api/events/names?projectId=1", {
  headers: { Authorization: `Bearer ${API_KEY}` },
});
```

```json
[{ "name": "Signed Up", "count": 8213, "recent_count": 402, "last_seen": 1753900800000 }]
```

```json
{ "error": "unauthorized" }
```

```json
{ "error": "forbidden" }
```

GET /api/events/properties Property keys observed on a given event name.

projectId string Target project id.

event string Event name to inspect.

raw "1" Bypass lexicon governance.

```bash
curl "https://cohorly-service.velloalabs.com/api/events/properties?projectId=1&event=Signed+Up" \
  -H "Authorization: Bearer $API_KEY"
```

```js
await fetch(
  "https://cohorly-service.velloalabs.com/api/events/properties?projectId=1&event=Signed+Up",
  { headers: { Authorization: `Bearer ${API_KEY}` } },
);
```

```json
["plan", "$city", "$browser"]
```

```json
{ "error": "event required" }
```

```json
{ "error": "unauthorized" }
```

```json
{ "error": "forbidden" }
```

## Users

GET /api/users List user profiles.

projectId string Target project id.

limit integer 50 Max 500.

search string Substring match on distinct\_id or the serialized properties.

```bash
curl "https://cohorly-service.velloalabs.com/api/users?projectId=1&limit=20" \
  -H "Authorization: Bearer $API_KEY"
```

```js
await fetch("https://cohorly-service.velloalabs.com/api/users?projectId=1&limit=20", {
  headers: { Authorization: `Bearer ${API_KEY}` },
});
```

```json
[
  {
    "distinct_id": "user-42",
    "properties": { "plan": "pro", "$name": "Ada Lovelace" },
    "first_seen": 1751600000000,
    "last_seen": 1753900800000,
    "event_count": 214
  }
]
```

```json
{ "error": "unauthorized" }
```

```json
{ "error": "forbidden" }
```

GET /api/users/{distinctId} One user's profile plus their last 100 events.

projectId string Target project id.

distinctId string Path parameter - the user to fetch.

```bash
curl "https://cohorly-service.velloalabs.com/api/users/user-42?projectId=1" \
  -H "Authorization: Bearer $API_KEY"
```

```js
await fetch("https://cohorly-service.velloalabs.com/api/users/user-42?projectId=1", {
  headers: { Authorization: `Bearer ${API_KEY}` },
});
```

```json
{
  "distinct_id": "user-42",
  "properties": { "plan": "pro", "$name": "Ada Lovelace" },
  "first_seen": 1751600000000,
  "last_seen": 1753900800000,
  "event_count": 214,
  "events": [
    {
      "id": 10245,
      "event": "Signed Up",
      "distinct_id": "user-42",
      "time": 1753900800000,
      "properties": { "plan": "free" }
    }
  ]
}
```

```json
{ "error": "unauthorized" }
```

```json
{ "error": "forbidden" }
```

```json
{ "error": "not found" }
```

## Stats

GET /api/stats Project-level counters, shown on the dashboard overview.

projectId string Target project id.

```bash
curl "https://cohorly-service.velloalabs.com/api/stats?projectId=1" \
  -H "Authorization: Bearer $API_KEY"
```

```js
await fetch("https://cohorly-service.velloalabs.com/api/stats?projectId=1", {
  headers: { Authorization: `Bearer ${API_KEY}` },
});
```

```json
{
  "events": 128340,
  "users": 812,
  "profiles": 790,
  "event_names": 34,
  "events_today": 1204,
  "sessions": 96
}
```

```json
{ "error": "unauthorized" }
```

```json
{ "error": "forbidden" }
```

## Status (no auth)

Public health endpoints backing the [status page](https://cohorly-status.velloalabs.com) -
no Bearer key or project token required.

### Components

Six components are monitored independently. `id` is the value used in every
request and response below.

| id          | name            | group         |
| ----------- | --------------- | ------------- |
| `api`       | API             | Core Platform |
| `db`        | Database        | Core Platform |
| `ingestion` | Event Ingestion | Core Platform |
| `dashboard` | Dashboard       | Web           |
| `docs`      | Documentation   | Web           |
| `status`    | Status Page     | Web           |

Each probe target is opt-in on the server. A component that has no probe
configured is simply absent from `components[]` - it is never fabricated as
healthy, and never counted into any uptime number.

A component's `status` is resolved by checking these rules in order - the
first match wins:

| order | status        | condition                                                                                              |
| ----- | ------------- | ------------------------------------------------------------------------------------------------------ |
| 1     | `unknown`     | No check recorded in the last 5 minutes (or ever). Rendered as "no data", not as an outage.            |
| 2     | `maintenance` | An `in_progress` maintenance window covers this component.                                             |
| 3     | `degraded`    | An open incident with `minor` impact covers this component.                                            |
| 3     | `down`        | An open incident with `major`/`critical` impact covers this component (`none` impact never overrides). |
| 4     | `operational` | None of the above, and the latest check was `ok`.                                                      |
| 4     | `down`        | None of the above, and the latest check failed.                                                        |

GET /status Current health snapshot: overall ok/db, every monitored component, active incidents, and scheduled maintenance.

```bash
curl https://cohorly-service.velloalabs.com/status
```

```js
await fetch("https://cohorly-service.velloalabs.com/status");
```

```json
{
  "ok": true,
  "db": true,
  "uptime_seconds": 128340,
  "latest": { "checked_at": "2026-07-31T12:00:00.000Z", "ok": true, "latency_ms": 42 },
  "components": [
    {
      "id": "api",
      "name": "API",
      "group": "Core Platform",
      "status": "operational",
      "latest": { "checked_at": "2026-07-31T12:00:00.000Z", "ok": true, "latency_ms": 11 },
      "uptime_30d": 99.99,
      "uptime_90d": 99.97
    },
    {
      "id": "db",
      "name": "Database",
      "group": "Core Platform",
      "status": "operational",
      "latest": { "checked_at": "2026-07-31T12:00:00.000Z", "ok": true, "latency_ms": 42 },
      "uptime_30d": 99.98,
      "uptime_90d": 99.95
    },
    {
      "id": "dashboard",
      "name": "Dashboard",
      "group": "Web",
      "status": "unknown",
      "latest": null,
      "uptime_30d": null,
      "uptime_90d": null
    }
  ],
  "active_incidents": [],
  "scheduled_maintenance": []
}
```

`ok` and `db` keep their original, narrower meaning: API + database health
only. A `docs`, `dashboard` or `status` probe going down never flips them -
those show up only inside `components[]`. Existing integrations reading
`ok`/`db`/`latest` do not need to change.

`active_incidents` holds every `kind: "incident"` row whose `status` is not
`resolved`. `scheduled_maintenance` holds every `kind: "maintenance"` row
whose `status` is not `completed`. Both are `Incident[]` - see the shape and
status enums under [Incidents](#incidents) below.

### History

GET /status/history Daily uptime aggregates for one component. The server records a health check every 60 seconds and keeps 90 days of history. Unchanged shape - existing integrations keep working.

days integer 90 Clamped to 1-90.

component string db One of the component ids above. Defaults to db, which is exactly today's meaning. An unknown id returns 400.

```bash
curl "https://cohorly-service.velloalabs.com/status/history?days=30&component=db"
```

```js
await fetch("https://cohorly-service.velloalabs.com/status/history?days=30&component=db");
```

```json
[{ "date": "2026-07-30", "total": 1440, "ok_count": 1439, "uptime_pct": 99.93, "avg_latency_ms": 38.2 }]
```

```json
{ "error": "<reason>" }
```

GET /status/history/all Daily uptime aggregates for every monitored component in one call - what the status page itself uses.

days integer 90 Clamped to 1-90.

```bash
curl "https://cohorly-service.velloalabs.com/status/history/all?days=30"
```

```js
await fetch("https://cohorly-service.velloalabs.com/status/history/all?days=30");
```

```json
{
  "days": 30,
  "components": {
    "api": [
      { "date": "2026-07-30", "total": 1440, "ok_count": 1440, "uptime_pct": 100, "avg_latency_ms": 12.4 }
    ],
    "db": [
      { "date": "2026-07-30", "total": 1440, "ok_count": 1439, "uptime_pct": 99.93, "avg_latency_ms": 38.2 }
    ]
  }
}
```

A day with no recorded checks is omitted from that component's array rather
than reported as `0`. Only components with a probe configured appear as keys
in `components`.

### Incidents

GET /status/incidents Incident and maintenance history, newest first.

days integer 90 Clamped to 1-365. Every unresolved incident and every open maintenance window is included regardless of age; resolved/completed ones are included only if they fall inside the window.

```bash
curl "https://cohorly-service.velloalabs.com/status/incidents?days=90"
```

```js
await fetch("https://cohorly-service.velloalabs.com/status/incidents?days=90");
```

```json
{
  "incidents": [
    {
      "id": 42,
      "kind": "incident",
      "title": "Elevated ingestion latency",
      "status": "resolved",
      "impact": "minor",
      "components": ["ingestion"],
      "started_at": "2026-07-28T09:15:00.000Z",
      "resolved_at": "2026-07-28T10:02:00.000Z",
      "scheduled_start": null,
      "scheduled_end": null,
      "updates": [
        { "id": 101, "status": "investigating", "body": "Investigating elevated /track latency.", "created_at": "2026-07-28T09:15:00.000Z" },
        { "id": 102, "status": "identified", "body": "Root cause is a slow query in the ingestion path.", "created_at": "2026-07-28T09:40:00.000Z" },
        { "id": 103, "status": "resolved", "body": "Fix deployed, latency back to normal.", "created_at": "2026-07-28T10:02:00.000Z" }
      ]
    },
    {
      "id": 43,
      "kind": "maintenance",
      "title": "Database upgrade",
      "status": "scheduled",
      "impact": "none",
      "components": ["db"],
      "started_at": "2026-08-01T00:00:00.000Z",
      "resolved_at": null,
      "scheduled_start": "2026-08-10T02:00:00.000Z",
      "scheduled_end": "2026-08-10T03:00:00.000Z",
      "updates": []
    }
  ]
}
```

`updates` is oldest-first. `status` is one of two enums depending on `kind`:

| kind          | status values                                                 |
| ------------- | ------------------------------------------------------------- |
| `incident`    | `investigating` -> `identified` -> `monitoring` -> `resolved` |
| `maintenance` | `scheduled` -> `in_progress` -> `completed`                   |

`impact` is `none` | `minor` | `major` | `critical` and only applies to
incidents (`none` never overrides a component's status; `minor` degrades it,
`major`/`critical` mark it down - see the status table above). `resolved_at`
is set automatically when `status` becomes `resolved` or `completed`.

Creating and updating incidents is a superadmin-only operation performed from
the backoffice, not documented here.
