VitrinaAPI

Roles personalizados

El catálogo de scopes y visibilidad por rol — acceso efectivo, no una etiqueta.

Beta
Puede cambiar en cualquier momento, con una entrada en el changelog y aviso a quienes la llamaron recientemente — ver versionado.

Descarga la proyección completa de la API pública: openapi.json.

Organizar equipos y roles explica este recurso en prosa, con ejemplos ejecutables.

MétodoRutaQué hace
GET/rolesList the workspace’s custom roles
POST/rolesCreate a custom role
DELETE/roles/{id}Delete a custom role
GET/roles/{id}Fetch one custom role
PUT/roles/{id}Update a custom role

GET /roles

List the workspace’s custom roles

Newest first, with a total. Unpaginated — a workspace has a handful of custom roles. Built-in tiers are not returned: they are not rows on this table. On a clinic workspace the list always includes the packaged seats (preset_key clinic_reception | clinic_professional; the third, Administración, is the built-in admin tier), created on first list; their permissions are managed by Vitrina (editing them, or deleting the role, is a 409).

curl https://api.vitrinadev.com/api/v1/roles \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": [
    {
      "id": "d2d2d2d2-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "name": "Vendedor sucursal",
      "description": "Ve solo sus propias conversaciones y el stock de su sucursal.",
      "scopes": [
        "conversations:read",
        "conversations:write",
        "leads:read",
        "leads:write"
      ],
      "record_visibility": "assigned",
      "stock_visibility": "own_locations",
      "created_at": "2026-01-10T13:00:00.000Z",
      "updated_at": "2026-09-10T13:00:00.000Z"
    }
  ],
  "meta": {
    "total": 1
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /roles

Create a custom role

scopes is optional; omitting it creates an EMPTY role, which grants nothing and is accepted. Only known, non-owner-only permissions are allowed — the wildcard *, an unknown string and the owner-only scopes (billing:write, tenant:delete) are all a 400.

record_visibility is all | assigned_unassigned | assigned and narrows conversations, tickets and leads by assignee. stock_visibility is all | own_locations and narrows VEHICLE reads to the sucursales on the member’s own workspace_member.location_ids — a member on an own_locations role with no sucursales sees no stock at all, and a vehicle with no location_id is invisible to such a member by definition. project_visibility (Módulo Proyectos) is all | sales_office | assigned and narrows LEADS (and the proyectos list) to the proyectos the member works: assigned = the proyectos they are assigned to (ejecutivo de ventas), sales_office = those plus every proyecto served by a sala de ventas on their location_ids (jefe de sala). A lead the member owns stays visible. All three default to all. max_discount_percent (Módulo Proyectos) is the role’s límite de descuento: the largest effective discount, in percent over the list total, a member on the role may send on a cotización without approval. null (the default) is no limit; above the limit a member holding cotizacion_discount:approve must approve first. clinic_record_visibility (clinics) is all | own_patients: with own_patients a clinic_record:read holder opens only the clinical records of patients their linked professional attended or is assigned to (lifted by the clinic setting clinic_records_shared_across_professionals).

Every field is priced against the caller’s own access: a scope you do not hold, a record_visibility wider than yours, or a stock_visibility or project_visibility wider than yours is a 403. Owners and unrestricted admins are unaffected — the ceilings can only ever bite a caller who is themselves restricted. A narrowly-scoped sk_ key cannot set record_visibility at all (a credential carries no such ceiling of its own, so there is nothing to bound the grant against); use a signed-in user.

Cuerpo

CampoTipoObligatorioRestricciones
namestringsímín. 1, máx. 200
descriptionstring | null—máx. 2000
tintstring | null—máx. 40
scopesstring[]—
record_visibilityall \assigned_unassigned \assigned
stock_visibilityall \own_locations—
project_visibilityall \sales_office \assigned
clinic_record_visibilityall \own_patients—
max_discount_percentnumber | null—≥ 0, ≤ 100
curl -X POST https://api.vitrinadev.com/api/v1/roles \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Vendedor sucursal",
    "scopes": [
      "conversations:read",
      "conversations:write",
      "leads:read",
      "leads:write"
    ],
    "record_visibility": "assigned",
    "stock_visibility": "own_locations"
  }'

Ejemplo de respuesta (201)

{
  "data": {
    "id": "d2d2d2d2-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Vendedor sucursal",
    "description": "Ve solo sus propias conversaciones y el stock de su sucursal.",
    "scopes": [
      "conversations:read",
      "conversations:write",
      "leads:read",
      "leads:write"
    ],
    "record_visibility": "assigned",
    "stock_visibility": "own_locations",
    "created_at": "2026-01-10T13:00:00.000Z",
    "updated_at": "2026-09-10T13:00:00.000Z"
  }
}

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /roles/{id}

