VitrinaAPI

Stages

A board's columns: their category, their ageing threshold and which other columns a card may jump to.

Beta
This may change at any time, with a changelog entry and a notice to recent callers — see versioning.

Download the full API pública projection: openapi.json.

Move leads across the board explains this resource in prose, with runnable examples.

MethodPathWhat it does
GET/stagesList stages
POST/stagesCreate stage
PUT/stagesUpdate stages in bulk
DELETE/stages/{id}Delete stage
GET/stages/{id}Get stage
PUT/stages/{id}Update stage
POST/stages/bulkCreate stages in bulk

GET /stages

List stages

Every stage of every board in the workspace, across pipelines and kinds — read pipeline_id on each row to group them, or GET /pipelines/\{id\}, which returns one board with its columns already nested. allowed_transitions_to is the set of stage ids a card may move to from here; empty or omitted means no restriction. A move the graph forbids is refused when the card is moved, not when the stage is saved.

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

Example response (200)

{
  "data": [
    {
      "id": "55555555-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "pipeline_id": "44444444-0000-4000-8000-000000000001",
      "name": "Nuevo",
      "description": "Existe una consulta concreta, pero todavía no hubo contacto sustantivo ni calificación.",
      "slug": "new",
      "position": 0,
      "category": "open",
      "is_terminal": false,
      "won_state": null,
      "sla_days": null,
      "allowed_transitions_to": [
        "55555555-0000-4000-8000-000000000002",
        "55555555-0000-4000-8000-000000000006",
        "55555555-0000-4000-8000-000000000007"
      ],
      "ai_agent_id": null,
      "ai_agent_graph_id": null,
      "is_human": false,
      "is_marketing": false,
      "template_stage_key": "new",
      "created_at": "2026-09-04T19:07:05.226Z"
    },
    {
      "id": "55555555-0000-4000-8000-000000000002",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "pipeline_id": "44444444-0000-4000-8000-000000000001",
      "name": "Contactado",
      "description": "Alguien del equipo hizo contacto real y está entendiendo la necesidad.",
      "slug": "contacted",
      "position": 1,
      "category": "open",
      "is_terminal": false,
      "won_state": null,
      "sla_days": 3,
      "allowed_transitions_to": [
        "55555555-0000-4000-8000-000000000003",
        "55555555-0000-4000-8000-000000000006",
        "55555555-0000-4000-8000-000000000007"
      ],
      "ai_agent_id": null,
      "ai_agent_graph_id": null,
      "is_human": false,
      "is_marketing": false,
      "template_stage_key": "contacted",
      "created_at": "2026-09-04T19:07:05.226Z"
    },
    {
      "id": "55555555-0000-4000-8000-000000000006",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "pipeline_id": "44444444-0000-4000-8000-000000000001",
      "name": "Ganado",
      "description": "La oportunidad se cerró a favor.",
      "slug": "won",
      "position": 6,
      "category": "won",
      "is_terminal": true,
      "won_state": "won",
      "sla_days": null,
      "allowed_transitions_to": [],
      "ai_agent_id": null,
      "ai_agent_graph_id": null,
      "is_human": false,
      "is_marketing": false,
      "template_stage_key": "won",
      "created_at": "2026-09-04T19:07:05.226Z"
    }
  ],
  "meta": {
    "total": 3
  }
}

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

POST /stages

Create stage

Adds one column to a board. slug is generated from the name when omitted; category defaults to open. is_terminal and won_state are DERIVED from category and are rejected as input (both bodies are strict, so sending either is a 400 naming the key). sla_days is the per-stage ageing threshold in days: an integer of at least 1, or null for "no limit" — omitting the key leaves the current value alone, sending null clears it, and those are different requests.

Body

FieldTypeRequiredConstraints
namestringyesmín. 1, máx. 120
descriptionstringmáx. 4000, por defecto ""
pipeline_iduuidyes
ai_agent_idstring | null
positioninteger≥ 0
slugstringmín. 1, máx. 80, patrón ^[a-z0-9]+(?:[-_][a-z0-9]+)*$
categoryopen \won \lost \
sla_daysinteger | null≥ 1
allowed_transitions_touuid[]
curl -X POST https://api.vitrinadev.com/api/v1/stages \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Contactado",
    "description": "Alguien del equipo hizo contacto real y está entendiendo la necesidad.",
    "pipeline_id": "44444444-0000-4000-8000-000000000001",
    "position": 1,
    "slug": "contacted",
    "category": "open",
    "sla_days": 3,
    "allowed_transitions_to": [
      "55555555-0000-4000-8000-000000000003",
      "55555555-0000-4000-8000-000000000006",
      "55555555-0000-4000-8000-000000000007"
    ]
  }'

