# CRM y contactos

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`:

```ts
const contact = await met.contacts.create({
  name: 'Ada Lovelace',
  phone: '+573001112233',
});
```
```python
contact = met.contacts.create(name="Ada Lovelace", phone="+573001112233")
```
```bash
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

```ts
await met.contacts.sendMessage(contact.id, '¡Hola! Te escribo desde la API 👋');
```
```python
met.contacts.send_message(contact["id"], "¡Hola! Te escribo desde la API 👋")
```

Para plantillas de WhatsApp aprobadas:

```ts
await met.contacts.sendTemplate(contact.id, { /* ... */ });
```

## Etiquetas y notas

```ts
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);
```
```python
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_key` **pelado**, 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:

```ts
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.

```ts
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:

```ts
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

```ts
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:

```ts
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:

```ts
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](eventos.html) 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:

```ts
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:

```ts
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](funciones-y-flujos.html) 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](../partners.html).
