Members
Provisioning a client's workspace does not stop at data: their team must be able to get in. The Members API covers this step: you invite each team member by email address, track the acceptance of their invitation, adjust their role, and remove access, without going through the Scribee interface. Each member carries a role (member, manager, or owner) and a status (invited, pending_activation, or activated), readable on every response.
What the API does
- Recognizes the email address: an unknown address triggers account creation and sends an invitation; an already-known address attaches the existing account and sends it the email matching its status. A single call covers both cases.
- Sends an invitation link valid for 7 days. Each resend generates a new link and invalidates the previous one.
- Refuses to demote the workspace's last
owner, and refuses any removal of anownermember. - Returns each member's status on every response, with no extra call.
The scopes required
Reads on this page require read: a token whose scopes omit it receives 403 on the list as well as on the detail. Writes - adding, changing a role, resending an invitation - require write, and removal accepts destroy or write. A token carrying only read receives 403 on those calls.
So request read write for anything that is not a plain read: it is the common combination, and read is verified on every read (Authentication).
Roles
| Role | What it allows |
|---|---|
member | Uses the workspace day to day, with no team management. |
manager | Everything member allows, plus team management: inviting, changing roles, removing members. |
owner | Everything manager allows, plus managing other owners. Every workspace keeps at least one. |
Through the API, all three roles can be assigned when a member is created. After that, changing a role only accepts member and manager: assigning owner to an existing member responds 403; that promotion is done from the Scribee interface. Demoting an existing owner remains possible through the API as long as they are not the workspace's last one (step 4).
A member created through the API always has access to the entire workspace: no per-company restriction parameter is exposed. That restriction is configured from the Scribee interface, and the API lists these members like any other, with the same fields.
One consequence to know before any removal: deleting a member restricted to specific companies (step 5) also destroys their per-company access, permanently. Re-inviting them through the API gives them the entire workspace back. After a round trip of this kind, reconfigure the restriction from the Scribee interface.
The status of a member
| Status | What it means exactly |
|---|---|
invited | An invitation token is active and has not yet been accepted. |
pending_activation | The invitation is accepted; account creation is not finished. |
activated | Neither invited nor pending_activation. It is the default value. |
activated is computed by elimination, not from an activity flag. Deactivating an account therefore never changes the status returned: the member keeps the one the elimination computes - an already-activated account stays activated, an account still invited stays invited - and the API exposes no deactivation field. If a member cannot sign in, deactivation is a possible cause; check it from the Scribee interface.
An already-active account added to a new workspace appears directly as activated.
Step 1: list members
This call is a read: run it as is, it modifies nothing and sends nothing externally. The read scope is enough.
curl https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/members \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
200 response:
{
"data": [
{
"id": 12,
"role": "owner",
"joined_at": "2025-11-04T09:12:00+01:00",
"status": "activated",
"user": {
"id": 7,
"email": "direction@client.example",
"first_name": "Marie",
"last_name": "Leroy",
"full_name": "Marie Leroy"
}
},
{
"id": 87,
"role": "member",
"joined_at": "2026-07-31T11:15:00+02:00",
"status": "invited",
"user": {
"id": 231,
"email": "camille.dupont@client.example",
"first_name": "Camille",
"last_name": "Dupont",
"full_name": "Camille Dupont"
}
}
],
"meta": { "current_page": 1, "per_page": 20, "total_pages": 1, "total_count": 2 }
}
Every member always carries these five fields - id, role, joined_at, status, user - on every response on this page. There is no field filtering and no include parameter on this resource.
The default sort is sort_by=role&sort_order=desc. role is sorted as a string, not as a privilege level: desc yields the order owner, member, manager; asc yields manager, member, owner. Within the same role, members are ordered by ascending join date. sort_by accepts role, created_at, and updated_at; sort_order accepts asc and desc. An unknown value for either causes no error: the sort falls back to the default. Pagination follows the common conventions (API conventions).
A single member is read at GET /api/v1/members/{id} - note the path with no workspace_id. That route looks the identifier up across all workspaces linked to your application, and the response carries no workspace identifier. If you manage several, keep the mapping between a member's id and their workspace yourself (Your first call, API reference).
Step 2: add a member
This call creates the member's access and sends a real email to the address provided - there is no sandbox. An unknown address receives an invitation to create its Scribee account; an already-known address triggers no account creation, and the email it receives depends on its status. An account still invited, which therefore never accepted its invitation, receives the invitation email and its activation link: its previous link stays valid as long as it has not expired, otherwise a new 7-day link is issued. An account pending_activation or activated receives the notification of its new access. To rehearse, invite an address you control, then remove the member (step 5): removal deletes the access, not the email already sent. The read write scope is required.
If the invited address belongs to a domain covered by an active SAML configuration of the workspace, the member is added directly and no email is sent: they sign in with their usual corporate identity. This path depends on the invited address's domain, not on the workspace as a whole.
curl -X POST https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/members \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"member": {
"email": "camille.dupont@client.example",
"first_name": "Camille",
"last_name": "Dupont",
"role": "member"
}
}'
201 response:
{
"data": {
"id": 87,
"role": "member",
"joined_at": "2026-07-31T11:15:00+02:00",
"status": "invited",
"user": {
"id": 231,
"email": "camille.dupont@client.example",
"first_name": "Camille",
"last_name": "Dupont",
"full_name": "Camille Dupont"
}
}
}
The member object is required: a request without it responds 400, with the usual JSON envelope whose error key is bad_request. email is required. role is optional and defaults to member; it accepts member, manager, and owner at creation.
first_name and last_name are optional and are only taken into account for an unknown address. On an address Scribee already knows, the existing account's identity wins: the values you send are ignored and the 201 response returns the stored names. No API endpoint changes a user's name or email address.
A deactivated account is refused
Inviting an address whose Scribee account has been deactivated responds 422, on the SAML path as well as the ordinary one: no access is created and no email is sent (see Errors). Deactivation blocking authentication, the access would have been unusable anyway.
Have the account reactivated from the Scribee interface, then replay the call. This check only covers the add: an account deactivated afterwards stays a member, and the deactivation does not change the status the API returns for it (see The status of a member).
Step 3: resend an invitation
This call sends a new real invitation email to the member. It applies to the invited and pending_activation statuses; an activated member responds 422 (see Errors). A new link valid for 7 days is generated and the previous link stops working. The read write scope is required.
On a pending_activation member, the resend resets the invitation: the acceptance is cleared and the response returns the invited status. The member goes through the whole flow again from the new link - choosing a password, then activating the account, which requires two-factor authentication - and a password they had already chosen no longer gives them access to Scribee.
curl -X POST https://app.scribee.tech/api/v1/members/87/resend_invitation \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
200 response, abridged to the three fields useful here; the real body carries the five fields from step 1:
{
"data": {
"id": 87,
"role": "member",
"status": "invited"
}
}
Step 4: change a member's role
This call changes the member's role, with immediate effect on their rights in the workspace. It sends no email. The read write scope is required.
PATCH is not a partial update: member.role is required. Omitting it does not leave the role unchanged, it responds 422.
curl -X PATCH https://app.scribee.tech/api/v1/members/87 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "member": { "role": "manager" } }'
200 response:
{
"data": {
"id": 87,
"role": "manager",
"joined_at": "2026-07-31T11:15:00+02:00",
"status": "invited",
"user": {
"id": 231,
"email": "camille.dupont@client.example",
"first_name": "Camille",
"last_name": "Dupont",
"full_name": "Camille Dupont"
}
}
}
role accepts member and manager. Assigning owner to an existing member responds 403: the owner role is assigned at creation (step 2) or from the Scribee interface. Demoting an owner works as long as the workspace keeps another one; demoting the last one responds 422 (see Errors). Every validation error on this call arrives under details.base, never under details.role. Reference: update a member's role.
Step 5: remove a member
This call immediately removes the member's access to the workspace. The user account is not deleted: re-inviting the same address (step 2) recreates access with the existing account. The write or destroy scope is required (see The scopes required).
An owner member cannot be removed through the API: the response is 403, whatever the number of owners in the workspace. If the member was restricted to specific companies, removal also destroys that per-company access (see Roles).
curl -X DELETE https://app.scribee.tech/api/v1/members/87 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
204 response, with no body. Reference: remove a member.
What happens next
- The invited member receives the email, accepts the invitation (
pending_activation), then finishes creating their account (activated). Track progress with theGETfrom step 1. - Member management only affects access to the workspace: nothing is sent to the PPF (Portail Public de Facturation) or the Peppol network, and invoicing data is not changed.
- After 7 days without acceptance, the invitation link expires; the member stays
invitedand step 3 generates a new link.
Errors and edge cases
422: details.email carries every add error
On POST, details.email gathers every message of the refusal, whatever field actually failed. An address that is already a member of the workspace:
{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"email": ["L'utilisateur est déjà membre de cet espace de travail"]
}
}
When the refusal comes from a field validation, that field appears as well under its own key. An invalid role:
{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"email": ["Role n'est pas inclus(e) dans la liste"],
"role": ["n'est pas inclus(e) dans la liste"]
}
}
Refusals that target no field - an address already a member, a deactivated account - appear under email alone. So treat details.email as the complete list of messages, and the other keys, when present, as the field at fault: on this endpoint the absence of a field key does not mean that field is valid. This is a partial exception to the rule described in API conventions, which maps each failing field to its errors.
On an address that is already a member, look up the existing member with the GET from step 1; if they have not accepted their invitation, resend it (step 3).
422: the invitee's account is deactivated
Adding an address whose Scribee account has been deactivated (see step 2):
{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"email": ["Cet utilisateur est désactivé. Réactivez le compte avant de lui accorder l'accès."]
}
}
Nothing is created and no email goes out. Have the account reactivated from the Scribee interface, then replay the same call.
422: the account is already activated
Resending an invitation to an activated member:
{
"error": "unprocessable_entity",
"code": "operation_failed",
"message": "L'utilisateur a déjà activé son compte"
}
This response carries no details, and this single message covers every refusal from step 3. There is nothing to resend: the member has finished setting up their account.
422: role missing or invalid on the role change
On PATCH, errors are grouped under details.base. An invalid role:
{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["Role n'est pas inclus(e) dans la liste"]
}
}
Omitting member.role produces the same shape, with an extra message reporting the empty field.
422: last owner
Demoting the workspace's only owner:
{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["Role Impossible de changer le rôle : le tenant doit avoir au moins un propriétaire"]
}
}
In this message, tenant refers to the workspace. First designate another owner from the Scribee interface, then replay the call. Base your handling on the HTTP status and the error and code keys, never on the message text (API conventions).
403: the owner role is protected
Assigning owner to an existing member (step 4) or removing an owner member (step 5) responds 403, even with the right scopes. These operations are done from the Scribee interface.
These two 403 responses carry the usual refusal envelope, with the error key set to forbidden, but their message is specific to each refusal: the step 4 one explains that the owner role cannot be assigned to an existing member and notes that demoting an owner remains available, the step 5 one explains that an owner member cannot be removed through the API. The two texts differ from each other and from the generic message of the other 403 responses on this page; as elsewhere, base your handling on the HTTP status and the error key, never on the message text.
403: insufficient scope
A write (adding, changing a role, resending an invitation) with a token without write, or a removal with a token without destroy or write (see The scopes required):
{
"error": "forbidden",
"message": "Vous n'êtes pas autorisé à effectuer cette action"
}
Request a new token with the scopes you need (Authentication).
403: workspace or IP address refused
On the two nested routes on this page - GET and POST /api/v1/workspaces/{workspace_id}/members - the workspace is resolved before the action, and two refusals return 403. The workspace is not linked to your application:
{
"error": "forbidden",
"message": "L'application n'a pas accès à cet espace de travail"
}
The workspace has an IP allowlist that does not cover your source address:
{
"error": "forbidden",
"message": "Cette adresse IP n'est pas autorisée pour cet espace de travail"
}
A workspace_id matching no workspace receives the first refusal above, word for word: nothing tells a workspace that does not exist apart from a workspace your application is not allowed to reach, so the response never tells you whether the workspace exists. Workspace and IP access are configured by Scribee: contact your Scribee representative.
404 Not Found
On the four flat routes - GET, PATCH, DELETE /api/v1/members/{id} and POST /api/v1/members/{id}/resend_invitation - the member is looked up only in the workspaces linked to your application whose IP allowlist permits your source address. Anything outside that perimeter is indistinguishably not found:
{
"error": "not_found",
"message": "La ressource demandée est introuvable"
}
An IP address outside the allowlist therefore produces 404 here, where the nested routes produce 403. If a known identifier suddenly responds 404, check your source IP address before the identifier (Your first call).
Related pages
- Your first call - identify your workspaces and their identifiers
- Categories - the workspace's other configuration resource
- API reference: list members
- API reference: add a member
- API reference: resend an invitation