Members
Members give people access to organizations and projects. Invite by email, assign roles, restrict languages, and remove access.
Every endpoint takes a session cookie, except GET /v1/invitations/{token}, which is public. A Bearer key returns 401 AUTHENTICATION_REQUIRED; see Sessions. Organization endpoints need organization.manage_members (owners and managers); project endpoints need project.manage_members on the project. Common errors: 401 AUTHENTICATION_REQUIRED, 403 FORBIDDEN, 403 READ_ONLY_MODE on writes. The full list is under Errors.
| Role | Grants |
|---|---|
owner |
Full control, including billing and member management |
manager |
Manage projects, members, and settings |
editor |
Edit translations and keys |
translator |
Translate content in assigned languages |
viewer |
Read-only access |
Owner and manager carry every project permission. At the organization level a manager lacks organization.delete and organization.manage_billing, so deleting the organization and every billing route are owner-only. Ownership moves only through POST /v1/organizations/{orgId}/transfer-ownership.
Organization roles set a baseline; a project role can restrict but not elevate, and a project role above the organization role is rejected with 400 ROLE_HIERARCHY_VIOLATION. You cannot assign a role above your own, modify a member above you, or change your own role: 403 ROLE_HIERARCHY_VIOLATION or 403 FORBIDDEN.
GET /v1/organizations/{orgId}/members
Section titled “GET /v1/organizations/{orgId}/members”List organization members with their roles. An organization with no members returns an empty array.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
orgId |
path | integer |
Yes | Organization ID |
curl -b cookies.txt https://api.comvi.io/v1/organizations/1/members[{ "userId": 43, "email": "bob@example.com", "name": "Bob Johnson", "role": "translator", "joinedAt": "2025-02-15T14:30:00Z", "accessScope": "project", "projectCount": 2 }]| Field | Type | Meaning |
|---|---|---|
role |
string |
Organization role |
accessScope |
string |
organization for all projects, project for a chosen set |
projectCount |
integer |
Projects the member can access. Optional |
POST /v1/organizations/{orgId}/members/invite
Section titled “POST /v1/organizations/{orgId}/members/invite”Invite by email. An email with a Comvi account is added immediately and no invitation is created; otherwise an invitation is stored and emailed. Invitations expire after 7 days and cannot be resent once expired, see Recovering an expired invitation.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
orgId |
path | integer |
Yes | Organization ID |
email |
body | string |
Yes | Address to invite |
role |
body | string |
Yes | One of the five roles |
projectId |
body | integer | null |
No | Grant one project instead of the whole organization |
languageRestrictions |
body | string[] | null |
No | Locale codes, such as ["fr", "de"] |
curl -X POST -b cookies.txt -H "Content-Type: application/json" \ -d '{"email": "translator@example.com", "role": "translator", "projectId": 5, "languageRestrictions": ["fr", "de"]}' \ https://api.comvi.io/v1/organizations/1/members/inviteReturns 200 OK in both cases with the same shape. The invitation token is never returned: it reaches the invited address only in the invitation email. To tell the two cases apart, list the pending invitations; a new invitation appears there.
{ "id": 12, "email": "translator@example.com", "role": "translator", "projectId": 5, "languageRestrictions": ["fr", "de"], "expiresAt": "2025-04-01T08:00:00Z", "createdAt": "2025-03-25T08:00:00Z" }| Field | New user | Existing user |
|---|---|---|
id |
Invitation ID | User ID |
expiresAt |
When the invitation expires | The current time. Ignore it |
GET /v1/organizations/{orgId}/members/{userId}/access
Section titled “GET /v1/organizations/{orgId}/members/{userId}/access”A member’s full access picture, for populating an edit form before PUT …/access.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
orgId |
path | integer |
Yes | Organization ID |
userId |
path | integer |
Yes | Member’s user ID |
curl -b cookies.txt https://api.comvi.io/v1/organizations/1/members/43/access{ "userId": 43, "accessScope": "project", "role": "translator", "projectIds": [5, 8], "languageRestrictions": ["fr", "de"], "projectAccessEntries": [], "hasProjectOverrides": false, "hasMixedProjectRoles": false, "hasMixedLanguageRestrictions": false, "requiresProjectLevelManagement": false }| Field | Type | Meaning |
|---|---|---|
projectIds |
integer[] |
Assigned projects |
languageRestrictions |
string[] | null |
Locale codes, or null for all |
projectAccessEntries |
object[] |
Per-project role and restrictions |
hasProjectOverrides |
boolean |
Some project gives a role other than the organization role |
hasMixedProjectRoles |
boolean |
Per-project roles differ |
hasMixedLanguageRestrictions |
boolean |
Per-project restrictions differ |
requiresProjectLevelManagement |
boolean |
Too varied to edit here; PUT …/access refuses with 409. Edit each project instead |
PUT /v1/organizations/{orgId}/members/{userId}/access
Section titled “PUT /v1/organizations/{orgId}/members/{userId}/access”Set scope, role, project assignments and language restrictions in one call.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
orgId |
path | integer |
Yes | Organization ID |
userId |
path | integer |
Yes | Member’s user ID |
accessScope |
body | string |
Yes | organization or project |
role |
body | string |
Yes | Role to apply |
projectIds |
body | integer[] |
When project |
Duplicates fail schema validation with 422; an empty array returns 400 |
languageRestrictions |
body | string[] | null |
No | Locale codes, or null for all |
curl -X PUT -b cookies.txt -H "Content-Type: application/json" \ -d '{"accessScope": "project", "role": "translator", "projectIds": [5, 8], "languageRestrictions": ["fr", "de"]}' \ https://api.comvi.io/v1/organizations/1/members/43/accessReturns 204 No Content.
PATCH /v1/organizations/{orgId}/members/{userId}
Section titled “PATCH /v1/organizations/{orgId}/members/{userId}”Change a member’s organization role, leaving project assignments alone. Demoting the last owner returns 400 CANNOT_REMOVE_LAST_OWNER; demoting an owner while a paid subscription is linked returns 400 ACTIVE_SUBSCRIPTION_REQUIRED.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
orgId |
path | integer |
Yes | Organization ID |
userId |
path | integer |
Yes | Member’s user ID |
role |
body | string |
Yes | New role |
curl -X PATCH -b cookies.txt -H "Content-Type: application/json" -d '{"role": "editor"}' \ https://api.comvi.io/v1/organizations/1/members/43Returns 204 No Content.
POST /v1/organizations/{orgId}/transfer-ownership
Section titled “POST /v1/organizations/{orgId}/transfer-ownership”Hand ownership to another member; the current owner becomes a manager. Only the current owner may call it, although the route checks organization.manage_members, and the new owner must already have organization-wide access.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
orgId |
path | integer |
Yes | Organization ID |
newOwnerId |
body | integer |
Yes | User ID of the new owner |
curl -X POST -b cookies.txt -H "Content-Type: application/json" -d '{"newOwnerId": 43}' \ https://api.comvi.io/v1/organizations/1/transfer-ownershipReturns 204 No Content.
| Status and code | When |
|---|---|
400 INVALID_INPUT |
newOwnerId is the caller |
403 FORBIDDEN |
The caller is not the current owner |
409 CONFLICT |
The target has project-scoped access, or is already an owner |
DELETE /v1/organizations/{orgId}/members/{userId}
Section titled “DELETE /v1/organizations/{orgId}/members/{userId}”Remove a user from the organization and every project in it. You cannot remove yourself or the last owner.
curl -X DELETE -b cookies.txt https://api.comvi.io/v1/organizations/1/members/43Returns 204 No Content.
GET /v1/organizations/{orgId}/invitations
Section titled “GET /v1/organizations/{orgId}/invitations”List invitations not yet accepted. Expired ones are included, so check expiresAt.
curl -b cookies.txt https://api.comvi.io/v1/organizations/1/invitations[{ "id": 12, "email": "translator@example.com", "role": "translator", "invitedByUserId": 42, "invitedByName": "Jane Smith", "projectId": 5, "projectName": "my-web-app", "languageRestrictions": ["fr", "de"], "expiresAt": "2025-04-01T08:00:00Z", "createdAt": "2025-03-25T08:00:00Z" }]GET /v1/invitations/{token}
Section titled “GET /v1/invitations/{token}”What an invitation is for, so the recipient can see who invited them and to what before signing up. Public: the 64-character token is the only credential. An expired token returns 400 INVITATION_EXPIRED and deletes the invitation, so anyone opening an expired link removes it; afterwards it is absent from the list and cancel and resend return 404 INVITATION_NOT_FOUND. To recover, invite again: the duplicate check finds no row, so the re-invite succeeds with a fresh token and a new 7-day window.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
token |
path | string |
Yes | Token from the email link, 64 characters |
curl https://api.comvi.io/v1/invitations/a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90{ "email": "translator@example.com", "organizationName": "Acme Corp", "role": "translator", "inviterName": "Jane Smith", "projectName": "my-web-app", "languageRestrictions": ["fr", "de"], "expiresAt": "2025-04-01T08:00:00Z" }| Status and code | When |
|---|---|
400 INVITATION_EXPIRED |
Expired. The invitation is deleted |
404 INVITATION_NOT_FOUND |
Unknown or already used token |
POST /v1/invitations/{token}/accept
Section titled “POST /v1/invitations/{token}/accept”Join the organization the invitation names. The caller must be signed in as the invited email address. No body.
curl -X POST -b cookies.txt https://api.comvi.io/v1/invitations/a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90/accept{ "organizationId": 1, "organizationName": "Acme Corp", "role": "translator" }| Status and code | When |
|---|---|
400 INVITATION_EXPIRED |
Expired. The invitation is deleted, so the token cannot be tried again |
400 USER_ALREADY_IN_ORGANIZATION |
Already a member. The invitation is deleted |
403 INVALID_INVITATION |
Signed in with a different email than the invitation was sent to |
POST /v1/organizations/{orgId}/invitations/{invitationId}/resend
Section titled “POST /v1/organizations/{orgId}/invitations/{invitationId}/resend”Send the invitation email again with the same token; the expiry is not extended. An expired invitation returns 400 INVITATION_EXPIRED and nothing is sent.
curl -X POST -b cookies.txt https://api.comvi.io/v1/organizations/1/invitations/12/resendReturns 204 No Content.
DELETE /v1/organizations/{orgId}/invitations/{invitationId}
Section titled “DELETE /v1/organizations/{orgId}/invitations/{invitationId}”Delete a pending invitation; its token stops working. An expired invitation can be cancelled as long as its row still exists.
curl -X DELETE -b cookies.txt https://api.comvi.io/v1/organizations/1/invitations/12Returns 204 No Content, or 404 INVITATION_NOT_FOUND when the row is already gone.
Recovering an expired invitation
Section titled “Recovering an expired invitation”Start with GET /v1/organizations/{orgId}/invitations. If the invitation is listed, cancel it, then invite again; skipping the cancel returns 400 INVITATION_ALREADY_SENT, because the duplicate check matches on email, organization and project without looking at expiresAt. If it is not listed, someone opened its link and it was deleted; invite again directly. Either way the new invitation carries a fresh token and a new 7-day window, and the old token stops working.
GET /v1/projects/{projectId}/members
Section titled “GET /v1/projects/{projectId}/members”Everyone explicitly assigned to the project, with organization and project roles.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
curl -b cookies.txt https://api.comvi.io/v1/projects/5/members[{ "userId": 44, "email": "maria@example.com", "name": "Maria Garcia", "organizationRole": "translator", "projectRole": "translator", "languageRestrictions": ["es", "pt"], "addedAt": "2025-03-01T12:00:00Z" }]languageRestrictions is null for all languages.
POST /v1/projects/{projectId}/members
Section titled “POST /v1/projects/{projectId}/members”Add an existing organization member to a project. A role above the user’s organization role returns 400 ROLE_HIERARCHY_VIOLATION.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
userId |
body | integer |
Yes | Organization member to add |
role |
body | string |
Yes | Project role |
languageRestrictions |
body | string[] |
No | Locale codes |
curl -X POST -b cookies.txt -H "Content-Type: application/json" \ -d '{"userId": 44, "role": "translator", "languageRestrictions": ["es", "pt"]}' \ https://api.comvi.io/v1/projects/5/membersReturns 201 Created with an empty body.
PATCH /v1/projects/{projectId}/members/{userId}
Section titled “PATCH /v1/projects/{projectId}/members/{userId}”Update a project member’s role or language restrictions.
| Parameter | In | Type | Required | Meaning |
|---|---|---|---|---|
projectId |
path | integer |
Yes | Project ID |
userId |
path | integer |
Yes | Member’s user ID |
role |
body | string |
Yes | New project role |
languageRestrictions |
body | string[] | null |
No | Locale codes, or null for all |
curl -X PATCH -b cookies.txt -H "Content-Type: application/json" \ -d '{"role": "editor", "languageRestrictions": null}' https://api.comvi.io/v1/projects/5/members/44Returns 204 No Content.
DELETE /v1/projects/{projectId}/members/{userId}
Section titled “DELETE /v1/projects/{projectId}/members/{userId}”Remove a user from a project, not from the organization. You cannot remove yourself, and the user must be explicitly assigned: removing someone with organization-wide access only returns 404 NO_PROJECT_ACCESS.
curl -X DELETE -b cookies.txt https://api.comvi.io/v1/projects/5/members/44Returns 204 No Content.
Errors
Section titled “Errors”The only 409 is CONFLICT; the “already” conditions are 400.
| Status and code | When |
|---|---|
400 CANNOT_REMOVE_LAST_OWNER |
Removing or demoting the last owner |
400 ROLE_HIERARCHY_VIOLATION |
A project role would exceed the organization role |
400 USER_ALREADY_IN_ORGANIZATION |
Already a member, or already has access to the project |
400 INVITATION_ALREADY_SENT |
An invitation is pending for this email |
400 INVITATION_EXPIRED |
The invitation has expired |
400 ACTIVE_SUBSCRIPTION_REQUIRED |
Removing an owner while a paid subscription is linked |
400 INVALID_INPUT |
Empty projectIds on a project-scoped update, or transferring ownership to yourself |
401 AUTHENTICATION_REQUIRED |
Missing session cookie, or a Bearer key |
403 FORBIDDEN |
Missing organization.manage_members or project.manage_members, or modifying your own access |
403 ROLE_HIERARCHY_VIOLATION |
Assigning or modifying a role above your own |
403 INVALID_INVITATION |
The invitation was sent to a different email |
403 LIMIT_EXCEEDED |
Member limit reached for the plan |
403 READ_ONLY_MODE |
The organization is read-only |
404 USER_NOT_FOUND |
The user does not exist. Project member endpoints return the same for a user outside the project’s organization |
404 NOT_ORGANIZATION_MEMBER |
The target user is not in the organization. 403 when the caller is not |
404 NO_PROJECT_ACCESS |
The target user is not explicitly assigned to the project |
404 ORGANIZATION_NOT_FOUND |
The organization does not exist |
404 PROJECT_NOT_FOUND |
The project does not exist |
404 INVITATION_NOT_FOUND |
The invitation does not exist or was used |
409 CONFLICT |
Ownership transfer to an ineligible member, or an access update blocked by project-level overrides |
422 VALIDATION_ERROR |
Duplicate entries in projectIds. meta.field names the field |
422 FIELD_REQUIRED |
A required body field is missing, such as email, role, accessScope or newOwnerId |
500 CACHE_INVALIDATION_FAILED |
The permission cache could not be flushed first. Retry |
Next steps
Section titled “Next steps”- Roles and permissions for the full permission model
- Organizations to create organizations
- Projects for the projects members are assigned to