CRM y contactos
Crea contactos, mándales mensajes de WhatsApp, etiquétalos y recorre tu base.
El dominio contacts te da acceso programático al CRM del workspace: contactos, mensajes, notas y etiquetas. Necesita los scopes contacts:read y/o contacts:write.
Las rutas son planas (/contacts/…): operan sobre el workspace al que pertenece la key, así que no hace falta pasar workspaceId.
Crear un contacto
Requiere phone o email:
const contact = await met.contacts.create({
name: 'Ada Lovelace',
phone: '+573001112233',
});contact = met.contacts.create(name="Ada Lovelace", phone="+573001112233")curl -X POST https://api.met.meteor.com.co/api/v1/contacts \
-H "Authorization: Bearer $MET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Ada Lovelace","phone":"+573001112233"}'Mandar un mensaje
await met.contacts.sendMessage(contact.id, '¡Hola! Te escribo desde la API 👋');met.contacts.send_message(contact["id"], "¡Hola! Te escribo desde la API 👋")Para plantillas de WhatsApp aprobadas:
await met.contacts.sendTemplate(contact.id, { /* ... */ });
Etiquetas y notas
await met.contacts.addTag(contact.id, 'lead-caliente');
await met.contacts.createNote(contact.id, 'Pidió una demo para el viernes.');
const tags = await met.contacts.tags(contact.id);met.contacts.add_tag(contact["id"], "lead-caliente")
met.contacts.create_note(contact["id"], "Pidió una demo para el viernes.")
tags = met.contacts.tags(contact["id"])Campos del contacto: $ para los de sistema, pelado para los tuyos
Esta convención es la fuente de la mayoría de los errores al escribir datos, así que vale la pena tenerla clara:
- Campos de sistema → van con
$:$name,$email,$phone,$status,$assigned_user_id,$tags,$last_message_at. - Campos personalizados (los que define el workspace) → van con su
field_keypelado, sin$:etapa,monto,empresa.
Escribir $etapa en un campo personalizado no falla: guarda una clave distinta que ninguna vista lee. El campo queda invisible en la app.
Para descubrir los campos personalizados de un workspace y sus valores válidos:
const defs = await met.variables.fieldDefinitions();
// [{ field_key: 'etapa', type: 'select', options: ['Nuevo', 'Calificado', …] }, …]
En los select hay que mandar exactamente una de sus options. En los currency, options trae un solo elemento con el código ISO de la moneda (['COP']) y el valor del contacto es solo el número.
El embudo comercial
Todo workspace estrena tres campos personalizados —etapa, monto y fecha_cierre_estimada— y una vista "Pipeline". No hay una entidad "oportunidad" aparte: el pipeline son campos del contacto, así que se leen y escriben como cualquier otro campo.
await met.contacts.update(contact.id, { data: { etapa: 'Negociación', monto: 6300000 } });
const summary = await met.contacts.pipelineSummary();
// { stages: [{ stage: 'Negociación', contacts: 12, amount: 45000000 }, …] }
pipelineSummary() agrega en la base sobre todos tus contactos, no sobre una página. Los contactos sin etapa se agrupan en __no_value__. Si tu workspace renombró esos campos, pásalos: pipelineSummary({ stage_field: 'fase', amount_field: 'valor' }).
Recorrer la base
Usa iterate() para recorrer todos los contactos sin manejar cursores a mano:
for await (const c of met.contacts.iterate({ limit: 100 })) {
console.log(c.id, c.name ?? c.phone);
}
Por debajo pide order=id y encadena starting_after. Ese detalle importa si paginas a mano: el orden por defecto de list() es por recencia de la conversación y se reordena con cada mensaje entrante, así que un barrido sobre él se saltea contactos. Para recorrer todo sin perder nada, pide order=id desde la primera página y usa starting_after con el id del último contacto recibido.
Para una sola página, list() acepta filtros del CRM (estado, canal, búsqueda) y devuelve { data, total, has_more }.
Leer conversaciones
const page = await met.contacts.messages(contact.id, { limit: 50 });
WhatsApp: más que texto
sendMessage manda texto por el canal activo del contacto y alcanza para casi todo. Cuando necesitas que la persona elija en vez de escribir, WhatsApp tiene formatos propios, y van por el canal —no por el contacto— porque el formato depende de qué permite ese canal:
await met.channels.whatsapp.sendButtons(channelId, {
contact_id: contact.id,
body_text: '¿Confirmamos la visita del jueves a las 3?',
buttons: [
{ id: 'si', title: 'Sí, confirmo' },
{ id: 'reagendar', title: 'Reagendar' },
],
});
Hasta 3 botones; con más opciones, una lista:
await met.channels.whatsapp.sendList(channelId, {
contact_id: contact.id,
body_text: 'Elige un horario',
button_text: 'Ver horarios',
sections: [{ title: 'Jueves', rows: [
{ id: 'j-9', title: '9:00 a. m.' },
{ id: 'j-15', title: '3:00 p. m.' },
] }],
});
El id de cada botón o fila es tuyo: es lo que te llega de vuelta cuando la persona elige, así que ponle algo que puedas interpretar sin una tabla aparte. Lo que vuelve entra como mensaje del contacto y dispara los mismos eventos que un mensaje escrito.
Los otros formatos son directos: sendReply cita un mensaje anterior (wa_message_id), sendReaction le pone un emoji, sendLocation manda coordenadas con nombre y dirección, y sendContactCard comparte una ficha de contacto.
Uno importante aparte: sendTemplate. Fuera de la ventana de 24 horas desde el último mensaje de la persona, WhatsApp solo deja escribir con una plantilla aprobada por Meta — cualquier otro envío falla. No es un límite de Meteor y no hay forma de saltearlo: si tu integración escribe a contactos que no acaban de responder, la plantilla es el camino, no la excepción.
Difusiones
Una difusión es un envío a un segmento, no a un contacto. Vale la pena antes de mandar:
const { count } = await met.broadcasts.previewCount([
{ field: 'estado', operator: 'eq', value: 'cliente' },
]);
previewCount resuelve el segmento y te dice a cuánta gente le va a llegar, sin enviar nada. Úsalo siempre: es la diferencia entre descubrir que el filtro estaba mal ahora o después de escribirle a toda la base.
Lo que se difunde es un flujo, no un texto:
const b = await met.broadcasts.create({
name: 'Promo julio',
flow_id: 'flw_123',
filters: [{ field: 'estado', operator: 'eq', value: 'cliente' }],
send_mode: 'scheduled',
scheduled_at: '2026-08-01T14:00:00Z',
throttle_per_minute: 60,
});
flow_id es obligatorio y es la diferencia de diseño que conviene entender: una difusión no manda un mensaje suelto, arranca un flujo por cada contacto del segmento. Por eso puede responder, ramificar según lo que contesten y encadenar pasos — cosas que un envío plano no puede.
throttle_per_minute existe porque mandar todo de golpe es la forma más rápida de que el proveedor te limite. Y send_mode puede ser now, scheduled, manual o recurring; en scheduled necesitas scheduled_at.
Programada se puede cancel() mientras no haya salido; ya enviada, no — cancel detiene lo pendiente, no deshace lo entregado. Y como una difusión sale fuera de la ventana de 24 horas por definición, aplica lo de arriba: el primer mensaje del flujo va con plantilla aprobada.
Los contactos operan sobre el workspace de la key. Una key de partner puede operar varios workspaces de sus clientes; en ese caso, mira la API de Partners.