Stages
A board's columns: their category, their ageing threshold and which other columns a card may jump to.
Download the full API pública projection: openapi.json.
Move leads across the board explains this resource in prose, with runnable examples.
| Method | Path | What it does |
|---|---|---|
GET | /stages | List stages |
POST | /stages | Create stage |
PUT | /stages | Update stages in bulk |
DELETE | /stages/{id} | Delete stage |
GET | /stages/{id} | Get stage |
PUT | /stages/{id} | Update stage |
POST | /stages/bulk | Create 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
| Field | Type | Required | Constraints |
|---|---|---|---|
name | string | yes | mín. 1, máx. 120 |
description | string | — | máx. 4000, por defecto "" |
pipeline_id | uuid | yes | |
ai_agent_id | string | null | — | |
position | integer | — | ≥ 0 |
slug | string | — | mín. 1, máx. 80, patrón ^[a-z0-9]+(?:[-_][a-z0-9]+)*$ |
category | open \ | won \ | lost \ |
sla_days | integer | null | — | ≥ 1 |
allowed_transitions_to | uuid[] | — |
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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | string | yes | mín. 1 |
reassign_to | query | uuid | no |
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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | string | yes | mí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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | string | yes | mín. 1 |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
name | string | — | mín. 1, máx. 120 |
description | string | — | máx. 4000 |
ai_agent_id | string | null | — | |
position | integer | — | ≥ 0 |
slug | string | — | mín. 1, máx. 80, patrón ^[a-z0-9]+(?:[-_][a-z0-9]+)*$ |
category | open \ | won \ | lost \ |
sla_days | integer | null | — | ≥ 1 |
allowed_transitions_to | uuid[] | — |
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