VitrinaAPI

Build teams and custom roles

The teams that route work, and the roles that decide what each member sees.

A team receives the work; a custom role decides what the person receiving it may do. They're two separate configuration resources and they're almost always edited together.

Teams

A channel's conversation lands with the team that channel defaults to, unless an assignment rule says otherwise. A ticket carries its own team_id. A team also holds a coverage schedule.

Reading needs teams:read; writing, teams:write.

curl -X POST https://api.vitrinadev.com/api/v1/teams \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Technical support",
    "hours_mode": "override",
    "hours": { "mon": { "on": true, "start": "09:00", "end": "19:00" } }
  }'
{
  "data": {
    "id": "cccccccc-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Technical support",
    "brand": null,
    "hours_mode": "override",
    "hours": { "mon": { "on": true, "start": "09:00", "end": "19:00" } },
    "created_at": "2026-09-22T13:00:00.000Z",
    "updated_at": "2026-09-22T13:00:00.000Z"
  }
}

hours_mode decides where that schedule comes from. On "workspace", the default, the team follows the workspace's; on "override" it follows its own, and hours spells it out as a partial weekday map. That own schedule feeds the SLA clock, the is_business_hours check in automations, and the AI agent's "office closed" tone. Timezone and holidays always come from the workspace and never get overridden.

Team members

curl -X POST https://api.vitrinadev.com/api/v1/teams/cccccccc-0000-4000-8000-000000000001/members \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "user_id": "11111111-0000-4000-8000-000000000001", "role": "member" }'

user_id has to be an active member of the workspace already. An unknown id, one from another workspace, or a deactivated one is refused with a 400. The role here (lead or member) is a label on the team rather than a permission; what the person may do is decided by their workspace role or their custom role.

Trap

Assigned tickets stay assigned

DELETE /teams/{id}/members/{membershipId} removes only the team membership. The person keeps their account and any other team they're on, and the tickets already assigned to them stay with them. Their assignee_user_id isn't cleared, so a team change never orphans work.

Removing someone from the workspace works differently. There, DELETE /memberships/{id} does hand over the person's open work, because they lose the ability to see it. That's in Team.

Custom roles

A custom role sets a membership's effective access. A membership pointed at one gets exactly that role's scopes, with no inherited floor from the base role, plus its two visibility ceilings, resolved on every request.

Reading needs roles:read; writing, roles:write.

curl -X POST https://api.vitrinadev.com/api/v1/roles \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Support agent",
    "scopes": ["conversations:read", "conversations:write", "tickets:read", "tickets:write"],
    "record_visibility": "assigned",
    "stock_visibility": "all"
  }'
{
  "data": {
    "id": "d2d2d2d2-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Support agent",
    "scopes": ["conversations:read", "conversations:write", "tickets:read", "tickets:write"],
    "record_visibility": "assigned",
    "stock_visibility": "all",
    "created_at": "2026-09-22T13:00:00.000Z",
    "updated_at": "2026-09-22T13:00:00.000Z"
  }
}

The two ceilings are independent:

FieldValuesWhat it narrows
record_visibilityall, assigned_unassigned, assignedWhich conversations, tickets and leads the member sees, keyed on who they're assigned to.
stock_visibilityall, own_locationsWhether catalog reads narrow to the member's own locations. The fact lives on the membership; the policy lives on the role.

Built-in roles (owner, admin, supervisor, agent) aren't custom roles, and they always resolve to all on both axes.

Trap

You can't build a role wider than your own

Creating or editing a role is checked against the caller's own access. A scope they don't hold, a record_visibility wider than theirs, or a stock_visibility wider than theirs is a 403. scopes also accepts only known permissions that aren't owner-only. An unrestricted owner or admin is unaffected: the ceiling reaches only a caller who is already restricted.

Deleting a role that still has active members is a 409. Empty it first, or pass ?reassign_to=<role id> to move everyone onto another role in one step, checked on all three axes the way a PUT is.

The full contracts live in the Teams and Custom roles reference.

On this page