VitrinaAPI

Armar la lista de a quién escribirle

Busca, filtra y etiqueta contactos para armar una lista exportable

Antes de escribirle a alguien hay que decidir a quién. Esta receta arma esa lista con los filtros del directorio de Contactos: etapa, canal, origen del lead, empresa o etiqueta. Funciona igual sobre diez contactos que sobre diez mil. Al final tienes un grupo exportable. Listo para trabajar fuera de Vitrina o para entregarlo a una campaña.

Trampa

Un contacto sin nombre no es un contacto sin dato: revisa named antes de saludar a nadie

display_name nunca viene vacío. Sin un nombre cargado, el campo se completa con el teléfono, el correo o el identificador del canal. named pasa a false en ese caso. Arma el mensaje con display_name sin mirar named y terminas saludando a alguien por su propio número de teléfono.

Antes de empezar

  • contacts:read para buscar, contar y exportar.
  • contacts:write para etiquetar en bloque y escribir atributos.
  • El resto de los campos del directorio está en Contactos; esta receta cubre solo la parte de segmentación.

1. Mira el panorama antes de filtrar

GET /contacts/stats no toma parámetros y responde rápido, así que sirve para calibrar el resto de la búsqueda antes de escribir un solo filtro:

curl https://api.vitrinadev.com/api/v1/contacts/stats \
  -H "Authorization: Bearer $VITRINA_KEY"
{
  "data": {
    "total": 69,
    "by_lifecycle": { "unknown": 62, "customer": 6, "prospect": 1 },
    "by_channel": { "instagram": 8, "website": 3, "whatsapp": 2 },
    "by_lead_source": { "manual": 9, "website": 8, "conversation": 3, "marketplace": 2 },
    "email_reachable": 31,
    "phone_reachable": 46,
    "duplicate_candidates": 6
  }
}

by_lifecycle y by_channel adelantan qué filtros de search van a devolver algo. Pedir channel=email en este workspace es pedir cero resultados, y la respuesta ya lo dice sin gastar una llamada de búsqueda. duplicate_candidates cuenta los pares que el workspace marcó como posible duplicado. Se revisan y se fusionan a mano desde Contactos; esta receta solo avisa que existen.

2. Busca y filtra

curl "https://api.vitrinadev.com/api/v1/contacts/search?lifecycle_stage=prospect,qualified_prospect&limit=25" \
  -H "Authorization: Bearer $VITRINA_KEY"
{
  "data": [
    {
      "id": "01a0c849-57c3-732e-9096-3457b9f916b2",
      "name": "María González",
      "email": "[email protected]",
      "phone": "+56912345678",
      "lifecycle_stage": "prospect",
      "channels": [],
      "lead_sources": [],
      "display_name": "María González",
      "named": true
    }
  ],
  "meta": { "total": 1, "limit": 25, "offset": 0 }
}

q busca a la vez en nombre, correo, teléfono y external_id. No distingue mayúsculas ni tildes. lifecycle_stage acepta una lista separada por comas, como en el ejemplo. Así se arma el bucket «Prospectos» de la aplicación en una sola llamada. channel, lead_source, tag_id y company_id se combinan entre sí, y todos son opcionales.

meta.total es el número real de coincidencias, no el tamaño de la página. Úsalo para decidir cuántas páginas faltan.

3. Recorre toda la lista, no solo la primera página

curl "https://api.vitrinadev.com/api/v1/contacts/search?limit=2&offset=2" \
  -H "Authorization: Bearer $VITRINA_KEY"
{
  "data": [
    { "id": "01a0ce15-c110-7264-931a-8efd42702e04", "display_name": "Lucía Herrera", "named": true },
    { "id": "01a0ce14-39dd-7fc8-8c45-b3b1721a78c2", "display_name": "Rodrigo Paredes", "named": true }
  ],
  "meta": { "total": 69, "limit": 2, "offset": 2 }
}

