Pipelines
The boards work moves across, and the versioned templates one is built from.
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 | /funnel-report | Get the funnel report |
GET | /funnel-report/by-linea | Get the funnel report split by Línea |
GET | /funnel-report/people | List the people behind a funnel count |
GET | /pipeline-templates | List pipeline templates |
POST | /pipeline-templates/{key}/apply | Apply a pipeline template to this workspace |
GET | /pipeline-templates/{key}/preview | Preview a localized pipeline template |
GET | /pipelines | List pipelines |
POST | /pipelines | Create pipeline |
DELETE | /pipelines/{id} | Delete pipeline |
GET | /pipelines/{id} | Get pipeline |
PUT | /pipelines/{id} | Update pipeline |
GET | /pipelines/{id}/funnel-steps | Get pipeline funnel steps |
GET /funnel-report
Get the funnel report
The board's Pasos del embudo (ADR 0132) with the number of leads that reached each one in the period, the conversion from the step before, the split by stage inside each step, and Perdido apart. Counts only: this is the door the aggregates-only Consultor seat reads, next to boards lists the sales boards that can be read (id + name) so a board picker needs no other scope; pipeline_id selects one. The last step (Venta) carries money: what its people paid since they arrived (null = more than 2 000 people, or unknown). leads:read. WHAT A COUNT MEANS. The cohort is the leads of the board whose person ARRIVED in the period (from to to, both days inclusive, in the workspace clock): the start of the conversation the lead came from (within 30 days of the lead), else the lead's creation — a conversation lead is minted when the conversation closes, days after the first message. People rows carry it as arrived_at. A lead counts in a step when it has ever stood on a stage of that step OR OF A LATER ONE — from its stage history, or from its current stage when it has none — so a lead that skipped a step still counts for the steps before it, and every cohort lead counts in the first step. The cohort is fixed by the arrival date but how far it got is read as of now: a lead arrived in the period and sold next month counts in the last step the next time you look. current is where the cohort stands today. lost is the cohort leads standing on a lost stage today, split by the step they were lost from; step_key: null is history that cannot say (never guessed into a step). sum(steps[].current) + lost.total + unplaced = cohort.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
pipeline_id | query | uuid | no | |
from | query | string | no | patrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$ |
to | query | string | no | patrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$ |
linea_id | query | any | no | |
origin | query | meta \ | organic \ | unknown |
origin_ad_id | query | string | no | patrón ^[0-9]{1,32}$ |
curl https://api.vitrinadev.com/api/v1/funnel-report \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"pipeline_id": "0b1c2d3e-0000-4000-8000-000000000001",
"period": {
"from": "2026-09-10",
"to": "2026-10-09",
"timezone": "America/Santiago"
},
"cohort": 120,
"steps": [
{
"key": "no_appointment",
"name": "Sin cita",
"position": 0,
"reached": 120,
"current": 61,
"conversion_from_previous": null,
"conversion_from_start": 1,
"stages": [
{
"id": "0b1c2d3e-0000-4000-8000-0000000000a1",
"name": "Nuevo",
"reached": 120,
"current": 61
}
]
},
{
"key": "scheduled",
"name": "Agendado",
"position": 1,
"reached": 48,
"current": 14,
"conversion_from_previous": 0.4,
"conversion_from_start": 0.4,
"stages": [],
"money": 3450000
}
],
"lost": {
"key": "lost",
"name": "Perdido",
"total": 22,
"from_steps": [
{
"step_key": "no_appointment",
"step_name": "Sin cita",
"count": 15
},
{
"step_key": "scheduled",
"step_name": "Agendado",
"count": 5
},
{
"step_key": null,
"step_name": null,
"count": 2
}
],
"stages": []
},
"unplaced": 0,
"unplaced_stages": [],
"boards": [
{
"id": "0b1c2d3e-0000-4000-8000-000000000001",
"name": "Tratamientos"
}
]
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /funnel-report/by-linea
Get the funnel report split by Línea
One row per Línea of the clinic (and «Sin línea», linea_id: null, when leads carry none), each with how many of ITS cohort leads reached every step and stand there today. Same cohort and counting rules as GET /funnel-report with one more grouping, so the rows add up to it: for every step, sum(lineas[].steps[].reached) = that step’s reached in the unsplit report, and sum(lineas[].cohort) = cohort. Open a cell’s people with GET /funnel-report/people and the same period plus linea_id (none for «Sin línea»). Counts only (the Consultor seat reads it). WHAT A COUNT MEANS. The cohort is the leads of the board whose person ARRIVED in the period (from to to, both days inclusive, in the workspace clock): the start of the conversation the lead came from (within 30 days of the lead), else the lead's creation — a conversation lead is minted when the conversation closes, days after the first message. People rows carry it as arrived_at. A lead counts in a step when it has ever stood on a stage of that step OR OF A LATER ONE — from its stage history, or from its current stage when it has none — so a lead that skipped a step still counts for the steps before it, and every cohort lead counts in the first step. The cohort is fixed by the arrival date but how far it got is read as of now: a lead arrived in the period and sold next month counts in the last step the next time you look. current is where the cohort stands today. lost is the cohort leads standing on a lost stage today, split by the step they were lost from; step_key: null is history that cannot say (never guessed into a step). sum(steps[].current) + lost.total + unplaced = cohort.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
pipeline_id | query | uuid | no | |
from | query | string | no | patrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$ |
to | query | string | no | patrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$ |
origin | query | meta \ | organic \ | unknown |
origin_ad_id | query | string | no | patrón ^[0-9]{1,32}$ |
curl https://api.vitrinadev.com/api/v1/funnel-report/by-linea \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"pipeline_id": "0b1c2d3e-0000-4000-8000-000000000001",
"period": {
"from": "2026-09-10",
"to": "2026-10-09",
"timezone": "America/Santiago"
},
"cohort": 120,
"steps": [
{
"key": "no_appointment",
"name": "Sin cita",
"position": 0
},
{
"key": "scheduled",
"name": "Agendado",
"position": 1
}
],
"lineas": [
{
"linea_id": "0b1c2d3e-0000-4000-8000-0000000000b1",
"nombre": "Implantes",
"cohort": 70,
"steps": [
{
"key": "no_appointment",
"reached": 70,
"current": 30,
"conversion_from_previous": null
},
{
"key": "scheduled",
"reached": 35,
"current": 10,
"conversion_from_previous": 0.5
}
],
"lost": 12,
"unplaced": 0
},
{
"linea_id": null,
"nombre": null,
"cohort": 50,
"steps": [
{
"key": "no_appointment",
"reached": 50,
"current": 31,
"conversion_from_previous": null
},
{
"key": "scheduled",
"reached": 13,
"current": 4,
"conversion_from_previous": 0.26
}
],
"lost": 10,
"unplaced": 0
}
]
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /funnel-report/people
List the people behind a funnel count
The leads behind one number of GET /funnel-report, read through the same cohort and the same rules, so the list always has as many rows as the count says (total). measure=reached&step= is a step headline; measure=current is who stands there today; measure=lost is Perdido (narrow with step, or lost_from_unknown=true); measure=unplaced is the leads on a stage that belongs to no step. Add stage_id for one stage of a step. Newest activity first. Needs leads:read; the contact name and phone ride only with contacts:read. D60: each person also carries score and score_reason («qué pasó»), lost_reason («motivo»), the next cita (else the last, cita_at/cita_status), paid (CLP the clinic recorded since the person arrived) and, for a clinic, origin (Meta naming the ad, Orgánico, Sin dato). WHAT A COUNT MEANS. The cohort is the leads of the board whose person ARRIVED in the period (from to to, both days inclusive, in the workspace clock): the start of the conversation the lead came from (within 30 days of the lead), else the lead's creation — a conversation lead is minted when the conversation closes, days after the first message. People rows carry it as arrived_at. A lead counts in a step when it has ever stood on a stage of that step OR OF A LATER ONE — from its stage history, or from its current stage when it has none — so a lead that skipped a step still counts for the steps before it, and every cohort lead counts in the first step. The cohort is fixed by the arrival date but how far it got is read as of now: a lead arrived in the period and sold next month counts in the last step the next time you look. current is where the cohort stands today. lost is the cohort leads standing on a lost stage today, split by the step they were lost from; step_key: null is history that cannot say (never guessed into a step). sum(steps[].current) + lost.total + unplaced = cohort.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
pipeline_id | query | uuid | no | |
from | query | string | no | patrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$ |
to | query | string | no | patrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$ |
linea_id | query | any | no | |
origin | query | meta \ | organic \ | unknown |
origin_ad_id | query | string | no | patrón ^[0-9]{1,32}$ |
measure | query | reached \ | current \ | lost \ |
step | query | string | no | mín. 1 |
stage_id | query | uuid | no | |
lost_from_unknown | query | true | no | |
limit | query | integer | no | ≥ 1, ≤ 200, por defecto 50 |
offset | query | integer | null | no | ≥ 0, por defecto 0 |
curl https://api.vitrinadev.com/api/v1/funnel-report/people \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"pipeline_id": "0b1c2d3e-0000-4000-8000-000000000001",
"period": {
"from": "2026-09-10",
"to": "2026-10-09",
"timezone": "America/Santiago"
},
"total": 1,
"limit": 50,
"offset": 0,
"people": [
{
"lead_id": "0b1c2d3e-0000-4000-8000-0000000000f1",
"title": "Implante molar",
"status": "open",
"contact_id": "0b1c2d3e-0000-4000-8000-0000000000c1",
"contact_name": "Camila Rojas",
"stage_id": "0b1c2d3e-0000-4000-8000-0000000000a2",
"stage_name": "Evaluación agendada",
"created_at": "2026-09-12T14:03:00.000Z",
"arrived_at": "2026-09-12T14:01:00.000Z",
"last_activity_at": "2026-10-08T09:30:00.000Z",
"last_move_at": "2026-09-20T16:00:00.000Z",
"last_move_stage_name": "Evaluación agendada",
"reached_step_key": "scheduled",
"phone": "+56 9 1234 5678",
"score": 72,
"score_reason": "Pidió hora para evaluación de implante; preguntó por cuotas.",
"lost_reason": null,
"cita_at": "2026-10-14T13:00:00.000Z",
"cita_status": "confirmed",
"paid": 45000,
"origin": {
"kind": "meta",
"label": "Meta · Implantes 2",
"ad_id": "120210000000000101",
"ad_name": "Implantes 2",
"ad_set_name": "INTERESES",
"campaign_name": "IMPLANTES",
"first_touch_at": "2026-09-12T14:01:00.000Z",
"via": "first_message"
}
}
]
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /pipeline-templates
List pipeline templates
The shipped, versioned board templates, localized to the resolved locale (es-CL → es, anything unshipped → en). Filter by kind and by vertical — the platform ships generic templates plus ones built for a particular vertical (automotive, healthcare, real-estate, restaurant, services, ecommerce), and a workspace may apply any of them. Pass for_vertical (the workspace's own vertical) for the picker's default view: the templates built for it plus the generic ones, its own first; omit it for the full catalog. locales_available says which languages a template can be materialised in.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
kind | query | sales \ | ticket \ | vehicle |
vertical | query | generic \ | automotive \ | healthcare \ |
for_vertical | query | string | no | mín. 1, máx. 40 |
locale | query | string | no | mín. 1, máx. 35 |
curl https://api.vitrinadev.com/api/v1/pipeline-templates \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"key": "generic_sales",
"version": 1,
"kind": "sales",
"vertical": "generic",
"name": "Ventas",
"description": "Pipeline de ventas general: sigue una oportunidad comercial desde la primera consulta concreta hasta un cierre ganado, perdido o no calificado.",
"stage_count": 9,
"locale": "es",
"locales_available": [
"en",
"es"
]
},
{
"key": "clinic_treatments",
"version": 1,
"kind": "sales",
"vertical": "healthcare",
"name": "Tratamientos",
"description": "Pipeline comercial de la clínica: desde una consulta de precio hasta un tratamiento presupuestado, aceptado e iniciado.",
"stage_count": 7,
"locale": "es",
"locales_available": [
"en",
"es"
]
}
],
"meta": {
"total": 2
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /pipeline-templates/{key}/apply
Apply a pipeline template to this workspace
Materialises the template into tenant-owned, fully editable pipeline + stage rows — after this the board is yours, and editing it never diverges from anything. IDEMPOTENT per (workspace, template key): a second call returns the existing board with already_applied: true and changes nothing. A workspace holds exactly one FALLBACK board per kind — the board a card lands on when nobody named one. is_fallback: true promotes this board and demotes the current holder of that kind; false is ignored (promote the other board instead, so there is never a moment with no fallback). 400 when is_fallback is asked for and another board of that kind already holds the flag.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
key | path | string | yes | mín. 1, máx. 80 |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
locale | string | — | mín. 1, máx. 35 |
version | integer | — | > 0 |
name_override | string | — | mín. 1, máx. 120 |
is_fallback | boolean | — | por defecto false |
curl -X POST https://api.vitrinadev.com/api/v1/pipeline-templates/<id>/apply \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"locale": "es",
"name_override": "Ventas usados",
"is_fallback": false
}'Example response (200)
{
"data": {
"already_applied": false,
"template": {
"key": "generic_sales",
"version": 1,
"locale": "es"
},
"pipeline": {
"id": "44444444-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"name": "Ventas usados",
"description": "Sigue una oportunidad comercial desde la primera consulta concreta hasta un cierre ganado, perdido o no calificado.",
"slug": "generic-sales",
"kind": "sales",
"is_fallback": true,
"direction": "sell_to_customer",
"template_key": "generic_sales",
"template_version": 1,
"template_locale": "es",
"created_at": "2026-09-04T19:07:05.226Z",
"updated_at": "2026-09-04T19:07:05.226Z"
},
"stages": [
{
"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",
"funnel_step_key": "contact",
"qualifies_lead": false,
"driver": "conversation",
"customer_message": null,
"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",
"funnel_step_key": "contact",
"qualifies_lead": false,
"driver": "conversation",
"customer_message": null,
"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",
"funnel_step_key": "won",
"qualifies_lead": false,
"driver": "conversation",
"customer_message": null,
"created_at": "2026-09-04T19:07:05.226Z"
}
]
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /pipeline-templates/{key}/preview
Preview a localized pipeline template
The exact board an apply would create: the pipeline name, description and kind, plus every stage with its slug, position, category, derived is_terminal, localized name/description and its allowed transitions (as stage KEYS — the uuids only exist once the board does), and lost_reasons: the reason list of the lost stage (each reason is a lost_reason value of PUT /leads/\{id\}/lost; empty = the default reasons). Read-only: previewing never writes.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
key | path | string | yes | mín. 1, máx. 80 |
locale | query | string | no | mín. 1, máx. 35 |
version | query | integer | no | > 0 |
curl https://api.vitrinadev.com/api/v1/pipeline-templates/<id>/preview \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"key": "generic_sales",
"version": 1,
"kind": "sales",
"vertical": "generic",
"locale": "es",
"locales_available": [
"en",
"es"
],
"pipeline": {
"name": "Ventas",
"description": "Pipeline de ventas general: sigue una oportunidad comercial desde la primera consulta concreta hasta un cierre ganado, perdido o no calificado.",
"kind": "sales"
},
"funnel_steps": [
{
"key": "contact",
"name": "Contacto",
"outcome": "progress",
"position": 0
},
{
"key": "qualified",
"name": "Calificado",
"outcome": "progress",
"position": 1
},
{
"key": "proposal",
"name": "Propuesta",
"outcome": "progress",
"position": 2
},
{
"key": "won",
"name": "Ganado",
"outcome": "progress",
"position": 3
},
{
"key": "lost",
"name": "Perdido",
"outcome": "lost",
"position": 4
}
],
"stages": [
{
"key": "new",
"slug": "new",
"position": 0,
"category": "open",
"is_terminal": false,
"name": "Nuevo",
"description": "Existe una consulta concreta, pero todavía no hubo contacto sustantivo ni calificación.",
"transitions": [
"contacted",
"won",
"lost",
"unqualified"
],
"funnel_step": "contact"
},
{
"key": "contacted",
"slug": "contacted",
"position": 1,
"category": "open",
"is_terminal": false,
"name": "Contactado",
"description": "Alguien del equipo hizo contacto real y está entendiendo la necesidad.",
"transitions": [
"new",
"qualified",
"won",
"lost",
"unqualified"
],
"funnel_step": "contact"
}
],
"lost_reasons": []
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /pipelines
List pipelines
Every board of the workspace. ?include=counts adds card_count to each row: THE WORK IN FLIGHT on that board — "how many cards would I see if I opened it" — per kind in that kind's own vocabulary: OPEN leads on a sales board, tickets that are neither resolved nor closed on a ticket board (pending and snoozed still count — deferred work is work), and currently-PLACED units on a vehicle board. It is NOT a historical total, so a long-finished funnel reads 0. Without the parameter the response is byte-identical to what it has always been; an unrecognised include value is a 400 rather than a silently ignored filter.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
include | query | counts | no |
curl https://api.vitrinadev.com/api/v1/pipelines \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "44444444-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"name": "Ventas",
"description": "Sigue una oportunidad comercial desde la primera consulta concreta hasta un cierre ganado, perdido o no calificado.",
"slug": "generic-sales",
"kind": "sales",
"is_fallback": true,
"direction": "sell_to_customer",
"template_key": "generic_sales",
"template_version": 1,
"template_locale": "es",
"created_at": "2026-09-04T19:07:05.226Z",
"updated_at": "2026-09-04T19:07:05.226Z",
"card_count": 13
},
{
"id": "44444444-0000-4000-8000-000000000002",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"name": "Soporte",
"description": "Sigue una solicitud resoluble desde la clasificación hasta la resolución, por todas las conversaciones que haga falta.",
"slug": "generic-support",
"kind": "ticket",
"is_fallback": true,
"direction": "sell_to_customer",
"template_key": "generic_support",
"template_version": 1,
"template_locale": "es",
"created_at": "2026-09-04T19:07:05.226Z",
"updated_at": "2026-09-04T19:07:05.226Z",
"card_count": 4
}
],
"meta": {
"total": 2
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /pipelines
Create pipeline
Creates an EMPTY board — a board with no stages accepts no cards, so follow it with POST /stages/bulk, or start from a shipped template with POST /pipeline-templates/\{key\}/apply instead. A workspace holds exactly one FALLBACK board per kind — the board a card lands on when nobody named one. is_fallback: true promotes this board and demotes the current holder of that kind; false is ignored (promote the other board instead, so there is never a moment with no fallback). direction says who the board's customers are: sell_to_customer (we sell to the contact; the default) or buy_from_customer (we buy from, or consign for, the contact: acquisition, consignación, buyback). The conversation-driven stage mover picks its qualification questions from it; the board's name never decides it. Boards applied from a template take the template's direction.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
name | string | yes | mín. 1, máx. 120 |
description | string | — | máx. 4000, por defecto "" |
slug | string | — | mín. 1, máx. 120 |
kind | ticket \ | sales \ | vehicle |
is_fallback | boolean | — | por defecto false |
direction | sell_to_customer \ | buy_from_customer | — |
curl -X POST https://api.vitrinadev.com/api/v1/pipelines \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Ventas mayoristas",
"description": "Oportunidades con empresas, con aprobación de precio.",
"kind": "sales",
"is_fallback": false
}'Example response (201)
{
"data": {
"id": "44444444-0000-4000-8000-000000000002",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"name": "Ventas mayoristas",
"description": "Oportunidades con empresas, con aprobación de precio.",
"slug": "ventas-mayoristas",
"kind": "sales",
"is_fallback": false,
"direction": "sell_to_customer",
"template_key": null,
"template_version": null,
"template_locale": null,
"created_at": "2026-09-22T08:41:22.273Z",
"updated_at": "2026-09-22T08:41:22.273Z"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /pipelines/{id}
Delete pipeline
DELETING A BOARD NEVER DELETES ITS CARDS. Every lead and ticket on it is MOVED first — to ?reassign_to=<pipeline id> (same kind), or to the kind's fallback board when it is omitted — and the response reports how many of each moved and where to. 409 when the board is the fallback of its kind (promote another one first) or the last board of its kind; 400 when reassign_to names a board of a different kind.
| 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/pipelines/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"tickets": 0,
"leads": 13,
"reassignedTo": "44444444-0000-4000-8000-000000000001"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /pipelines/{id}
Get pipeline
One board WITH its columns nested under stage[], in position order — the single call that renders a board. A stage's ai_agent embed is omitted for a caller without ai_agents:read.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | string | yes | mín. 1 |
curl https://api.vitrinadev.com/api/v1/pipelines/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"id": "44444444-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"name": "Ventas",
"description": "Sigue una oportunidad comercial desde la primera consulta concreta hasta un cierre ganado, perdido o no calificado.",
"slug": "generic-sales",
"kind": "sales",
"is_fallback": true,
"direction": "sell_to_customer",
"template_key": "generic_sales",
"template_version": 1,
"template_locale": "es",
"created_at": "2026-09-04T19:07:05.226Z",
"updated_at": "2026-09-04T19:07:05.226Z",
"stage": [
{
"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",
"funnel_step_key": "contact",
"qualifies_lead": false,
"driver": "conversation",
"customer_message": null,
"created_at": "2026-09-04T19:07:05.226Z",
"ai_agent": null
},
{
"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",
"funnel_step_key": "contact",
"qualifies_lead": false,
"driver": "conversation",
"customer_message": null,
"created_at": "2026-09-04T19:07:05.226Z",
"ai_agent": null
},
{
"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",
"funnel_step_key": "won",
"qualifies_lead": false,
"driver": "conversation",
"customer_message": null,
"created_at": "2026-09-04T19:07:05.226Z",
"ai_agent": null
}
]
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /pipelines/{id}
Update pipeline
Only the fields present in the body are written. kind is editable only for a non-fallback, EMPTY board that is not the last of its kind — otherwise 409. A workspace holds exactly one FALLBACK board per kind — the board a card lands on when nobody named one. is_fallback: true promotes this board and demotes the current holder of that kind; false is ignored (promote the other board instead, so there is never a moment with no fallback). direction says who the board's customers are: sell_to_customer (we sell to the contact; the default) or buy_from_customer (we buy from, or consign for, the contact: acquisition, consignación, buyback). The conversation-driven stage mover picks its qualification questions from it; the board's name never decides it. Boards applied from a template take the template's direction.
| 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 |
slug | string | — | mín. 1, máx. 120 |
kind | ticket \ | sales \ | vehicle |
is_fallback | boolean | — | |
direction | sell_to_customer \ | buy_from_customer | — |
default_team_id | string | null | — |
curl -X PUT https://api.vitrinadev.com/api/v1/pipelines/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "Oportunidades con empresas. Requiere aprobación de precio sobre 20 %."
}'Example response (200)
{
"data": {
"id": "44444444-0000-4000-8000-000000000002",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"name": "Ventas mayoristas",
"description": "Oportunidades con empresas. Requiere aprobación de precio sobre 20 %.",
"slug": "ventas-mayoristas",
"kind": "sales",
"is_fallback": false,
"direction": "sell_to_customer",
"template_key": null,
"template_version": null,
"template_locale": null,
"created_at": "2026-09-22T08:41:22.273Z",
"updated_at": "2026-09-22T08:41:22.273Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /pipelines/{id}/funnel-steps
Get pipeline funnel steps
The board's Pasos del embudo (ADR 0132): the few ordered steps its stages roll up into when the business reads its funnel, each with the stages it holds in board order — so a step holding several stages carries its per-stage split. The STEP LIST is declared by the board's template; a board with no template of its own (or one this release no longer ships) reads the generic template of its kind and says so with inherited: true. outcome: lost marks the step that holds lost and unqualified stages, reported apart from the path. unplaced_stages lists every stage with no step (or a step the template does not declare) — it is never dropped. Reassign a stage with PUT /stages/\{id\} \{ "funnel_step_key": "<key>" \} (null un-places it); default_step_key is what the template would give it.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | string | yes | mín. 1 |
curl https://api.vitrinadev.com/api/v1/pipelines/<id>/funnel-steps \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"pipeline_id": "44444444-0000-4000-8000-000000000001",
"steps_template_key": "clinic_treatments",
"inherited": false,
"locale": "es",
"steps": [
{
"key": "no_appointment",
"name": "Sin cita",
"outcome": "progress",
"position": 0,
"stages": [
{
"id": "55555555-0000-4000-8000-000000000101",
"name": "Nuevo",
"slug": "new",
"category": "open",
"position": 0,
"default_step_key": "no_appointment"
}
]
},
{
"key": "scheduled",
"name": "Agendado",
"outcome": "progress",
"position": 1,
"stages": [
{
"id": "55555555-0000-4000-8000-000000000102",
"name": "Evaluación agendada",
"slug": "evaluation-scheduled",
"category": "open",
"position": 1,
"default_step_key": "scheduled"
},
{
"id": "55555555-0000-4000-8000-000000000103",
"name": "No asistió",
"slug": "no-show",
"category": "open",
"position": 2,
"default_step_key": "scheduled"
}
]
},
{
"key": "evaluated",
"name": "Evaluado",
"outcome": "progress",
"position": 2,
"stages": [
{
"id": "55555555-0000-4000-8000-000000000104",
"name": "Evaluado",
"slug": "evaluated",
"category": "open",
"position": 3,
"default_step_key": "evaluated"
}
]
},
{
"key": "sale",
"name": "Venta",
"outcome": "progress",
"position": 3,
"stages": [
{
"id": "55555555-0000-4000-8000-000000000105",
"name": "Presupuesto enviado",
"slug": "budget-sent",
"category": "open",
"position": 4,
"default_step_key": "sale"
},
{
"id": "55555555-0000-4000-8000-000000000106",
"name": "Aceptado",
"slug": "budget-accepted",
"category": "open",
"position": 5,
"default_step_key": "sale"
},
{
"id": "55555555-0000-4000-8000-000000000107",
"name": "Tratamiento iniciado",
"slug": "won",
"category": "won",
"position": 6,
"default_step_key": "sale"
}
]
},
{
"key": "lost",
"name": "Perdido",
"outcome": "lost",
"position": 4,
"stages": [
{
"id": "55555555-0000-4000-8000-000000000108",
"name": "Perdido",
"slug": "lost",
"category": "lost",
"position": 7,
"default_step_key": "lost"
},
{
"id": "55555555-0000-4000-8000-000000000109",
"name": "No calificado",
"slug": "unqualified",
"category": "unqualified",
"position": 8,
"default_step_key": "lost"
}
]
}
],
"unplaced_stages": [
{
"id": "55555555-0000-4000-8000-000000000110",
"name": "Control post tratamiento",
"slug": "control",
"category": "open",
"position": 9,
"default_step_key": null
}
]
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429