Members and Invitations
Manage project members, roles, and invitations
Projects are collaborative. Anybody holding members:write adds people by sending email invitations, and once accepted
the invitee becomes a member carrying the role
named at invitation time - or the
project’s default role, when none was named.
These routes address the project by path id, so the X-Mailyard-Project-Id header is not used here.
These take a session, not an API key
The routes on this page address a project by path id and read the caller’s membership, which an API key does not have - it is refused on all of them. Do this work in the console, or with a signed-in session.
Members
List members
GET /api/v1/projects/{id}/members
Response:
{
"members": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "b8493407-52ae-4d51-8396-9c8864977976",
"name": "Ada Lovelace",
"email": "ada@example.com",
"owner": true,
"role_id": "0f7c1c62-2b3e-4a55-9f2e-6d0e2c9b1a44",
"role_name": "Support",
"inherited_role": false,
"created_at": "2026-05-31T10:00:00Z"
}
]
}
inherited_role says the member carries the project’s default rather than a role of their own - the difference matters,
because only one of the two survives a change to the default.
Update a member’s role
PATCH /api/v1/projects/{id}/members/{user_id}
{id} is the project and {user_id} is the member’s user id (the user_id field returned by the list endpoint).
{
"role_id": "0f7c1c62-2b3e-4a55-9f2e-6d0e2c9b1a44"
}
role_id names one of the project’s own roles. Send "role_id": "" to clear it, which drops the member back to the
project’s default role - and to nothing at all if the project has named none. A role id that does not exist in this
project answers 404, the same answer a role belonging to another project gets.
The same endpoint grants and revokes ownership:
{
"owner": true
}
Ownership is not a role and is gated harder than one. members:write hands out roles, and roles are bounded by the
permission catalogue - ownership is the ability to delete the project, so only an existing owner may grant or revoke it
(403 otherwise). A project must keep at least one owner: demoting the last one answers 409.
Roles themselves are managed at GET/POST /api/v1/projects/{id}/roles and
PATCH/DELETE /api/v1/projects/{id}/roles/{roleId}, and the project’s default role at
PUT /api/v1/projects/{id}/default-role. The permission catalogue the checkboxes render from is
GET /api/v1/permissions.
Remove a member
DELETE /api/v1/projects/{id}/members/{user_id}
Removes the member and returns 204. Any member may remove themself. Removing somebody else needs members:delete. A
project’s last owner cannot be removed, by themself or by anybody (409) - a project with no owner could never be
deleted or handed on.
Invitations (project side)
Outgoing invitations live under /api/v1/projects/{id}/invitations and need members:write.
Invite a member
POST /api/v1/projects/{id}/invitations
{
"email": "newuser@example.com",
"role_id": "0f7c1c62-2b3e-4a55-9f2e-6d0e2c9b1a44"
}
Only email is required. Leaving role_id out offers the project’s default role, which is how most people are meant to
be invited. There is no way to invite somebody as an owner - ownership is granted afterwards, deliberately, by an owner.
If the email already belongs to a member, the request returns 409. The inviter’s email must be verified.
A pending invitation is created with a unique token, valid for 7 days, and an invitation email is sent to the
address with an accept link of the form /<app>/invitations?token=<token>.
Response (201):
{
"invitation": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "81af718e-f0ae-4780-a0d7-9f05b34dabcc",
"project": "",
"email": "newuser@example.com",
"role_id": "0f7c1c62-2b3e-4a55-9f2e-6d0e2c9b1a44",
"role_name": "Support",
"status": "pending",
"expires_at": "2026-06-07T10:00:00Z",
"created_at": "2026-05-31T10:00:00Z"
}
}
List pending invitations
GET /api/v1/projects/{id}/invitations
Returns the project’s pending invitations as an array of the object shown above.
Cancel an invitation
DELETE /api/v1/projects/{id}/invitations/{invId}
Deletes the pending invitation by its ID and returns 204.
Invitations (invitee side)
The recipient redeems an invitation with the token from the invitation mail. Both routes need a signed-in session and no project header - the invitee is not a member of anything yet.
POST /api/v1/invitations/{token}/accept
POST /api/v1/invitations/{token}/decline
Neither takes a body: the token in the path is the whole claim.
On accept the invitee joins the project carrying the invited role - or the project default, if the invitation named none
or named a role that has since been deleted - and the invitation is marked accepted. Somebody who is already a member
keeps the role they have: an invitation is an offer to join, not the moment to overwrite a role somebody set
deliberately. The invitation’s address must match the signed-in account, otherwise
403. A non-pending or expired invitation is 400. If the account is already a member the invitation is marked
accepted and the call answers 409.
There is no list-my-invitations endpoint
An invitation is redeemed from the link in its mail, or from the copyable link the project admin sees after creating it. Nothing enumerates the invitations addressed to you, and there is no accept-by-id route - the token is the only way in.