Members
List your organization's members, invite or add people, change roles, and remove 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 |
users:read |
GET |
/users/{id} |
users:read |
POST |
/users |
users:write |
PATCH |
/users/{id} |
users:write |
DELETE |
/users/{id} |
users:write |
Member changes don't fire webhooks.
The JavaScript examples assume a headers object holding your key:
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. |
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"
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
{
"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. |
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"
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.
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. |
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" }'
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. |
{
"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. |
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" }'
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
{
"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 instead.
Scope: users:write
| Path parameter | Notes |
|---|---|
id |
Member ID. |
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"
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. |
{
"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. |

