---
title: Members
description: List your organization's members, invite or add people, change roles, and remove members.
product: gps
section: api
order: 18
---

# Members

Members are the people in your GPS organization. A member's `id` (their **member ID**) is the value every other endpoint uses for people: `assignedTo`, `assignees`, `attendees`, `onBehalfOf`, `createdBy` and so on.

The paths say `users`, but every endpoint works with members of your organization.

| Method | Path | Scope |
|--------|------|-------|
| `GET` | [`/users`](#list-members) | `users:read` |
| `GET` | [`/users/{id}`](#get-a-member) | `users:read` |
| `POST` | [`/users`](#invite-or-add-a-member) | `users:write` |
| `PATCH` | [`/users/{id}`](#update-a-member) | `users:write` |
| `DELETE` | [`/users/{id}`](#remove-a-member) | `users:write` |

Member changes don't fire webhooks.

The JavaScript examples assume a `headers` object holding your key:

```js
const headers = {
  'X-API-Key': process.env.GPS_API_KEY_ID,
  'X-API-Secret': process.env.GPS_API_SECRET,
};
```

## The member object

| Field | Type | Notes |
|-------|------|-------|
| `id` | string | Member ID. |
| `organizationId` | string | |
| `userId` | string | The person's GPS account ID. Not used by other endpoints. |
| `name` | string | Empty string if unknown. |
| `email` | string | Empty string if unknown. |
| `role` | string | `Owner`, `Super Admin`, `Admin`, `Team Leader`, `Team Member` or `Viewer`. |
| `position` | string or null | Job title. |
| `inviteStatus` | string or null | `pending`, `invited`, `accepted`, `added`, `declined` or `revoked`. |
| `isActive` | boolean | `false` for deactivated members. |
| `joinedAt` | string or null | ISO 8601 timestamp. |
| `createdAt` | string or null | ISO 8601 timestamp. |

## List members

`GET /users`

By default, returns active members only.

**Scope:** `users:read`

**Query parameters**

| Parameter | Type | Notes |
|-----------|------|-------|
| `includePending` | boolean | `true` also returns people whose invitation hasn't been accepted yet. |
| `includeInactive` | boolean | `true` also returns deactivated members. |
| `role` | string | One of `Owner`, `Super Admin`, `Admin`, `Team Leader`, `Team Member`, `Viewer`. Match the capitals, and encode the space (`Team%20Leader`). |
| `inviteStatus` | string | One of `pending`, `invited`, `accepted`, `added`, `declined`, `revoked`. |
| `isActive` | boolean | `true` or `false`. To find deactivated members, combine `isActive=false` with `includeInactive=true`. |
| `limit` | integer | 1–200, default 50. |
| `offset` | integer | Default 0. |

```bash
curl "https://plan.empiraa.app/api/v1/users?role=Team%20Leader&includePending=true" \
  -H "X-API-Key: $GPS_API_KEY_ID" \
  -H "X-API-Secret: $GPS_API_SECRET"
```

```js
const params = new URLSearchParams({ role: 'Team Leader', includePending: 'true' });
const res = await fetch(`https://plan.empiraa.app/api/v1/users?${params}`, { headers });
const { data: members, meta } = await res.json();
```

**Response `200`**

```json
{
  "data": [
    {
      "id": "6e8a0c2e-4f6b-4d8f-a0b1-3c4d5e6f7a81",
      "organizationId": "9b2f4e1a-6c3d-4e8f-a1b2-c3d4e5f6a7b8",
      "userId": "0f2a4c6e-8b9d-4f1a-a3b4-5c6d7e8f9a0b",
      "name": "Jordan Lee",
      "email": "jordan@northwind.example.com",
      "role": "Team Leader",
      "position": "Head of Client Success",
      "inviteStatus": "accepted",
      "isActive": true,
      "joinedAt": "2026-03-02T00:12:40.000Z",
      "createdAt": "2026-03-01T23:58:02.000Z"
    }
  ],
  "meta": { "total": 1, "limit": 50, "offset": 0 }
}
```

**Errors:** `400 INVALID_STATUS_QUERY` for an unknown `role` or `inviteStatus`; `422 VALIDATION_ERROR` for a bad `limit` or `offset`.

## Get a member

`GET /users/{id}`

Returns any member of the organization, including pending and deactivated ones.

**Scope:** `users:read`

| Path parameter | Notes |
|----------------|-------|
| `id` | Member ID. |

```bash
curl https://plan.empiraa.app/api/v1/users/6e8a0c2e-4f6b-4d8f-a0b1-3c4d5e6f7a81 \
  -H "X-API-Key: $GPS_API_KEY_ID" \
  -H "X-API-Secret: $GPS_API_SECRET"
```

```js
const res = await fetch('https://plan.empiraa.app/api/v1/users/6e8a0c2e-4f6b-4d8f-a0b1-3c4d5e6f7a81', { headers });
const { data: member } = await res.json();
```

**Response `200`**: `{ "data": <member> }`, with the same fields as the item in [List members](#list-members).

**Errors:** `404 NOT_FOUND` (`Member not found`) if the ID isn't a member of your organization.

## Invite or add a member

`POST /users`

Invites someone to the organization by email, or adds them straight away with `skipEmail: true`. The API can't make anyone an Owner or Super Admin.

The invitation is sent from the member who created the API key (or, if they've left, from the person who created the organization's account).

**Scope:** `users:write`

**Request body**

| Field | Type | Required | Allowed values | Notes |
|-------|------|----------|----------------|-------|
| `email` | string | Yes | A valid email address | |
| `role` | string | Yes | `Viewer`, `Team Member`, `Team Leader`, `Admin` | |
| `name` | string | No | 1–100 characters after trimming | |
| `skipEmail` | boolean | No | | Default `false`, which creates an invitation and emails it. `true` adds the person directly without an invitation email. |

```bash
curl -X POST https://plan.empiraa.app/api/v1/users \
  -H "X-API-Key: $GPS_API_KEY_ID" \
  -H "X-API-Secret: $GPS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "email": "sam@northwind.example.com", "role": "Team Member", "name": "Sam Patel" }'
```

```js
const res = await fetch('https://plan.empiraa.app/api/v1/users', {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'sam@northwind.example.com', role: 'Team Member', name: 'Sam Patel' }),
});
const { data: member, meta } = await res.json();
if (meta.delivery === 'invitation' && !meta.emailSent) {
  console.warn('Invitation created but the email failed to send');
}
```

**Response `201`**: the member, plus a `meta` object that says how they were added.

| `meta` field | Notes |
|--------------|-------|
| `delivery` | `invitation` (an invitation was created) or `direct_add` (`skipEmail: true`). |
| `invitationExpiresAt` | When the invitation expires. Only for `invitation`. |
| `emailSent` | Whether the invitation email went out. Only for `invitation`. |

```json
{
  "data": {
    "id": "7f9b1d3f-5a6c-4e8b-b1c2-4d5e6f7a8b92",
    "organizationId": "9b2f4e1a-6c3d-4e8f-a1b2-c3d4e5f6a7b8",
    "userId": "1a3c5e7a-9c0e-4a2b-b4c5-6d7e8f9a0b1c",
    "name": "Sam Patel",
    "email": "sam@northwind.example.com",
    "role": "Team Member",
    "position": null,
    "inviteStatus": "pending",
    "isActive": true,
    "joinedAt": "2026-11-12T03:22:30.000Z",
    "createdAt": "2026-11-12T03:22:30.000Z"
  },
  "meta": {
    "delivery": "invitation",
    "invitationExpiresAt": "2026-11-19T03:22:30.000Z",
    "emailSent": true
  }
}
```

**Errors**

| Status | `code` | When |
|--------|--------|------|
| 422 | `VALIDATION_ERROR` | Invalid `email`, a `role` that isn't allowed, or the invitation was refused (the `error` text says why, for example when the person is already a member). |
| 422 | `NO_INVITER` | No member could be found to send the invitation from. |

## Update a member

`PATCH /users/{id}`

Changes a member's role, job title, display name, or whether they're active. Send at least one field.

The person who created the organization's account can't be deactivated, and their role can't be changed, through the API.

**Scope:** `users:write`

| Path parameter | Notes |
|----------------|-------|
| `id` | Member ID. |

**Request body**

| Field | Type | Allowed values | Notes |
|-------|------|----------------|-------|
| `role` | string | `Viewer`, `Team Member`, `Team Leader`, `Admin` | |
| `position` | string or null | Up to 200 characters | Job title. `null` clears it. |
| `name` | string | 1–100 characters after trimming | |
| `isActive` | boolean | | `false` deactivates the member; `true` reactivates them. |

```bash
curl -X PATCH https://plan.empiraa.app/api/v1/users/6e8a0c2e-4f6b-4d8f-a0b1-3c4d5e6f7a81 \
  -H "X-API-Key: $GPS_API_KEY_ID" \
  -H "X-API-Secret: $GPS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "role": "Admin", "position": "Director of Client Success" }'
```

```js
const res = await fetch('https://plan.empiraa.app/api/v1/users/6e8a0c2e-4f6b-4d8f-a0b1-3c4d5e6f7a81', {
  method: 'PATCH',
  headers: { ...headers, 'Content-Type': 'application/json' },
  body: JSON.stringify({ role: 'Admin', position: 'Director of Client Success' }),
});
const { data: member } = await res.json();
```

**Response `200`**

```json
{
  "data": {
    "id": "6e8a0c2e-4f6b-4d8f-a0b1-3c4d5e6f7a81",
    "organizationId": "9b2f4e1a-6c3d-4e8f-a1b2-c3d4e5f6a7b8",
    "userId": "0f2a4c6e-8b9d-4f1a-a3b4-5c6d7e8f9a0b",
    "name": "Jordan Lee",
    "email": "jordan@northwind.example.com",
    "role": "Admin",
    "position": "Director of Client Success",
    "inviteStatus": "accepted",
    "isActive": true,
    "joinedAt": "2026-03-02T00:12:40.000Z",
    "createdAt": "2026-03-01T23:58:02.000Z"
  }
}
```

**Errors**

| Status | `code` | When |
|--------|--------|------|
| 404 | `NOT_FOUND` | The ID isn't a member of your organization. |
| 403 | `FORBIDDEN` | Deactivating, or changing the role of, the person who created the organization's account. |
| 422 | `VALIDATION_ERROR` | No fields sent, or a value that isn't allowed. |

## Remove a member

`DELETE /users/{id}`

Removes the member from the organization. GPS refuses while records still point to them, so reassign their Goals, Objectives, Actions and Ideas first, or deactivate them with [`PATCH`](#update-a-member) instead.

**Scope:** `users:write`

| Path parameter | Notes |
|----------------|-------|
| `id` | Member ID. |

```bash
curl -X DELETE https://plan.empiraa.app/api/v1/users/6e8a0c2e-4f6b-4d8f-a0b1-3c4d5e6f7a81 \
  -H "X-API-Key: $GPS_API_KEY_ID" \
  -H "X-API-Secret: $GPS_API_SECRET"
```

```js
const res = await fetch('https://plan.empiraa.app/api/v1/users/6e8a0c2e-4f6b-4d8f-a0b1-3c4d5e6f7a81', {
  method: 'DELETE',
  headers,
});
if (res.status === 409) {
  const { blockers } = await res.json();
  console.log(blockers); // what still points to this member
}
```

**Response `204`**: no body.

**Errors**

| Status | `code` | When |
|--------|--------|------|
| 404 | `NOT_FOUND` | The ID isn't a member of your organization. |
| 403 | `FORBIDDEN` | The member created the organization's account. |
| 409 | `MEMBER_IN_USE` | Records still point to the member. The body lists them in `blockers`. |

```json
{
  "error": "Cannot remove member: referenced by 2 assigned goals, 5 assigned actions",
  "code": "MEMBER_IN_USE",
  "docs": "https://empiraa.com/docs/api",
  "blockers": [
    { "code": "ASSIGNED_GOALS", "message": "Assigned to goals", "count": 2 },
    { "code": "ASSIGNED_ACTIONS", "message": "Assigned to actions", "count": 5 }
  ]
}
```

| Blocker `code` | Meaning |
|----------------|---------|
| `ASSIGNED_GOALS` | Assigned to Goals. |
| `ASSIGNED_OBJECTIVES` | Assigned to Objectives. |
| `ASSIGNED_ACTIONS` | Assigned to Actions. |
| `CIP_IDEA_ASSIGNEE` | Assigned to Ideas. |
| `ORG_CHART_NODE` | Placed on the organization chart. |
| `CIP_BOARD_MEMBER` | A member of an Ideas board. |
| `FK_VIOLATION` | Another record still refers to the member. |