Example response (201)

{
  "data": {
    "id": "55555555-0000-4000-8000-000000000002",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "pipeline_id": "44444444-0000-4000-8000-000000000001",
    "name": "Contactado",
    "description": "Alguien del equipo hizo contacto real y está entendiendo la necesidad.",
    "slug": "contacted",
    "position": 1,
    "category": "open",
    "is_terminal": false,
    "won_state": null,
    "sla_days": 3,
    "allowed_transitions_to": [
      "55555555-0000-4000-8000-000000000003",
      "55555555-0000-4000-8000-000000000006",
      "55555555-0000-4000-8000-000000000007"
    ],
    "ai_agent_id": null,
    "ai_agent_graph_id": null,
    "is_human": false,
    "is_marketing": false,
    "template_stage_key": "contacted",
    "created_at": "2026-09-04T19:07:05.226Z"
  }
}

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

PUT /stages

Update stages in bulk

Reordering a board: an array of \{ id, …fields \}, up to 100. This is the endpoint a drag-and-drop save uses, because positions have to move together. Only the fields present on an element are written; the body is strict, so an unknown or derived key is a 400. is_terminal and won_state are DERIVED from category and are rejected as input (both bodies are strict, so sending either is a 400 naming the key). sla_days is the per-stage ageing threshold in days: an integer of at least 1, or null for "no limit" — omitting the key leaves the current value alone, sending null clears it, and those are different requests.

curl -X PUT https://api.vitrinadev.com/api/v1/stages \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "id": "55555555-0000-4000-8000-000000000001",
      "position": 0
    },
    {
      "id": "55555555-0000-4000-8000-000000000002",
      "position": 1,
      "sla_days": 5
    }
  ]'

Example response (200)

{
  "data": [
    {
      "id": "55555555-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "pipeline_id": "44444444-0000-4000-8000-000000000001",
      "name": "Nuevo",
      "description": "Existe una consulta concreta, pero todavía no hubo contacto sustantivo ni calificación.",
      "slug": "new",
      "position": 0,
      "category": "open",
      "is_terminal": false,
      "won_state": null,
      "sla_days": null,
      "allowed_transitions_to": [
        "55555555-0000-4000-8000-000000000002",
        "55555555-0000-4000-8000-000000000006",
        "55555555-0000-4000-8000-000000000007"
      ],
      "ai_agent_id": null,
      "ai_agent_graph_id": null,
      "is_human": false,
      "is_marketing": false,
      "template_stage_key": "new",
      "created_at": "2026-09-04T19:07:05.226Z"
    },
    {
      "id": "55555555-0000-4000-8000-000000000002",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "pipeline_id": "44444444-0000-4000-8000-000000000001",
      "name": "Contactado",
      "description": "Alguien del equipo hizo contacto real y está entendiendo la necesidad.",
      "slug": "contacted",
      "position": 1,
      "category": "open",
      "is_terminal": false,
      "won_state": null,
      "sla_days": 5,
      "allowed_transitions_to": [
        "55555555-0000-4000-8000-000000000003",
        "55555555-0000-4000-8000-000000000006",
        "55555555-0000-4000-8000-000000000007"
      ],
      "ai_agent_id": null,
      "ai_agent_graph_id": null,
      "is_human": false,
      "is_marketing": false,
      "template_stage_key": "contacted",
      "created_at": "2026-09-04T19:07:05.226Z"
    }
  ],
  "meta": {
    "total": 2
  }
}

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

DELETE /stages/{id}

Delete stage

DELETING A COLUMN NEVER DELETES THE CARDS ON IT. Every lead and ticket sitting there is MOVED first — to ?reassign_to=<stage id>, or to the first sibling column of the same board when it is omitted — and the response reports how many of each moved and where to. 409 when the stage is the last one on its board, or when there is no sibling to move the cards into; 400 when reassign_to names a stage on another board. The column, its transition graph and its ageing threshold are gone for good; the transition history that mentions it is not.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
reassign_toqueryuuidno
curl -X DELETE https://api.vitrinadev.com/api/v1/stages/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "tickets": 0,
    "leads": 4,
    "reassignedTo": "55555555-0000-4000-8000-000000000002"
  }
}

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

GET /stages/{id}

Get stage

