---
title: Members
description: Manage who has access to a project, and at what role — console session only, no API key path.
---

Members are the people with access to a project through the console. This is a **session-only** surface —
there is no way to manage members with a project API key or a scoped key, even a read.

## Authentication

Every endpoint below requires the `trackly_session` cookie from a logged-in console user. A project API key
(legacy pair or scoped) gets no access at all here, not even `GET` — this differs from
[Projects](/docs/console/projects), where the two read routes do accept a project key.

## List members

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

Requires any project role. Returns the member list and `currentUserRole` for the caller.

## Add a member

```
POST /api/projects/{projectId}/members
```

Requires project ADMIN.

```json
{ "email": "teammate@yourcompany.com", "role": "VIEWER" }
```

<ParamField name="email" type="string" required>
  Must belong to a user who **already has an account in the same tenant**. There is no "invite a brand-new
  person directly onto a project" path — a person outside the tenant has to be invited to the tenant first
  (a separate, tenant-level invitation flow, not covered by this project-scoped endpoint).
</ParamField>
<ParamField name="role" type="string">
  `ADMIN` | `EDITOR` | `VIEWER`. Default `VIEWER`.
</ParamField>

`POST /api/projects/{projectId}/members` returns `409` if the user is already a project member, `404` if
the user doesn't exist, `400` if the user exists but isn't in the project's tenant.

## Change a member's role

```
PUT /api/projects/{projectId}/members/{memberId}
```

Requires project ADMIN. Body: `{ "role": "ADMIN" | "EDITOR" | "VIEWER" }`.

<Warning title="The OWNER's role can't be changed here">
  `PUT /api/projects/{projectId}/members/{memberId}` can move anyone between `ADMIN`/`EDITOR`/`VIEWER`, but
  the project's `OWNER` is immutable through this route — attempting it returns `403`. Ownership transfer is
  a separate, tenant-level operation.
</Warning>

## Remove a member

```
DELETE /api/projects/{projectId}/members/{memberId}
```

Requires project ADMIN. On `DELETE /api/projects/{projectId}/members/{memberId}`, the `OWNER` cannot be
removed (`403`).

## Role reference

| Role | Can do |
| --- | --- |
| `VIEWER` | Read analytics, campaigns, links, project events — anything gated to VIEWER+. |
| `EDITOR` | Everything VIEWER can, plus create/update most resources (templates, triggers, links, platforms). |
| `ADMIN` | Everything EDITOR can, plus destructive operations (delete), member management, project settings, scoped API keys. |
| `OWNER` | Everything ADMIN can, plus delete the project and transfer ownership. Exactly one per project; immutable through the members endpoints above. |

Two resource groups deviate from the usual EDITOR-writes/ADMIN-deletes pattern, worth knowing if you're
handing out roles by task rather than by trust level:

- **Custom domains** (`/api/projects/{projectId}/domains`) require EDITOR for **every** route, including `GET` —
  it's the one resource group with no VIEWER-readable path.
- **Derived rules** (`/api/projects/{projectId}/derived-rules`) require ADMIN for all writes, where most other
  resources only need EDITOR.

## Next steps

<CardGroup cols={2}>
  <Card title="Projects" href="/docs/console/projects">
    Project details, the legacy key pair, and scoped API keys.
  </Card>
  <Card title="Data queries" href="/docs/console/queries">
    Analytics, funnels, retention, and the user/value-user directory.
  </Card>
</CardGroup>
