---
title: Projects
description: The tenant boundary — project details, the legacy key pair, and scoped API keys.
---

A **project** is the isolation boundary for everything in Retidal: every event, campaign, short link, and
email lives under exactly one `projectId`, and every management path is scoped to
`/api/projects/{projectId}/...`.

## Authentication on this page

Project management is mostly a **console session** (`trackly_session` cookie) surface. Two read routes are
the exception — they also accept the project's own legacy API key:

| Route | Accepts a project API key? |
| --- | --- |
| `GET /api/projects` | Yes — returns only that key's own project. |
| `GET /api/projects/{projectId}` | Yes — read-only. |
| Everything else on this page (update, regenerate keys, manage scoped keys) | No — session cookie only. |

See [Authentication](/docs/authentication) for the full model.

## Get and update a project

```
GET https://retidal.com/api/projects/{projectId}
PUT https://retidal.com/api/projects/{projectId}
```

`GET` returns the safe project shape to VIEWER/EDITOR callers, and additionally includes the project's
`apiKey`/`apiSecret` when the caller has role ADMIN or OWNER (or is that project's own API key).

`PUT` (requires ADMIN) updates `name`, `description`, and `domain` — all optional, `description`/`domain`
nullable.

<Note title="Creating and deleting a project aren't API-key operations at all">
  `POST /api/projects` requires a **tenant** ADMIN role (not a project role) and a `name` + `slug` — it's how
  a new project gets its `apiKey`/`apiSecret` minted in the first place, so naturally it can't be
  authenticated with a project key that doesn't exist yet. `DELETE /api/projects/{projectId}` requires
  project **OWNER** specifically — ADMIN is not enough. Both are console-only actions; there's no
  script-friendly path around them.
</Note>

## Regenerate the legacy key pair

```
POST /api/projects/{projectId}/regen-key
```

Requires ADMIN. Rotates the project's `apiKey`/`apiSecret` — the pair used as `X-API-Key` on the
[ingestion API](/docs/sending-data/events) and (where a project key is accepted) on read-only console routes.
**The old pair is invalidated immediately** with no overlap window; update every deployed integration before
you rotate, not after.

This is a distinct credential from the scoped API keys below.

## Scoped API keys

```
GET    /api/projects/{projectId}/api-keys
POST   /api/projects/{projectId}/api-keys      (ADMIN)
PATCH  /api/projects/{projectId}/api-keys/{id} (ADMIN)
DELETE /api/projects/{projectId}/api-keys/{id} (ADMIN)
```

A scoped key is a second, independent kind of credential: it carries an explicit list of capability scopes
instead of a project role, and is used as `X-API-Key` (no secret) on the ingestion/decisioning surface and on
the [MCP server](/docs/mcp/overview).

```json title="Create a scoped key"
{ "name": "Backend event forwarder", "scopes": ["ingest:write", "decision:call"] }
```

<ParamField name="name" type="string" required>
  1–120 characters.
</ParamField>
<ParamField name="scopes" type="array" required>
  At least one of: `ingest:write`, `tracking:ingest`, `decision:call`, `decision:read`, `profile:write`,
  `coupon:redeem`, `coupon:read`, `metrics:read`, `metrics:write`, `mcp:events_read`, `mcp:events_write`,
  `mcp:links_write`, `mcp:attribution_read`.
</ParamField>

<Danger title="The raw key is shown exactly once">
  `POST` returns the raw key string in the response body — that's the only time it's ever visible. `GET`
  always returns keys masked. Store it in your secret manager immediately; there's no recovery endpoint.
</Danger>

`PATCH` updates `scopes` and/or `name` (at least one required) and invalidates the credential cache
immediately, so a scope change takes effect on the next request rather than waiting out a TTL. `DELETE
/api/projects/{projectId}/api-keys/{id}` revokes a key; it's idempotent — revoking an already-revoked key
still returns `200`.

## Roles at a glance

Project roles form a strict hierarchy: `VIEWER < EDITOR < ADMIN < OWNER`. A project API key (the legacy
pair, not a scoped key) authenticates the two read routes above as if it were the project itself — it never
satisfies an `OWNER`-only check (delete project, transfer ownership), which is why project deletion is
console-only.

## Next steps

<CardGroup cols={2}>
  <Card title="Members" href="/docs/console/members">
    Who has which role on this project, and how to change it.
  </Card>
  <Card title="Data queries" href="/docs/console/queries">
    Analytics, funnels, retention, and the user/value-user directory.
  </Card>
</CardGroup>
