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:readpara buscar, contar y exportar.contacts:writepara 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:00La 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.
