# REST API (/docs/reference/rest-api)

Everything you do in the app, you can do over the REST API. It's what the [MCP tools](/docs/agents/tools) use too. For most agent work, [MCP](/docs/agents/mcp) or the [CLI](/docs/agents/cli) is easier.

## Basics [#basics]

```text
https://app.datahop.dev/api/projects/:projectId/…
```

- **Project id**: it's in the app's URL (`/p/1/…`), or ask `GET /api/me`.
- **Auth**: `Authorization: Bearer dh_sk_…`, with an [API key](/docs/agents/api-keys) for that project.
- **JSON** in and out. `PATCH` changes only the fields you send.
- **Times** are ISO 8601 in UTC. Chart labels are in the project timezone.
- **Endpoints that take a person** accept a person id or a distinct id.
- **Names in paths** must be URL-encoded, for example `/events/%24pageview`.

## Errors [#errors]

Every error looks like this:

```json
{ "error": "invalid_query", "message": "Math \"sum\" needs mathProperty", "hint": "Set mathProperty to a numeric event property, e.g. \"amount\"" }
```

Branch on `error`. Show or log `message` and `hint`: hints say how to fix the call.

| Status | `error` | Meaning |
| --- | --- | --- |
| 400 | `invalid_query`, `invalid_request`, … | Something in the request is wrong. Check the hint. |
| 401 | `unauthorized` | Missing or invalid key. |
| 403 | `forbidden` | A read-only key tried to write, the key belongs to another project, or only signed-in people can do this. |
| 403 | `module_disabled` | That product is switched off in Settings → Products. |
| 404 | `not_found` | It doesn't exist. |
| 409 | `key_taken`, `in_use`, `invalid_state`, `experiment_running` | A conflict: a flag key already exists, something is still in use, an experiment is in the wrong state, or you're changing a running experiment's variants. |

## Run any analysis [#run-any-analysis]

`POST /query` takes any [query object](/docs/reference/query-language) and returns the result with a `summary`.

```bash
curl -s https://app.datahop.dev/api/projects/1/query \
  -H "Authorization: Bearer $DATAHOP_KEY" -H 'content-type: application/json' \
  -d '{ "kind": "funnel", "steps": [{ "event": "signed_up" }, { "event": "plan_upgraded" }], "windowDays": 14 }'
```

Results can be up to a minute old. Add `?refresh=1` to get fresh numbers.

## Endpoints [#endpoints]

Paths are relative to `/api/projects/:projectId`. **W** needs a read & write key.

| Area | Endpoints |
| --- | --- |
| Schema | `GET /events` · `PATCH /events/:name` W (description) · `GET /properties?type=event\|person\|group` · `PATCH /properties/:type/:key` W · `GET /property-values?key=` |
| Insights | `GET /insights` · `GET /insights/:id` · `POST /insights` W · `PATCH /insights/:id` W · `DELETE /insights/:id` W |
| Dashboards | `GET /dashboards` · `GET /dashboards/:id` · `POST /dashboards` W · `PATCH /dashboards/:id` W · `DELETE /dashboards/:id` W · `POST /dashboards/:id/tiles` W · `PATCH /dashboards/:id/tiles/:tileId` W · `DELETE /dashboards/:id/tiles/:tileId` W · `PUT /dashboards/:id/share` W |
| Persons | `GET /persons?search=&cohortId=` · `GET /persons/:id` · `PATCH /persons/:id` W (`{ set, unset }`) · `DELETE /persons/:id` W |
| Cohorts | `GET /cohorts` · `GET /cohorts/:id` · `POST /cohorts` W · `PATCH /cohorts/:id` W · `DELETE /cohorts/:id` W · `GET /cohorts/:id/persons` · `POST /cohorts/:id/calculate` · `POST /cohorts/:id/members` W · `DELETE /cohorts/:id/members` W |
| Groups | `GET /group-types` · `PATCH /group-types/:index` W (display names) · `GET /groups?groupTypeIndex=&search=` · `GET /groups/:index/:key` |
| Feature flags | `GET /flags` · `GET /flags/:id` · `POST /flags` W · `PATCH /flags/:id` W · `DELETE /flags/:id` W · `POST /flags/evaluate` |
| Experiments | `GET /experiments` · `GET /experiments/:id` · `POST /experiments` W · `PATCH /experiments/:id` W · `POST /experiments/:id/launch` W · `POST /experiments/:id/stop` W · `POST /experiments/:id/conclude` W · `DELETE /experiments/:id` W · `GET /experiments/:id/results` · `POST /assist/experiment` (draft from a description) |
| Feedback | `GET /feedback?status=&cohortId=&search=` · `GET /feedback/stats` · `PATCH /feedback/:id` W |
| Context | `GET /annotations` · `POST /annotations` W · `DELETE /annotations/:id` W · `GET /activity` |
| Project | `GET /api/projects/:projectId` · `PATCH /api/projects/:projectId` W (name, timezone) · `GET /modules` · `PATCH /modules` W (products on/off) |

Lists of persons and groups return `{ total, results }` and page with `limit` (up to 200) and `offset`.

Creating and revoking API keys, rotating the project token and deleting the project are only possible for signed-in people in the app.

## Examples [#examples]

```bash
# create a cohort of power users
curl -s https://app.datahop.dev/api/projects/1/cohorts \
  -H "Authorization: Bearer $DATAHOP_KEY" -H 'content-type: application/json' \
  -d '{ "name": "Power users", "definition": { "match": "all", "criteria": [
        { "type": "behavior", "event": "project_opened", "withinDays": 14, "minCount": 20 } ] } }'

# widen a flag to 30% of everyone
curl -s -X PATCH https://app.datahop.dev/api/projects/1/flags/4 \
  -H "Authorization: Bearer $DATAHOP_KEY" -H 'content-type: application/json' \
  -d '{ "active": true, "filters": { "conditions": [{ "properties": [], "rolloutPercentage": 30 }] } }'

# conclude an experiment and roll out the winner
curl -s https://app.datahop.dev/api/projects/1/experiments/4/conclude \
  -H "Authorization: Bearer $DATAHOP_KEY" -H 'content-type: application/json' \
  -d '{ "winner": "test", "reason": "+26.9% upgrades, driven by new users.", "rollOut": true }'
```

The fields for flags, experiments and cohorts match what you see in the app. Flag conditions use [property filters](/docs/reference/query-language#filters), and cohort rules use [cohort definitions](/docs/reference/query-language#cohort-definitions).
