VitrinaAPI

Embudos

Los tableros por los que avanza el trabajo, y las plantillas versionadas con las que se arma uno.

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.

Mover leads por el embudo explica este recurso en prosa, con ejemplos ejecutables.

MétodoRutaQué hace
GET/funnel-reportGet the funnel report
GET/funnel-report/by-lineaGet the funnel report split by Línea
GET/funnel-report/peopleList the people behind a funnel count
GET/pipeline-templatesList pipeline templates
POST/pipeline-templates/{key}/applyApply a pipeline template to this workspace
GET/pipeline-templates/{key}/previewPreview a localized pipeline template
GET/pipelinesList pipelines
POST/pipelinesCreate pipeline
DELETE/pipelines/{id}Delete pipeline
GET/pipelines/{id}Get pipeline
PUT/pipelines/{id}Update pipeline
GET/pipelines/{id}/funnel-stepsGet 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.

ParámetroEnTipoObligatorioRestricciones
pipeline_idqueryuuidno
fromquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
toquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
linea_idqueryanyno
originquerymeta \organic \unknown
origin_ad_idquerystringnopatrón ^[0-9]{1,32}$
curl https://api.vitrinadev.com/api/v1/funnel-report \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (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"
      }
    ]
  }
}

Responde: 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.

ParámetroEnTipoObligatorioRestricciones
pipeline_idqueryuuidno
fromquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
toquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
originquerymeta \organic \unknown
origin_ad_idquerystringnopatrón ^[0-9]{1,32}$
curl https://api.vitrinadev.com/api/v1/funnel-report/by-linea \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (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
      }
    ]
  }
}

Responde: 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.

ParámetroEnTipoObligatorioRestricciones
pipeline_idqueryuuidno
fromquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
toquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
linea_idqueryanyno
originquerymeta \organic \unknown
origin_ad_idquerystringnopatrón ^[0-9]{1,32}$
measurequeryreached \current \lost \
stepquerystringnomín. 1
stage_idqueryuuidno
lost_from_unknownquerytrueno
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
offsetqueryinteger | nullno≥ 0, por defecto 0
curl https://api.vitrinadev.com/api/v1/funnel-report/people \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (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"
        }
      }
    ]
  }
}

Responde: 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.

ParámetroEnTipoObligatorioRestricciones
kindquerysales \ticket \vehicle
verticalquerygeneric \automotive \healthcare \
for_verticalquerystringnomín. 1, máx. 40
localequerystringnomín. 1, máx. 35
curl https://api.vitrinadev.com/api/v1/pipeline-templates \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (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
  }
}

Responde: 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.

ParámetroEnTipoObligatorioRestricciones
keypathstringsímín. 1, máx. 80

Cuerpo

CampoTipoObligatorioRestricciones
localestring—mín. 1, máx. 35
versioninteger—> 0
name_overridestring—mín. 1, máx. 120
is_fallbackboolean—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
  }'

Ejemplo de respuesta (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"
      }
    ]
  }
}

Responde: 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.

ParámetroEnTipoObligatorioRestricciones
keypathstringsímín. 1, máx. 80
localequerystringnomín. 1, máx. 35
versionqueryintegerno> 0
curl https://api.vitrinadev.com/api/v1/pipeline-templates/<id>/preview \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (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": []
  }
}

Responde: 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.

ParámetroEnTipoObligatorioRestricciones
includequerycountsno
curl https://api.vitrinadev.com/api/v1/pipelines \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (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
  }
}

Responde: 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.

Cuerpo

CampoTipoObligatorioRestricciones
namestringsímín. 1, máx. 120
descriptionstring—máx. 4000, por defecto ""
slugstring—mín. 1, máx. 120
kindticket \sales \vehicle
is_fallbackboolean—por defecto false
directionsell_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
  }'

Ejemplo de respuesta (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"
  }
}

Responde: 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=&lt;pipeline id&gt; (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.

ParámetroEnTipoObligatorioRestricciones
idpathstringsímín. 1
reassign_toqueryuuidno
curl -X DELETE https://api.vitrinadev.com/api/v1/pipelines/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "tickets": 0,
    "leads": 13,
    "reassignedTo": "44444444-0000-4000-8000-000000000001"
  }
}

Responde: 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.

ParámetroEnTipoObligatorioRestricciones
idpathstringsímín. 1
curl https://api.vitrinadev.com/api/v1/pipelines/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (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
      }
    ]
  }
}

Responde: 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.

ParámetroEnTipoObligatorioRestricciones
idpathstringsímín. 1

Cuerpo

CampoTipoObligatorioRestricciones
namestring—mín. 1, máx. 120
descriptionstring—máx. 4000
slugstring—mín. 1, máx. 120
kindticket \sales \vehicle
is_fallbackboolean—
directionsell_to_customer \buy_from_customer—
default_team_idstring | 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 %."
  }'

Ejemplo de respuesta (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"
  }
}

Responde: 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": "&lt;key&gt;" \} (null un-places it); default_step_key is what the template would give it.

ParámetroEnTipoObligatorioRestricciones
idpathstringsímín. 1
curl https://api.vitrinadev.com/api/v1/pipelines/<id>/funnel-steps \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (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
      }
    ]
  }
}

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

En esta página