One column, with its category, its ageing threshold and its transition graph. allowed_transitions_to is the set of stage ids a card may move to from here; empty or omitted means no restriction. A move the graph forbids is refused when the card is moved, not when the stage is saved.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl https://api.vitrinadev.com/api/v1/stages/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "55555555-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "pipeline_id": "44444444-0000-4000-8000-000000000001",
    "name": "Nuevo",
    "description": "Existe una consulta concreta, pero todavía no hubo contacto sustantivo ni calificación.",
    "slug": "new",
    "position": 0,
    "category": "open",
    "is_terminal": false,
    "won_state": null,
    "sla_days": null,
    "allowed_transitions_to": [
      "55555555-0000-4000-8000-000000000002",
      "55555555-0000-4000-8000-000000000006",
      "55555555-0000-4000-8000-000000000007"
    ],
    "ai_agent_id": null,
    "ai_agent_graph_id": null,
    "is_human": false,
    "is_marketing": false,
    "template_stage_key": "new",
    "created_at": "2026-09-04T19:07:05.226Z"
  }
}

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

PUT /stages/{id}

Update stage

Only the fields present in the body are written. is_terminal and won_state are DERIVED from category and are rejected as input (both bodies are strict, so sending either is a 400 naming the key). sla_days is the per-stage ageing threshold in days: an integer of at least 1, or null for "no limit" — omitting the key leaves the current value alone, sending null clears it, and those are different requests.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1

Body

FieldTypeRequiredConstraints
namestringmín. 1, máx. 120
descriptionstringmáx. 4000
ai_agent_idstring | null
positioninteger≥ 0
slugstringmín. 1, máx. 80, patrón ^[a-z0-9]+(?:[-_][a-z0-9]+)*$
categoryopen \won \lost \
sla_daysinteger | null≥ 1
allowed_transitions_touuid[]
curl -X PUT https://api.vitrinadev.com/api/v1/stages/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "position": 2,
    "sla_days": 5
  }'

Example response (200)

{
  "data": {
    "id": "55555555-0000-4000-8000-000000000002",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "pipeline_id": "44444444-0000-4000-8000-000000000001",
    "name": "Contactado",
    "description": "Alguien del equipo hizo contacto real y está entendiendo la necesidad.",
    "slug": "contacted",
    "position": 2,
    "category": "open",
    "is_terminal": false,
    "won_state": null,
    "sla_days": 5,
    "allowed_transitions_to": [
      "55555555-0000-4000-8000-000000000003",
      "55555555-0000-4000-8000-000000000006",
      "55555555-0000-4000-8000-000000000007"
    ],
    "ai_agent_id": null,
    "ai_agent_graph_id": null,
    "is_human": false,
    "is_marketing": false,
    "template_stage_key": "contacted",
    "created_at": "2026-09-04T19:07:05.226Z"
  }
}

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

POST /stages/bulk

Create stages in bulk

Up to 100 stages in one call — how a board is laid out in a single request instead of one round trip per column. Each element is exactly the single-create body, so the same defaults and the same strictness apply. Prefer POST /pipeline-templates/\{key\}/apply when a shipped template already describes the board you want.

curl -X POST https://api.vitrinadev.com/api/v1/stages/bulk \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "name": "Nuevo",
      "pipeline_id": "44444444-0000-4000-8000-000000000001",
      "position": 0,
      "category": "open"
    },
    {
      "name": "Ganado",
      "pipeline_id": "44444444-0000-4000-8000-000000000001",
      "position": 6,
      "category": "won"
    }
  ]'

Example response (201)

{
  "data": [
    {
      "id": "55555555-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "pipeline_id": "44444444-0000-4000-8000-000000000001",
      "name": "Nuevo",
      "description": "Existe una consulta concreta, pero todavía no hubo contacto sustantivo ni calificación.",
      "slug": "new",
      "position": 0,
      "category": "open",
      "is_terminal": false,
      "won_state": null,
      "sla_days": null,
      "allowed_transitions_to": [
        "55555555-0000-4000-8000-000000000002",
        "55555555-0000-4000-8000-000000000006",
        "55555555-0000-4000-8000-000000000007"
      ],
      "ai_agent_id": null,
      "ai_agent_graph_id": null,
      "is_human": false,
      "is_marketing": false,
      "template_stage_key": "new",
      "created_at": "2026-09-04T19:07:05.226Z"
    },
    {
      "id": "55555555-0000-4000-8000-000000000006",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "pipeline_id": "44444444-0000-4000-8000-000000000001",
      "name": "Ganado",
      "description": "La oportunidad se cerró a favor.",
      "slug": "won",
      "position": 6,
      "category": "won",
      "is_terminal": true,
      "won_state": "won",
      "sla_days": null,
      "allowed_transitions_to": [],
      "ai_agent_id": null,
      "ai_agent_graph_id": null,
      "is_human": false,
      "is_marketing": false,
      "template_stage_key": "won",
      "created_at": "2026-09-04T19:07:05.226Z"
    }
  ],
  "meta": {
    "total": 2
  }
}

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

On this page