Delete a custom role

A role that still has members is a 409: unlinking them would resolve each one to their BUILT-IN tier with record_visibility: all and stock_visibility: all, i.e. silently promote them. Empty the role first, or pass ?reassign_to=<role id> to move every member onto a named replacement in one step — the replacement is a grant and is priced on all three axes exactly as the body of a PUT is. reassign_to may not name the role being deleted (400), and reassignment to a built-in tier is not supported.

The delete also REVOKES every still-pending invitation that named the role, in the same transaction: that foreign key is ON DELETE SET NULL, so such an invitation would otherwise accept on its plain built-in tier. Accepted, expired and already-revoked invitations are untouched.

ParámetroEnTipoObligatorioRestricciones
idpathuuidsí
reassign_toqueryuuidno
curl -X DELETE https://api.vitrinadev.com/api/v1/roles/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429

GET /roles/{id}

Fetch one custom role

The role including both visibility ceilings. A role belonging to another workspace is a 404, never a 403 — nothing here confirms a guessed id.

ParámetroEnTipoObligatorioRestricciones
idpathuuidsí
curl https://api.vitrinadev.com/api/v1/roles/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "id": "d2d2d2d2-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Vendedor sucursal",
    "description": "Ve solo sus propias conversaciones y el stock de su sucursal.",
    "scopes": [
      "conversations:read",
      "conversations:write",
      "leads:read",
      "leads:write"
    ],
    "record_visibility": "assigned",
    "stock_visibility": "own_locations",
    "created_at": "2026-01-10T13:00:00.000Z",
    "updated_at": "2026-09-10T13:00:00.000Z"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

PUT /roles/{id}

Update a custom role

A PUT that behaves as a PATCH: only the fields present are applied and at least one is required. scopes is a FULL REPLACEMENT array, not a merge — but only the scopes being ADDED are priced, so an admin who lacks X can still rename a role that already carries it.

The change takes effect on the next request of every member already on the role, including the caller.

record_visibility is all | assigned_unassigned | assigned and narrows conversations, tickets and leads by assignee. stock_visibility is all | own_locations and narrows VEHICLE reads to the sucursales on the member’s own workspace_member.location_ids — a member on an own_locations role with no sucursales sees no stock at all, and a vehicle with no location_id is invisible to such a member by definition. project_visibility (Módulo Proyectos) is all | sales_office | assigned and narrows LEADS (and the proyectos list) to the proyectos the member works: assigned = the proyectos they are assigned to (ejecutivo de ventas), sales_office = those plus every proyecto served by a sala de ventas on their location_ids (jefe de sala). A lead the member owns stays visible. All three default to all. max_discount_percent (Módulo Proyectos) is the role’s límite de descuento: the largest effective discount, in percent over the list total, a member on the role may send on a cotización without approval. null (the default) is no limit; above the limit a member holding cotizacion_discount:approve must approve first. clinic_record_visibility (clinics) is all | own_patients: with own_patients a clinic_record:read holder opens only the clinical records of patients their linked professional attended or is assigned to (lifted by the clinic setting clinic_records_shared_across_professionals).

Every field is priced against the caller’s own access: a scope you do not hold, a record_visibility wider than yours, or a stock_visibility or project_visibility wider than yours is a 403. Owners and unrestricted admins are unaffected — the ceilings can only ever bite a caller who is themselves restricted. A narrowly-scoped sk_ key cannot set record_visibility at all (a credential carries no such ceiling of its own, so there is nothing to bound the grant against); use a signed-in user.

ParámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
namestring—mín. 1, máx. 200
descriptionstring | null—máx. 2000
tintstring | null—máx. 40
scopesstring[]—
record_visibilityall \assigned_unassigned \assigned
stock_visibilityall \own_locations—
project_visibilityall \sales_office \assigned
clinic_record_visibilityall \own_patients—
max_discount_percentnumber | null—≥ 0, ≤ 100
curl -X PUT https://api.vitrinadev.com/api/v1/roles/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "record_visibility": "assigned_unassigned"
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "id": "d2d2d2d2-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "Vendedor sucursal",
    "description": "Ve solo sus propias conversaciones y el stock de su sucursal.",
    "scopes": [
      "conversations:read",
      "conversations:write",
      "leads:read",
      "leads:write"
    ],
    "record_visibility": "assigned_unassigned",
    "stock_visibility": "own_locations",
    "created_at": "2026-01-10T13:00:00.000Z",
    "updated_at": "2026-09-10T13:00:00.000Z"
  }
}

Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429

En esta página