La paginación es por offset, nunca por cursor. Trae veinticinco contactos por página por defecto, hasta mil por llamada. Sube offset en pasos de limit. Para cuando data llegue vacío, o cuando lo recorrido alcance meta.total.

4. Suma lo que tu workspace ya sabe de cada uno

Cada workspace puede definir sus propios campos sobre un contacto, además del nombre y el teléfono. Se leen con una sola llamada, ya fusionados con su definición:

curl https://api.vitrinadev.com/api/v1/contacts/01a0cc9b-e6c6-77ce-90d3-5c1b338443a4/attributes \
  -H "Authorization: Bearer $VITRINA_KEY"
{
  "data": [
    {
      "key": "presupuesto",
      "value": "12000000",
      "source": "admin",
      "label": "Presupuesto",
      "data_type": "number",
      "required": false
    }
  ]
}

Un campo definido pero nunca cargado en ESTE contacto también aparece en la lista, con id y value en null. Es la señal para dibujar un campo vacío en vez de omitirlo. Definir campos nuevos se hace en Contactos; aquí solo se leen.

5. Etiqueta en bloque a los que coinciden

curl -X POST https://api.vitrinadev.com/api/v1/contacts/tags/bulk \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_ids": ["01a0cc9b-e6c6-77ce-90d3-5c1b338443a4", "01a0cbfe-0e3a-7960-9901-ad153b1db135"],
    "tag": { "name": "recetas-demo" }
  }'
{ "data": { "tagged": 2, "tag_id": "68d3b69b-4050-4a6e-8d26-ef802199e8d7" } }

Manda tag.tag_id para una etiqueta existente o tag.name para resolverla o crearla por su slug, nunca ambos. tagged cuenta los contactos que de verdad quedaron etiquetados. Repetir la misma llamada sobre los mismos dos contactos responde { "tagged": 0, "tag_id": "..." }, porque ninguno de los dos estaba sin la etiqueta. Con tag_id a mano, el filtro tag_id= de search confirma quién quedó dentro del grupo.

6. Saca el archivo

curl "https://api.vitrinadev.com/api/v1/contacts/export?lifecycle_stage=prospect" \
  -H "Authorization: Bearer $VITRINA_KEY"
name,email,phone,external_id,country,language,city,job_title,brand,lifecycle_stage,created_at
María González,[email protected],+56912345678,,,,,Gerente de operaciones,,prospect,2026-09-22T08:44:04.923+00:00

La respuesta llega como text/csv y acepta los mismos filtros q y lifecycle_stage que la búsqueda. Va paginado por dentro con un cursor propio. No hay un limit que se te pueda quedar corto: un directorio de cien mil contactos sale completo en una sola llamada. Las columnas son las mismas que acepta POST /contacts/import, para que un archivo exportado se pueda editar y volver a subir sin transformarlo.

En la aplicación: el mismo directorio, con filtros y etiquetado en bloque desde la interfaz, vive en Contactos. Guía completa en Manual de plataforma → Armar la lista de a quién escribirle.

Cuando falla

Un lifecycle_stage que no está en la lista fija responde 400, con el valor recibido en el mensaje:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": {
      "query": [{ "path": "lifecycle_stage.0", "message": "Invalid enum value. Expected 'unknown' | 'prospect' | 'qualified_prospect' | 'customer' | 'repeat_customer' | 'inactive' | 'blocked', received 'interesado'" }]
    }
  }
}

Valida los tokens contra esa lista antes de armar la consulta. Un valor mal escrito no encuentra cero contactos: corta la llamada entera.

La lista es tuya; la campaña se arma en otra parte

Búsqueda, etiquetado y exportación son operaciones publicadas; audiencias y campañas no lo son, así que esta receta no termina con un envío. El archivo del paso 6 y la etiqueta del paso 5 son el punto de partida. El siguiente paso es abrir Campañas y construir el envío ahí. Guía completa en Manual de plataforma → Enviar una campaña.

La lista de campañas de Vitrina, con el estado de cada una y sus destinatarios, enviados, leídos y respuestas

En esta página