Sari la conținut
Pagini APITeam, roles, and invitations

Documentație API

Această documentație este disponibilă doar în limba engleză.

Team, roles, and invitations

Who works at the organization, what each person may see, and how new people are brought in.

There are two vocabularies here and they do different jobs.

  • A role is organization-wide: Member, Admin, or Owner. It decides what somebody may do to the organization itself, its billing, and its people.
  • A grant is per client company: a list of permissions on one profile. It decides what somebody may do to that client's documents, sources, reminders, and settings.

A person needs both. An Admin with no grants can manage the team and see nothing about any client's invoices. See Authentication for the full ladder and Profiles for what a client company is.

Routes

Method Path Needs
GET /v1/members Admin
GET /v1/members/{memberId}/grants Admin
PUT /v1/members/{memberId}/grants Admin
PUT /v1/members/{memberId}/role Owner
DELETE /v1/members/{memberId} Admin
POST /v1/members/{memberId}/transfer-ownership Owner
GET /v1/invitations Admin
POST /v1/invitations Admin
DELETE /v1/invitations/{invitationId} Admin
POST /v1/invitations/{invitationId}/resend Admin
GET /v1/me/invitations any
POST /v1/me/invitations/{invitationId}/accept any
POST /v1/me/invitations/{invitationId}/decline any

The first ten are organization-scoped and need X-Organization-Id. The three under /v1/me are not, because an invitation is addressed to a person before they belong to the organization that sent it. See Authentication.

The role ladder

Value Name What it carries
0 Member Only what their grants say. No team routes, no billing, no organization settings
1 Admin Everything a Member has, plus the team, invitations, sources, and reading billing
2 Owner Everything, plus changing the subscription and changing roles

Roles serialize as integers. See Conventions.

There is exactly one Owner. It is the person who created the organization or the person it was handed to, and the seat does not split. That is why a role change and a transfer are separate routes.

One rule runs through every write on this page: you may only act on a role below your own. An Admin may invite and remove Members and other Admins but may not touch the Owner, may not hand out the Owner role, and may not change their own grants. Breaking it answers 403 with member_role_not_below_caller, member_role_not_allowed, member_owner_protected, member_self_grant_change, or invitation_role_not_allowed, depending on which write asked. See Error codes.

GET /v1/members

Everybody in the organization. Admin. Answers a plain array, not a page.

Field Type Means
id uuid The membership. This is the memberId in the routes below
userId uuid The person behind it
email string Their sign-in address
displayName string or null Their name, when they have set one
avatarUrl string or null Full-size picture
avatarThumbnailUrl string or null Small picture for a roster row
role OrganizationRole 0 Member, 1 Admin, 2 Owner
grantedProfileCount integer How many clients they have a grant on
hasOrganizationWideAccess boolean True when their role reaches every client without grants
createdAt date-time When they joined

memberId is the membership id, not the user id. The same person can belong to two organizations and has a different memberId in each. Address these routes with id, never with userId.

hasOrganizationWideAccess is the field to read before you read grantedProfileCount. When it is true, a grantedProfileCount of 0 does not mean the person sees nothing; it means they do not need grants. Showing "0 clients" next to an Owner is the classic bug here.

[
  {
    "id": "33333333-3333-4333-8333-333333333333",
    "userId": "44444444-4444-4444-8444-444444444444",
    "email": "ana@example.com",
    "displayName": "Ana Example",
    "avatarUrl": null,
    "avatarThumbnailUrl": null,
    "role": 1,
    "grantedProfileCount": 12,
    "hasOrganizationWideAccess": false,
    "createdAt": "2026-09-01T08:00:00Z"
  }
]

GET /v1/members/{memberId}/grants

One person's access grid. Admin.

Field Type Means
memberId uuid The membership
userId uuid The person
role OrganizationRole Their role
hasOrganizationWideAccess boolean True when the role already reaches every client
grants array One entry per client they hold permissions on

Each entry in grants:

Field Type Means
profileId uuid The client company
permissions array ProfilePermission values, as integers
Value Name Lets them
0 View See the client, its documents, and its invoices
1 ManageDocuments Upload, confirm, correct, and trash documents
2 ManageSources Connect and disconnect email, e-Factura, and connectors
3 SendReminders Send and stop chasing
4 ManageSettings Change the client's own settings

The permissions are a set, not a ladder. ManageDocuments does not imply View, so a grant that is meant to let somebody work on a client usually lists View alongside whatever else it lists. Grant what the job needs and read the list literally.

When hasOrganizationWideAccess is true, grants is usually empty and it does not limit anything.

PUT /v1/members/{memberId}/grants

Replaces the whole grid. Admin.

{
  "grants": [
    { "profileId": "22222222-2222-4222-8222-222222222222", "permissions": [0, 1, 3] },
    { "profileId": "55555555-5555-4555-8555-555555555555", "permissions": [0] }
  ]
}

Answers 200 with the same shape GET returns.

This is not a patch. Every client missing from the body loses its grant, and sending {"grants": []} removes all of them. To add one client, read the grid, append, and send the whole thing back. A client-side screen that sends only the row somebody just edited will silently revoke the rest.

Refusals:

Code Status Means
member_self_grant_change 403 An Admin editing their own grants
member_role_not_below_caller 403 Editing somebody at your level or above

An Admin may not widen their own access, which is why the first one exists. Somebody else with the authority has to do it.

PUT /v1/members/{memberId}/role

Changes a role. Owner only.

{ "role": 1 }

Answers 200 with the member. Member and Admin are the only values that go through here. Promoting somebody to Owner answers 403 member_role_not_allowed, because there is one Owner and handing the seat over is a different route that also moves the caller out of it.

