Manage the workspace's people
Invite people, move them between roles or branches, and remove them cleanly.
GET /memberships lists the people in the workspace. Every member carries a base role (owner, admin, supervisor, agent, consultant), and a custom_role_id can replace it outright. When that field has a value, the member's effective scopes are exactly the custom role's.
location_ids says which branches a member is posted to. It only matters when their role carries stock_visibility: own_locations.
Reading needs memberships:read; writing, memberships:write.
Trap
You can't grant a rank above your own
Changing a member's role or branch goes through an escalation ceiling. You never grant a rank higher than yours, and you never hand out branches you can't see. An API key works the same way: a connected app doesn't end up with more authority than the member who connected it.
See the roster
curl https://api.vitrinadev.com/api/v1/memberships \
-H "Authorization: Bearer $VITRINA_KEY"{
"data": [
{
"id": "11111111-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"user_id": "11111111-0000-4000-8000-000000000001",
"role": "agent",
"custom_role_id": "d2d2d2d2-0000-4000-8000-000000000001",
"custom_role_name": "Vendedor sucursal",
"status": "active",
"display_name": "Camila Rojas",
"phone": "+56912345001",
"avatar_url": null,
"location_ids": ["b1b1b1b1-0000-4000-8000-000000000001"],
"created_at": "2026-01-10T13:00:00.000Z",
"updated_at": "2026-09-10T13:00:00.000Z"
}
],
"meta": { "total": 1 }
}Invite by email
Most people join by invitation rather than by an id you already know:
curl -X POST https://api.vitrinadev.com/api/v1/memberships/invitations \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"role": "agent",
"custom_role_id": "d2d2d2d2-0000-4000-8000-000000000001",
"location_ids": ["b1b1b1b1-0000-4000-8000-000000000001"]
}'The response carries accept_url, the sign-up link. It's what you use while the workspace has no mail provider configured:
{
"data": {
"id": "e5e5e5e5-0000-4000-8000-000000000001",
"email": "[email protected]",
"role": "agent",
"custom_role_id": "d2d2d2d2-0000-4000-8000-000000000001",
"custom_role_name": "Vendedor sucursal",
"location_ids": ["b1b1b1b1-0000-4000-8000-000000000001"],
"accept_url": "https://app.vitrinadev.com/invite/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"expires_at": "2026-09-17T13:00:00.000Z"
}
}role and custom_role_id are independent, same as adding a member directly. role sets the escalation rank; the custom role decides effective access. location_ids rides along with the invitation and lands on the membership the moment the person accepts. A branch-scoped seat is already narrowed at the first login.
An API key can't accept an invitation
POST /memberships/invitations/by-token/{token}/accept needs no scope. Its
boundary is the token plus an exact email match. Only a person can call it: a
user session, or a personal token acting as that person (see Personal
tokens). An API key gets 403.
Accepting twice with the same token returns the same membership and applies
nothing a second time.
Change a role, status or branch
The PUT is partial: send only what changes.
curl -X PUT https://api.vitrinadev.com/api/v1/memberships/11111111-0000-4000-8000-000000000002 \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "custom_role_id": "d2d2d2d2-0000-4000-8000-000000000001", "location_ids": ["b1b1b1b1-0000-4000-8000-000000000001"] }'display_name, phone and avatar_url ride on the same PUT and write the target member's profile. Those three skip the self-action restriction that role and status carry, so editing your own identity is always allowed. To upload a photo as a file instead of passing a URL, use POST /memberships/{id}/avatar (multipart, up to 5MB).
Remove someone without stranding their work
curl -X DELETE "https://api.vitrinadev.com/api/v1/memberships/11111111-0000-4000-8000-000000000002?handover=round_robin" \
-H "Authorization: Bearer $VITRINA_KEY"{
"data": {
"mode": "round_robin",
"conversationsMoved": 6,
"leadsMoved": 3,
"perAssignee": { "11111111-0000-4000-8000-000000000003": 9 }
}
}handover decides where the person's open work goes.
handover | What happens to the open work |
|---|---|
unassign (default) | Conversations surface as unassigned and nobody is notified. |
user | Everything moves to the inheritor you name in handover_assignee_id. |
round_robin | It's dealt across the rest of the team. |
Before you remove anyone, GET /memberships/{id}/handover-preview shows how much open work they hold and who could inherit it.
Field by field, memberships and invitations are in the Team reference.