Demoting the Owner answers 403 member_owner_protected. An organization is never left without one.

Grants are untouched by a role change. Promoting a Member to Admin does not widen what they see of any client; it widens what they may do to the organization.

DELETE /v1/members/{memberId}

Removes somebody from the organization. Admin. Answers 204.

Their grants go with them, and they lose the organization from their switcher immediately. Nothing they collected, confirmed, or sent is deleted or reattributed. The audit trail keeps their name against what they did.

Removing the Owner answers 403 member_owner_protected. Transfer the seat first, then remove the person.

Removing somebody at or above your own role answers 403 member_role_not_below_caller.

POST /v1/members/{memberId}/transfer-ownership

Hands the Owner seat to another member. Owner. No body.

Answers 200 with the member who is now the Owner.

The caller becomes an Admin in the same transaction. There is no moment with two Owners and no moment with none. Expect the caller's own role to have changed when the call returns, and refresh whatever you cached about it. Anything the caller could do as Owner, changing the subscription above all, they cannot do on the next request.

This is the only way the Owner seat moves. It is deliberately a separate route from a role change, because it is the one write in this area that takes something away from the person making it.

Invitations

Somebody who does not have a Syllevo account yet cannot be added; they have to be invited. An invitation is an email plus the role and grants they will get when they accept.

An invitation lives 14 days. Past that, accepting it answers 409 invitation_expired and the only fix is a resend, which puts the clock back to a full 14 days.

Value Name Means
0 Pending Sent, not yet answered, may still be live
1 Accepted They joined
2 Declined They said no
3 Revoked The firm took it back

status and expired answer different questions. A row can be Pending and expired: true at the same time: nobody answered it and the 14 days ran out. Read expired before you show "waiting for a reply".

GET /v1/invitations

Every invitation this organization has sent, in any state. Admin. A plain array.

Field Type Means
id uuid The invitation
email string Who it went to
role OrganizationRole What they will be
status InvitationStatus 0 pending, 1 accepted, 2 declined, 3 revoked
expired boolean Whether the 14 days have run out
expiresAt date-time When they do
lastSentAt date-time, null When the email last went out
emailSent boolean Whether the last send reached the mail relay
invitedByName string or null Who invited them
grantedProfileCount integer How many clients the invitation carries
hasOrganizationWideAccess boolean Whether the role reaches every client anyway
createdAt date-time When it was created

emailSent: false with a 201 is not a contradiction. The invitation exists and the link works; the mail relay is what did not take it. Show a resend button rather than an error, because the row is good.

POST /v1/invitations

Creates and sends one. Admin. Answers 201.

Field Type Required Means
email string yes Where to send it
role OrganizationRole yes 0 Member or 1 Admin
grants array no The same {profileId, permissions} shape as grants
{
  "email": "mihai@example.com",
  "role": 0,
  "grants": [{ "profileId": "22222222-2222-4222-8222-222222222222", "permissions": [0, 1] }]
}

The grants ride with the invitation and are applied the moment it is accepted, so a new person lands with the right clients already visible instead of waiting for a second setup step.

Refusals:

Code Status Means
invitation_role_not_allowed 403 Inviting somebody at or above your own role, Owner included
invitation_already_member 409 That address already belongs to this organization

DELETE /v1/invitations/{invitationId}

Revokes a pending invitation. Admin. Answers 204. The link stops working.

The row is not deleted, it moves to status 3. Who invited whom is part of the record.

Revoking anything that is not Pending, already accepted, declined, or revoked, answers 409 invitation_not_pending. Removing somebody who already joined is DELETE /v1/members/{memberId}, a different act.

POST /v1/invitations/{invitationId}/resend

Sends the email again. Admin. Answers 200 with the invitation.

A resend renews the clock. expiresAt moves to 14 days from now, which is what makes it the fix for an expired invitation rather than a second copy of a dead one.

Refused with 409 invitation_not_pending when it is not pending.

Invitations you hold

/v1/me/invitations is the other side: what other organizations have sent to the signed-in person. These three routes do not take X-Organization-Id. Whoever is being invited does not belong to the inviting organization yet, so there is no organization to scope to.

GET /v1/me/invitations

Every pending invitation addressed to the caller, across every organization. A plain array.

Field Type Means
id uuid The invitation
organizationId uuid Who is inviting them
organizationName string Its name, to show in the prompt
organizationKind OrganizationKind 0 accounting firm, 1 business
role OrganizationRole What they would be
invitedByName string or null Who sent it
expiresAt date-time When it stops working
grantedProfileCount integer How many clients it carries
hasOrganizationWideAccess boolean Whether the role reaches every client anyway

Nothing here names the email address, because the invitation was matched to the caller's account already.

POST /v1/me/invitations/{invitationId}/accept

Joins the organization. No body. Answers 200 with the new membership:

{
  "organizationId": "11111111-1111-4111-8111-111111111111",
  "organizationName": "Example Contabil SRL",
  "role": 0
}

Put that organizationId in X-Organization-Id on the next request and the rest of the API opens.

Refusals:

Code Status Means
invitation_not_found 404 No such invitation for this person
invitation_expired 409 More than 14 days old. Ask the firm to resend it
invitation_not_pending 409 Already accepted, declined, or revoked

Accepting when the caller is already a member of that organization is not an error. The invitation is closed and the existing membership is returned unchanged, role and grants included. Clicking an old link twice does not demote anybody.

POST /v1/me/invitations/{invitationId}/decline

Says no. No body. Answers 204. The row moves to status 2 and the link stops working.

There is no undo. A declined invitation needs a new one, not a resend, because a resend only works on a pending row.