Fuente: https://crm.begraffic.com/docs#introduccion
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# Documentación de la API de Be CRM

Bienvenido a la referencia de la API REST de Be CRM. Con ella puedes crear y actualizar leads, enviar correos, enviar y recibir WhatsApp, SMS y Telegram, dar de baja contactos y verificar tus integraciones desde tus propios sistemas.

> **BeCRM Connect** — añade un botón «Conectar con BeCRM» para que tus clientes inicien sesión, seleccionen su workspace y autoricen los permisos. Sigue la [guía de integración para plataformas](https://crm.begraffic.com/docs#becrm-connect): registro, botón, callback, renovación y desconexión.

> **Mensajería** — consulta las guías de [WhatsApp](https://crm.begraffic.com/docs#whatsapp-sms), [SMS](https://crm.begraffic.com/docs#sms) y [Telegram](https://crm.begraffic.com/docs#telegram): configuración de canales, ventana de 24 horas, plantillas, botones, enlaces, envío y recepción mediante webhooks. Estas funciones usan la API v1 y requieren una integración activa en tu workspace.

> **Nueva: API v1** — claves con alcances y caducidad, contactos, correo, negocios, actividades y tareas, conversaciones, tickets, reuniones y webhooks, con especificación OpenAPI. Documentación en [/docs/api](https://crm.begraffic.com/docs/api). Las rutas clásicas de leads y correos siguen funcionando con las claves `bmkt_...`.

## URL base

Todas las peticiones se hacen sobre HTTPS contra:

```
https://crm.begraffic.com
```

## Formato

- Las peticiones y respuestas usan **JSON** (`Content-Type: application/json`).
- Cada token está asociado a un **workspace** (tu `tableId`), así que solo operas sobre los datos de tu propio workspace.
- Las respuestas devuelven códigos HTTP estándar: `2xx` éxito, `400` body inválido, `401/403` autenticación, `404` no encontrado, `405` método no permitido.

## Recursos

| Recurso | Para qué sirve |
| --- | --- |
| **[BeCRM Connect](https://crm.begraffic.com/docs#becrm-connect)** | Conectar aplicaciones Be o plataformas externas mediante login y selección de workspace. |
| **Leads** | Crear, buscar y actualizar contactos, sus listas y organización. |
| **Correos** | Enviar correos personalizados o por plantilla desde tu remitente verificado. |
| **[WhatsApp](https://crm.begraffic.com/docs#whatsapp-sms)** | Mensajes, ventana de respuesta, plantillas y mensajes interactivos. |
| **[SMS](https://crm.begraffic.com/docs#sms)** | Texto, números de origen, saldo y recepción bidireccional. |
| **[Telegram](https://crm.begraffic.com/docs#telegram)** | Bot del workspace, conversaciones, respuestas y eventos de entrada. |
| **Baja (unsubscribe)** | Dar de baja a un contacto desde un enlace público. |
| **Webhook de verificación** | Confirmar que una integración externa quedó conectada. |

---

Fuente: https://crm.begraffic.com/docs#autenticacion
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# Autenticación

Todas las peticiones autenticadas requieren el encabezado:

```
Authorization: Bearer <token>
```

## Conectar una plataforma con login

Para aplicaciones Be y clientes externos, usa [BeCRM Connect](https://crm.begraffic.com/docs#becrm-connect). El usuario inicia sesión, selecciona un workspace y autoriza los permisos; tu servidor recibe tokens de acceso y renovación mediante OAuth con PKCE. El token de acceso se utiliza exclusivamente en la **API v1** con el mismo encabezado Bearer.

También puedes generar una clave v1 manual en Ajustes → API. Las claves v1 tienen formato `bcrm_...`, permisos y caducidad. Consulta la [referencia v1](https://crm.begraffic.com/docs/api).

## Tokens del contrato clásico

Las rutas clásicas, como `/api/leads` y `/api/emails/sendEmail`, conservan su autenticación existente. Los tokens de BeCRM Connect y las claves v1 no sirven en estas rutas.

1. **Token de API clásico**: se genera en la consola y tiene el formato `bmkt_...`. El servidor guarda solo un hash SHA‑256, por lo que el secreto nunca se almacena ni se expone. Cópialo en el momento de crearlo — es la única vez que lo verás completo.
2. **ID token de sesión**: el token de un usuario autenticado.

En ambos casos se valida que el token esté **activo** y asociado a un **workspace** (`tableId`). Si el token no existe o está deshabilitado, recibirás `401 Unauthorized`.

## Cómo crear un token de API

1. Entra a tu consola de Be CRM.
2. Ve a **Ajustes → API keys**.
3. Crea una nueva clave, asígnale un nombre y **cópiala** (solo se muestra una vez).
4. Úsala en el encabezado `Authorization: Bearer bmkt_...`.

> Trata tus tokens como contraseñas: no los publiques en el frontend ni los subas a un repositorio. Si se filtra uno, elimínalo desde la consola y genera otro.

```bash
curl https://crm.begraffic.com/api/leads?email=lead@ejemplo.com \
  -H "Authorization: Bearer bmkt_tu_token"
```

---

Fuente: https://crm.begraffic.com/docs#becrm-connect
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# BeCRM Connect

Conecta un workspace con un botón, sin pedir al usuario una clave de API.
Compatible con aplicaciones Be y clientes externos con servidor propio (Node.js 22 u otro lenguaje).

Para presentar la integración, utiliza los [recursos de marca y el botón de Be CRM](https://crm.begraffic.com/docs#recursos-marca): logotipos, símbolos, colores, CSS y kit descargable.

## Qué debe implementar tu plataforma

**Experiencia recomendada: ventana emergente de autorización**, similar al inicio de sesión con Google.
La plataforma del cliente abre la ventana; BeCRM muestra dentro el login, los workspaces y los permisos.
La página original queda abierta y consulta el estado de la integración cuando termina el flujo.
Consulta el [ejemplo completo de botón con ventana emergente](https://crm.begraffic.com/docs#botón-con-ventana-emergente).

El recorrido del cliente es: **Conectar con BeCRM → iniciar sesión → elegir workspace → autorizar → conectado**.
BeCRM proporciona el login, la selección y la autorización. Tu plataforma implementa:

| Pieza | Responsabilidad de tu plataforma |
| --- | --- |
| Botón de conexión | Abrir la ruta de inicio en popup o página completa. |
| Ruta de inicio | Comprobar al administrador local y guardar la transacción con state y PKCE. |
| Callback registrado | Validar y consumir la transacción, canjear el código y guardar los tokens cifrados. |
| Configuración | Asociar el workspace recibido a la organización local y configurar los webhooks necesarios. |
| Renovación y desconexión | Rotar credenciales desde el servidor y permitir revocar la conexión. |

Necesitas un backend y una sesión local autenticada. Cada plataforma registra su propia aplicación;
no comparte su secreto de cliente con otras plataformas ni con el navegador.
Puedes usar el [SDK de servidor Node.js](https://crm.begraffic.com/sdk/becrm-connect.mjs) y el
[helper del botón y popup](https://crm.begraffic.com/sdk/becrm-connect-browser.mjs), o implementar el mismo protocolo en otro lenguaje.
Los ejemplos siguientes usan almacenes de sesiones e integraciones ilustrativos: adapta esas funciones a tu backend.

## 1. Registra tu aplicación

En BeCRM: [Ajustes → API → Aplicaciones conectadas](https://crm.begraffic.com/dashboard/settings/connected-apps) → Registrar aplicación.
Necesitas ser propietario o administrador del workspace que registra la aplicación.
Los usuarios de la aplicación podrán autorizar OTROS workspaces que administren.

Indica nombre, sitio web HTTPS, direcciones de retorno exactas y los permisos máximos que podrá solicitar.
Guarda `client_id` y `client_secret` en el gestor de secretos de tu servidor. El secreto solo se muestra una vez.
Abre la tarjeta de la aplicación para consultar su client ID, direcciones de retorno y permisos. En el detalle, pulsa «Editar configuración» para cambiar nombre, sitio web, direcciones de retorno y permisos autorizables. Se conservan client ID y secreto. Los permisos editados se aplican a nuevas autorizaciones: las conexiones existentes mantienen lo aprobado y añadir permisos requiere una nueva autorización del cliente. Una dirección de retorno retirada ya no acepta nuevas autorizaciones ni canjes de códigos pendientes. El secreto solo puede verse tras crearlo o regenerarlo. Si lo pierdes, usa «Regenerar secreto» en el detalle: se conserva el client ID y la configuración, pero debes actualizar el nuevo secreto en tu servidor para canjear códigos y renovar tokens. Una aplicación desactivada deja de funcionar inmediatamente.
El registro es autoservicio: el nombre no representa una verificación de Be Graffic.
La pantalla muestra el dominio del sitio y el de retorno para que el usuario los reconozca.

Las direcciones de retorno deben usar HTTPS; HTTP se permite exclusivamente en localhost, 127.0.0.1 o ::1.
No se permiten comodines, fragmentos ni credenciales en la URL. Incluye puerto, ruta y consulta exactos.
No alojes un redireccionador abierto en la dirección de retorno.

## 2. Abre el flujo

Desde un botón «Conectar con BeCRM», abre en popup o redirige a una ruta de TU servidor.
Esa ruta verifica la sesión y el permiso de administrador EN TU aplicación antes de iniciar la conexión.
Genera `state` impredecible y PKCE S256 en el servidor. Guarda la transacción durante 10 minutos,
ligada al usuario, la sesión y la organización local que se conectará (almacenamiento de servidor o cookie autenticada HttpOnly).
No aceptes la organización local desde los parámetros del callback.

Ejemplo usando el SDK de servidor, disponible en `/sdk/becrm-connect.mjs`:

```js
import { createBeCRMConnect } from './becrm-connect.mjs';
const becrm = createBeCRMConnect({
  issuer: 'https://crm.begraffic.com',
  clientId: process.env.BECRM_CLIENT_ID,
  clientSecret: process.env.BECRM_CLIENT_SECRET,
  redirectUri: 'https://tu-app.com/api/becrm/callback',
});

// GET /api/becrm/start (sesión autenticada y administrador local)
const { url, transaction } = becrm.begin([
  'conversations:read', 'conversations:write', 'webhooks:manage',
]);
// Implementa tu almacén: token de sesión HttpOnly, Secure, SameSite=Lax.
await sessions.savePending(currentSessionId, {
  ...transaction, localOrganizationId: authorizedLocalOrganizationId,
});
return Response.redirect(url, 302);
```

BeCRM reutiliza la sesión actual; si no hay sesión, muestra el login habitual, incluyendo MFA.
El correo debe estar verificado. Solo aparecen workspaces con acceso completo de propietario/administrador.
El usuario selecciona el workspace y acepta o cancela los permisos. La selección no cambia su workspace activo en el CRM.

URL del flujo: `/connect` con `response_type=code`, `client_id`, `redirect_uri`, `scope`
(permisos separados por espacios), `state`, `code_challenge`, `code_challenge_method=S256`.
`state` debe tener entre 16 y 512 caracteres. PKCE y secreto de cliente son obligatorios.

## 3. Completa la conexión en el servidor

El callback recibe `code` y `state`, o `error=access_denied` y `state` si el usuario cancela.
Verifica la sesión local, vuelve a comprobar el permiso de administrador local y consume atómicamente
la transacción guardada. Compara `state` antes de llamar a BeCRM. No registres la URL del callback ni tokens en logs.

```js
// GET /api/becrm/callback
const pending = await sessions.takePending(currentSessionId); // atómico, de un solo uso
await assertLocalAdmin(currentUser, pending.localOrganizationId);
const tokens = await becrm.complete(request.url, pending);
// Cifra los tokens y vincúlalos SOLO a la organización guardada en pending.
await integrations.saveEncrypted(pending.localOrganizationId, {
  ...tokens,
  accessExpiresAt: Date.now() + tokens.expires_in * 1000,
});
// Configura aquí los webhooks que tu app necesita usando la API v1.
// Si falla, permite reintentarlo y muestra "configuración pendiente", no "conectado".
return Response.redirect('https://tu-app.com/configuracion/integraciones', 303);
```

`POST /api/oauth/token` admite JSON o formulario URL encoded:

```json
{
  "grant_type": "authorization_code",
  "client_id": "...",
  "client_secret": "...",
  "code": "...",
  "redirect_uri": "https://tu-app.com/api/becrm/callback",
  "code_verifier": "..."
}
```

El código dura 5 minutos, solo se usa una vez y está ligado a cliente, retorno, PKCE y workspace.
Respuesta: `access_token`, `token_type=Bearer`, `expires_in=3600`, `refresh_token`,
`refresh_expires_in`, `scope`, `workspace_id`, `connection_id`.
El servidor de la app guarda todo cifrado; ningún token ni secreto llega al navegador.
Los canjes y las renovaciones son entre servidores. No hay CORS para aplicaciones sin backend.

## Importación inicial opcional y sincronización manual

Después de guardar la conexión puedes ofrecer «Importar historial al conectar» y «Sincronizar ahora».
La [guía de sincronización del historial](https://crm.begraffic.com/docs#historial-workspace) incluye SDK, paginación completa de
correo/WhatsApp/SMS/Telegram, reanudación y guardado sin duplicados. No se ejecuta automáticamente al autorizar.
Solicita `emails:read` para correo y `conversations:read` para los demás mensajes; sin permisos de envío.

## 4. Usa la API y renueva

```js
const response = await fetch('https://crm.begraffic.com/api/v1/me', {
  headers: { Authorization: `Bearer ${stored.access_token}` },
});
```

La respuesta de `/api/v1/me` incluye `data.integrations`: correo verificado, números SMS activos, cuentas WhatsApp y bot Telegram, disponibilidad del plan, permisos de envío y enlaces de configuración. Consulta la [guía de estado de canales](https://crm.begraffic.com/docs#estado-canales) para mostrarlo en el panel de tu plataforma y mantenerlo actualizado.

Estos tokens sirven en `/api/v1/*`, conservando permisos, cuotas, límites del plan y rate limiting.
No sirven en los endpoints legacy `/api/emails/sendEmail` y `/api/leads`;
los consumidores existentes mantienen sus claves actuales. Para conectar mediante OAuth, usa los equivalentes v1
(consulta la [referencia API v1](https://crm.begraffic.com/docs/api)).

Renueva antes de una hora mediante `becrm.refresh(stored.refresh_token)` o:

```json
{
  "grant_type": "refresh_token",
  "client_id": "...",
  "client_secret": "...",
  "refresh_token": "..."
}
```

Cada renovación consume el refresh token anterior e invalida el access token anterior.
La idempotencia y el límite de llamadas de la API v1 mantienen el mismo ámbito de conexión después de renovar.
Serializa las renovaciones por conexión (bloqueo distribuido) y guarda ambos tokens nuevos atómicamente.
Ante `invalid_grant`, solicita reconectar; no repitas indefinidamente un token consumido.
Si se pierde la respuesta del canje o de una renovación, reconecta: no se conserva una copia recuperable del secreto.
El permiso dura 90 días desde la autorización, sin extensión automática; después hay que reconectar.
Los tokens se guardan en BeCRM solo como hashes.

## 5. Desconecta

Desde tu aplicación: elimina los webhooks que hayas registrado mientras el token siga activo,
llama a `becrm.revoke(stored.refresh_token)` y borra tus credenciales cifradas.
La revocación admite token de acceso o refresh token actual.
`POST /api/oauth/revoke` recibe `client_id`, `client_secret`, `token` y opcionalmente `token_type_hint`.
Los tokens desconocidos devuelven éxito sin revelar otras conexiones.

Desde BeCRM: Ajustes → API → Aplicaciones conectadas → Desconectar.
La revocación bloquea acceso y renovación. Desactivar el registro de una aplicación bloquea todas sus conexiones.
Eliminar el workspace o retirar el rol de administrador del autorizador también bloquea su conexión.
Tras una desconexión iniciada en BeCRM, tu app debe manejar 401 y ofrecer «Volver a conectar».
Los webhooks creados con OAuth quedan ligados a la conexión y dejan de entregarse al revocarla,
al desactivar la aplicación o al expirar la autorización. Sus destinos permanecen visibles para el administrador
hasta que los elimine. Una conexión OAuth solo puede listar, editar, borrar y probar sus propios destinos.

## Direcciones del protocolo

Todas las rutas siguientes pertenecen a `https://crm.begraffic.com` en producción.

| Dirección | Uso |
| --- | --- |
| `GET /.well-known/oauth-authorization-server` | Descubrir endpoints, permisos y métodos admitidos. |
| `GET /connect` | Abrir el login y consentimiento con los parámetros del paso 2. |
| `POST /api/oauth/token` | Canjear códigos y renovar tokens desde tu servidor. |
| `POST /api/oauth/revoke` | Revocar credenciales desde tu servidor. |
| `GET /api/v1/me` | Consultar el workspace autorizado con el token de acceso. |

## Operación y despliegue de BeCRM

Esta sección corresponde al equipo que opera BeCRM; las plataformas consumidoras implementan los pasos anteriores.

Firestore: colecciones `oauthClients`, `oauthCodes`, `oauthConnections` y credenciales en `apiKeys`.
Acceso solo por Admin SDK; las reglas generales ya niegan lecturas/escrituras de cliente.
Consultas por un único campo: no se necesitan nuevos índices compuestos.
Configura TTL en `oauthCodes.expiresAt` para limpiar códigos abandonados (su caducidad se verifica incluso sin TTL).
No habilites TTL en `oauthConnections.expiresAt` sin una política de limpieza de tokens y webhooks.
La rotación crea una sola credencial vigente por conexión y elimina la anterior.
Los cambios de autorización quedan en la auditoría del workspace; los secretos no se registran.

Tu aplicación configura los webhooks, correspondencia de canales y sincronización que necesita después del callback.
BeCRM vincula automáticamente los destinos creados con estos tokens a su conexión y verifica su autorización
antes de cada intento de entrega. Una petición ya enviada antes de revocar no puede retirarse.

## Prueba de aceptación por aplicación

- Login nuevo, sesión existente y MFA; selección entre dos workspaces sin cambiar el workspace activo.
- Usuario no administrador, membresía retirada y correo sin verificar.
- Cancelar, retorno no registrado, scope no autorizado, state y PKCE alterados.
- Dos canjes del mismo código; renovación concurrente y uso del refresh token anterior.
- Llamada v1, cuota agotada y credencial caducada; no reintentar un 402 como error temporal.
- Desconexión en la app y en BeCRM; aplicación desactivada; webhook sin entrega después de revocar.

## Botón con ventana emergente

### Quién abre la ventana

Esto lo implementa la **plataforma que se conecta**, utilizando nuestro helper de navegador.
BeCRM no abre una segunda ventana desde su pantalla de autorización: recibe el flujo en la ventana
que abrió tu botón. Una pestaña con `target="_blank"` por sí sola no solicita una ventana emergente.
El helper llama a `window.open` con `popup,width=560,height=760` directamente desde el clic del usuario.
Así solicita una ventana independiente donde iniciar sesión mientras la plataforma original permanece abierta.

El navegador decide la presentación final: puede mostrar una ventana o una pestaña, especialmente en móvil,
y no se puede garantizar que permanezca siempre por encima de la ventana original.
Si bloquea la apertura, el helper continúa en la página completa.
Consulta la [referencia de window.open](https://developer.mozilla.org/en-US/docs/Web/API/Window/open).

### Pasos de implementación

1. Copia el helper de navegador en tu plataforma e impórtalo antes de que el usuario pulse el botón.
2. Llama a `connectBeCRM` directamente en el evento de clic. No hagas un `await fetch` ni una importación dinámica antes de abrir la ventana: puedes perder el gesto de usuario y bloquear la apertura.
3. La ventana abre `/api/becrm/start` en tu dominio. Esa ruta valida tu sesión, guarda state y PKCE, y redirige a BeCRM como en el paso 2 de esta guía.
4. BeCRM devuelve el resultado al callback registrado. Tu servidor valida la transacción, canjea el código y guarda los tokens cifrados como en el paso 3.
5. Tu callback redirige a una página de resultado de tu dominio, que comunica el resultado a la ventana original con `finishBeCRMConnect`.
6. La ventana original consulta tu backend y actualiza su panel. Un mensaje del popup nunca sustituye la comprobación del estado guardado.

### Botón en tu plataforma

Copia también `/sdk/becrm-connect-browser.mjs` en tu aplicación. Se usa directamente desde el clic:

```js
import { connectBeCRM } from './becrm-connect-browser.mjs';
async function onConnectClick() {
  try {
    await connectBeCRM({ startUrl: '/api/becrm/start' });
  } catch (error) {
    // Cancelación, cierre manual o tiempo agotado: conserva la opción de reintentar.
    showConnectionNotice(error.message);
  } finally {
    // Comprueba el estado guardado en TU servidor antes de mostrar "Conectado".
    // Así también detectas una conexión completada aunque se haya perdido el mensaje del popup.
    await reloadIntegrationStatus();
  }
}
// El usuario debe saber que se abrirá otra ventana.
document.querySelector('#connect-becrm').addEventListener('click', onConnectClick);
```

```html
<button id="connect-becrm" type="button">Conectar con Be CRM</button>
<p>Se abrirá una ventana para iniciar sesión y elegir tu workspace.</p>
```

`showConnectionNotice` y `reloadIntegrationStatus` son funciones de tu plataforma.
Bloquea el botón mientras haya una conexión en curso para evitar transacciones simultáneas en la misma sesión.

### Página de resultado y cierre

Si el navegador bloquea el popup, el helper usa redirección de página completa.
Tras guardar la integración, tu backend redirige a una página de éxito EN TU dominio
(en vez de regresar directamente a configuración). Esa página importa el helper y ejecuta:

```js
import { finishBeCRMConnect } from './becrm-connect-browser.mjs';
if (!finishBeCRMConnect(true)) {
  // Sin opener: muestra un enlace "Volver a integraciones" para el flujo de redirección.
}
```

La página de éxito debe requerir sesión y comprobar que la operación se completó en el servidor.
Tras una cancelación, consume la transacción y usa una página de resultado con `finishBeCRMConnect(false)`.
El mensaje solo transmite éxito o fallo; nunca tokens. El helper verifica dominio y ventana de origen.
Mantén el opener disponible durante el flujo (`Cross-Origin-Opener-Policy: same-origin-allow-popups`
si tu aplicación usa COOP); si el navegador lo separa, usa el enlace de vuelta y consulta el estado.

### Cancelación, cierre y políticas del navegador

- Si el usuario cancela en BeCRM, tu callback valida `state`, consume la transacción y devuelve un resultado cancelado. No canjea un código ni guarda tokens.
- Si cierra la ventana manualmente o pasan diez minutos, el helper rechaza la promesa. Consulta el estado en tu servidor y ofrece reintentar; cerrar la ventana no revoca una conexión que ya se haya guardado.
- Si no hay `window.opener`, la página de resultado debe mostrar un enlace a integraciones en lugar de dejar al usuario en una pantalla vacía.
- `postMessage` solo comunica un booleano. El helper acepta mensajes del mismo origen y de la ventana que abrió. No envíes códigos, tokens ni secretos por mensajes, URLs de resultado o almacenamiento del navegador.
- Si utilizas COOP, revisa las cabeceras de la página original, la ruta de inicio y la página de resultado. Una política que separe las ventanas puede romper `window.opener`; mantén disponible el flujo de página completa y la consulta de estado.

Comprueba en Chrome, Safari y Firefox: apertura desde el clic, login con y sin sesión, selección de workspace,
MFA, autorización, cancelación, cierre manual, popup bloqueado y regreso en móvil.

---

Fuente: https://crm.begraffic.com/docs#historial-workspace
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# Importar y sincronizar el historial del workspace

BeCRM permite recuperar a demanda los correos y los mensajes de WhatsApp, SMS y Telegram
almacenados en el workspace conectado. La plataforma consumidora decide cuándo hacerlo y dónde guardarlos.
No se inicia una importación al autorizar OAuth ni se programa una sincronización automática.

## Opciones que debe ofrecer tu plataforma

- **Importar historial al conectar**: opción desmarcada inicialmente. Tras guardar la conexión, crea un trabajo de importación solo si el administrador la seleccionó.
- **Sincronizar ahora**: crea un trabajo manual con los canales elegidos.
- **Reanudar importación**: continúa un trabajo interrumpido desde el último punto guardado.

Ofrece canales independientes: correo, WhatsApp, SMS y Telegram. Por defecto se recuperan **recibidos**;
permite elegir también enviados si el usuario quiere conservar el contexto completo.
Muestra progreso, mensajes guardados, errores y fecha de la última sincronización terminada.
Que un canal esté inactivo hoy no significa que no tenga historial: no omitas su historial por su estado actual.

## Permisos y límites

| Canal | Permiso | Lectura |
| --- | --- | --- |
| Correo compartido | `emails:read` | `GET /api/v1/emails/history` |
| WhatsApp, SMS y Telegram | `conversations:read` | `GET /api/v1/conversations?channel=…` y mensajes de cada conversación |

Registra estos permisos máximos en la aplicación y solicítalos al conectar. Si una conexión existente
no los autorizó, pide una nueva autorización: renovar el token no amplía sus permisos.
No se necesita permiso de envío para importar. Las lecturas no envían mensajes ni consumen cuotas de envío;
siguen los límites de llamadas y la restricción de exportación si se supera el límite de contactos (402).

Solo se exporta el contenido **compartido del workspace** autorizado. Los buzones privados y las notas internas
quedan excluidos. No se accede a otros workspaces ni se descargan mensajes que el proveedor nunca entregó al CRM.
Los chats se recuperan desde las conversaciones indexadas de la bandeja. El correo se lee directamente del
historial almacenado, incluyendo registros antiguos sin hilo o fecha.

El cuerpo de correo es el que está almacenado en el documento. `htmlAvailable` indica que existe HTML externo,
pero esta lectura no descarga ese HTML ni adjuntos. Los mensajes de chat conservan los campos y referencias
que entrega la API de mensajes; no descarga archivos binarios. Nunca renderices HTML importado sin sanitizarlo.

## Lectura directa de correo

```http
GET /api/v1/emails/history?direction=inbound&limit=100
Authorization: Bearer <access_token_del_servidor>
```

`direction`: `inbound` (por defecto), `outbound` o `all`.
La respuesta tiene `{ data: [...], nextCursor }`. Cada correo incluye `id`, `jobId`, `messageId`,
`threadId`, `direction`, `subject`, `body`, `at`, `from`, `to` y `htmlAvailable`.
Las fechas o identidades que no se guardaron originalmente pueden ser `null`.

La paginación recorre documentos por ID, **no por fecha**, para conservar correos antiguos sin timestamp.
`limit` limita los documentos examinados: una página puede estar vacía después de excluir registros privados
y aun tener `nextCursor`. Continúa hasta que sea `null`. Repite exactamente los mismos filtros con cada cursor.
Ordena por fecha en tu plataforma después de importar si lo necesitas.

## Helper de servidor: todos los canales, todas las páginas

Descarga [becrm-history.mjs](https://crm.begraffic.com/sdk/becrm-history.mjs) en tu backend (Node.js 22).
Este módulo independiente no necesita el secreto del cliente: recibe una función que obtiene el token
actual desde tu almacenamiento protegido y lo renueva, si es necesario, usando el [SDK de Connect](https://crm.begraffic.com/sdk/becrm-connect.mjs).
La renovación debe estar serializada por conexión y guardar ambos tokens nuevos de forma atómica.

```js
import { readBeCRMHistory } from './becrm-history.mjs';

// Worker de un trabajo creado por el administrador local.
await assertLocalAdmin(job.userId, job.organizationId);
const connection = await integrations.getForOrganization(job.organizationId);
await jobs.lock(job.id); // evita dos workers sobre el mismo trabajo/conexión
try {
  const pages = readBeCRMHistory({
    workspaceId: connection.workspaceId,
    getAccessToken: () => integrations.getValidAccessToken(connection.id),
    channels: job.channels, // ['email', 'whatsapp', 'sms', 'telegram']
    direction: 'inbound', // también 'outbound' o 'all'
    checkpoint: job.checkpoint ?? undefined,
    signal: workerAbortSignal,
  });
  for await (const page of pages) {
    await database.transaction(async (tx) => {
      if (page.conversation) {
        await history.upsertConversation(tx, job.organizationId, connection.id, page.conversation);
      }
      for (const item of page.items) {
        const sourceId = page.channel === 'email'
          ? (item.direction === 'outbound' && item.jobId ? `job:${item.jobId}` : item.id)
          : `${page.conversation.id}/${item.id}`;
        // Unicidad: organización local + conexión + workspace + canal + sourceId.
        await history.upsertMessage(tx, {
          organizationId: job.organizationId,
          connectionId: connection.id,
          workspaceId: page.workspaceId,
          channel: page.channel,
          sourceId,
          message: item,
        });
      }
      // Guarda el checkpoint DESPUÉS de guardar los mensajes, en la misma transacción.
      await jobs.saveProgress(tx, job.id, page.checkpoint, page.done);
    });
    if (workerTimeBudgetReached()) break; // otro worker reanuda con el checkpoint guardado
  }
} catch (error) {
  await jobs.pauseWithError(job.id, { code: error.code, status: error.status });
} finally {
  await jobs.unlock(job.id);
}
```

`jobs`, `database`, `history`, `integrations` y las funciones del worker son adaptadores que implementa tu plataforma.
El helper no guarda datos por ti. Comprueba el workspace del token con `/me` antes de importar, pagina cada
canal y cada conversación, y entrega puntos de reanudación ligados al workspace, canales y dirección.
No contiene tokens en sus páginas ni checkpoints. Guárdalos solo en el servidor: contienen metadatos de conversaciones.

Cada página entrega `{ workspaceId, channel, conversation, items, checkpoint, done }`.
Puede haber páginas vacías de progreso; guárdalas también. `done=true` indica que se recorrieron todos los canales.
La importación no se detiene a los 200 mensajes. Trabaja en lotes y reanuda para evitar el timeout del callback OAuth.
No esperes a que termine todo el historial para cerrar el popup: autoriza, guarda la conexión y crea el trabajo.

## Repeticiones y cambios durante la importación

Reanudar utiliza el checkpoint del mismo trabajo. Una nueva sincronización manual empieza sin checkpoint
y vuelve a recorrer el historial con **upsert**, para recoger mensajes y estados nuevos sin duplicar los anteriores.
No hay una instantánea congelada: las conversaciones pueden cambiar de orden mientras llegan mensajes.
Una pasada puede necesitar otra sincronización para recoger cambios concurrentes. No borres datos locales porque
no aparezcan en una página, ni marques como no leídos todos los mensajes históricos ni dispares notificaciones al importarlos.

No retransmitas los mensajes importados al CRM: esto es lectura, no reenvío ni restauración.
La sincronización por webhooks en tiempo real es una opción distinta; no la actives por elegir una importación inicial.

## Errores y pruebas de aceptación

- `401`: reconectar o renovar según el estado de la conexión; no repetir indefinidamente el token rechazado.
- `403`: permisos insuficientes o acceso retirado. Revisa los permisos autorizados.
- `402`: resolver el bloqueo de exportación; reintentar no lo corrige.
- `429`: respetar `retry-after` y reanudar desde el último checkpoint confirmado.
- Timeout o caída del worker: conserva lo guardado y reanuda; si repites la última página, el upsert evita duplicados.
- Un cursor inválido o una conversación eliminada durante el trabajo puede requerir reiniciar la pasada sin checkpoint.

Prueba más de 200 mensajes, páginas vacías con cursor, caída a mitad de una conversación, repetición completa,
cuatro canales, correos sin hilo/fecha, buzones privados, token de otro workspace, permisos insuficientes y cancelación del trabajo.

---

Fuente: https://crm.begraffic.com/docs#estado-canales
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# Estado de los canales del workspace

Después de conectar una plataforma, consulta **`GET /api/v1/me` desde tu servidor** con el token de acceso de BeCRM Connect o una clave API v1. La respuesta conserva `data.key` y `data.workspace` y añade `data.integrations` con el estado de **correo, SMS, WhatsApp y Telegram**. No necesitas solicitar permisos adicionales para leer este resumen: funciona con cualquier alcance válido.

```bash
curl https://crm.begraffic.com/api/v1/me \
  -H "Authorization: Bearer <TOKEN_DE_ACCESO>"
```

## Qué mostrar en tu panel

| Campo de cada canal | Significado |
| --- | --- |
| `available` | El plan del workspace incluye este canal. |
| `configured` | BeCRM tiene una configuración utilizable: identidad verificada, número activo o bot conectado. |
| `active` | El canal está configurado y disponible según el plan. |
| `status` | `active`, `not_configured`, `configuration_pending` o `plan_required`. |
| `reason` | Motivo de configuración o plan cuando el canal no está activo. |
| `apiCanSend` | El canal está activo y la integración tiene el permiso de envío. Cuotas y proveedor se comprueban al enviar. |
| `canReceive` | Recepción configurada: `true` o `false`; `null` significa sin confirmar. |
| `configurationUrl` | Enlace a los ajustes correspondientes de BeCRM para completar la configuración. |

El resumen incluye `checkedAt` y `source: "crm_configuration"`. Es una foto de la configuración guardada en el CRM; no hace llamadas de comprobación a los proveedores. Un canal activo no garantiza entrega, saldo, cuota, disponibilidad del proveedor ni que un token haya sido revocado fuera del CRM. La petición de envío sigue validando esas condiciones.

## Información específica

- **Correo (`email`)**: `verified` indica que existe al menos un remitente o dominio verificado; `identitiesCount`, `verifiedSendersCount` y `verifiedDomainsCount` permiten distinguir verificaciones pendientes. La verificación corresponde al envío del workspace, no al correo de inicio de sesión del usuario. `canReceive` queda sin confirmar. Para que la persona elija el remitente, `GET /api/v1/emails/senders` (alcance `emails:send`) lista los correos (`id` es el `senderId` de `POST /api/v1/emails`) y los dominios, con `verified`; con un dominio verificado vale `from` con cualquier dirección suya.
- **SMS (`sms`)**: `numbersCount` y `activeNumbersCount` muestran los números vinculados y activos. La recepción utiliza el dato guardado `twoWayEnabled`: si hay un número activo con recepción habilitada es `true`; si hay datos sin confirmar es `null`; si ninguno admite recepción es `false`.
- **WhatsApp (`whatsapp`)**: `accountsCount` cuenta las WABA distintas identificadas y `numbersCount` todos los números conectados, incluidas varias cuentas; `activeNumbersCount` excluye números sin configuración utilizable o con token caducado. La recepción queda sin confirmar: compruébala antes de ofrecerla al cliente.
- **Telegram (`telegram`)**: `botConnected` indica una credencial utilizable guardada. `webhookRegistered` informa el registro del webhook conocido en BeCRM; `null` indica dato sin confirmar. La API v1 permite responder a conversaciones existentes de Telegram; la ruta `/messaging/messages` también envía texto a una conversación existente de Telegram mediante `conversationId`. Consulta la [guía de Telegram](https://crm.begraffic.com/docs#telegram).

Este resumen no expone tokens de proveedores, secretos, direcciones de remitentes ni teléfonos. Para consultar números y capacidades de WhatsApp/SMS con `conversations:read`, usa la [guía de mensajería](https://crm.begraffic.com/docs#whatsapp-sms).

## Ejemplo de panel

```js
// Backend de tu plataforma: no enviar el token al navegador.
const response = await fetch('https://crm.begraffic.com/api/v1/me', {
  headers: { Authorization: `Bearer ${stored.access_token}` },
  cache: 'no-store',
});
if (!response.ok) {
  // Ante 401, renueva el token si corresponde o pide reconectar.
  throw new Error('No se pudo consultar el estado de BeCRM');
}
const { data } = await response.json();
const channels = ['email', 'sms', 'whatsapp', 'telegram'].map((channel) => {
  const info = data.integrations[channel];
  return {
    channel,
    workspaceName: data.workspace.name,
    label: {
      active: 'Activo',
      not_configured: 'Configurar en BeCRM',
      configuration_pending: 'Configuración pendiente',
      plan_required: 'Requiere otro plan',
    }[info.status],
    canSend: info.apiCanSend,
    canReceive: info.canReceive,
    configurationUrl: info.configurationUrl,
  };
});
// Devuelve este resumen al navegador para dibujar las tarjetas de tu panel.
```

Si `active=true` y `apiCanSend=false`, el workspace tiene el canal activo pero tu integración no tiene permiso para enviar. Solicita una nueva autorización con `emails:send` para correo o `conversations:write` para mensajería. Si `canReceive=false` o `null`, muestra ese dato aparte; no lo presentes como recepción confirmada.

Consulta el estado tras el callback, al abrir el panel de integraciones y al pulsar «Actualizar estado» o volver de los ajustes del CRM. No conserves indefinidamente la primera respuesta: el cliente puede conectar canales, retirar cuentas o cambiar de plan después de autorizar tu aplicación. Si la consulta falla, muestra «Estado no disponible» y permite reintentar; evita convertir un error en «sin configurar».

---

Fuente: https://crm.begraffic.com/docs#recursos-marca
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# Recursos de marca para integraciones

Usa la identidad de **Be CRM** en el botón de conexión, en tu catálogo de integraciones y en la ficha de configuración de tu plataforma. Estos recursos siguen el manual y los materiales que utiliza actualmente Be CRM.

[Descargar el kit de integración ZIP](https://crm.begraffic.com/brand/integrations/be-crm-integration-kit.zip) · [Guía Markdown](https://crm.begraffic.com/docs/becrm-brand.md) · [Tokens de diseño JSON](https://crm.begraffic.com/brand/integrations/be-crm-brand-tokens.json)

## Logotipos y símbolos

| Recurso | SVG editable | PNG transparente |
| --- | --- | --- |
| Logotipo para fondo claro | [SVG color](https://crm.begraffic.com/brand/integrations/be-crm-logo-color.svg) | [PNG color](https://crm.begraffic.com/brand/integrations/be-crm-logo-color.png) |
| Logotipo para fondo oscuro | [SVG blanco](https://crm.begraffic.com/brand/integrations/be-crm-logo-white.svg) | [PNG blanco](https://crm.begraffic.com/brand/integrations/be-crm-logo-white.png) |
| Símbolo para fondo claro | [SVG símbolo color](https://crm.begraffic.com/brand/integrations/be-crm-symbol-color.svg) | [PNG símbolo color](https://crm.begraffic.com/brand/integrations/be-crm-symbol-color.png) |
| Símbolo para fondo oscuro | [SVG símbolo blanco](https://crm.begraffic.com/brand/integrations/be-crm-symbol-white.svg) | [PNG símbolo blanco](https://crm.begraffic.com/brand/integrations/be-crm-symbol-white.png) |

El kit incluye versiones horizontales para las tarjetas de integración y símbolos compactos para botones.
Los PNG conservan el nombre tal como está diseñado y funcionan sin instalar fuentes.
Los SVG del logotipo contienen texto con Google Sans: si tu herramienta no dispone de esa fuente, utiliza el PNG para conservar el aspecto del nombre. Los SVG del símbolo no dependen de fuentes.
Copia los archivos a tu plataforma y sírvelos desde tu dominio junto con el CSS; los ejemplos usan rutas locales ilustrativas.

## Colores y composición

| Elemento | Valor | Aplicación |
| --- | --- | --- |
| Azul relación | `#2558D9` | Símbolo sobre fondo claro y botón primario. |
| Cian contacto | `#18A4C9` | Acento secundario; conserva el color original del logotipo. |
| Naranja cierre | `#FF7A45` | Acento puntual. |
| Tinta | `#18181B` | Texto sobre fondo claro. |
| Blanco | `#FFFFFF` | Logotipo y texto sobre fondos oscuros. |
| Radio del botón | `8px` | Botones de conexión. |

- Nombre de marca: **Be CRM**, con espacio, «Be» regular y «CRM» en negrita en el logotipo suministrado. El servicio de autorización se llama **BeCRM Connect**.
- Reserva alrededor del logotipo un margen libre de al menos **un cuarto del ancho del símbolo**. Los logotipos descargables ya incluyen esa zona; conserva su lienzo completo.
- Tamaño mínimo del símbolo: **16px** solo y **24px** cuando acompaña al nombre. Para botones con etiqueta usa un símbolo de 20px y un objetivo de clic de al menos 44px de alto.
- Conserva proporciones, orientación, tipografía y colores de los archivos. Evita deformar, recortar, rotar, añadir sombras o degradados y mezclar colores de otros productos Be.
- Sobre claro usa color; sobre oscuro usa blanco. El botón con borde se coloca sobre un fondo claro. Comprueba que texto, foco y estados sigan siendo legibles en tu interfaz.

## Botón de conexión

Etiqueta recomendada: **Conectar con Be CRM**. Conserva texto visible; el símbolo decorativo lleva `alt=""`.
Para una integración existente usa **Volver a conectar** cuando sea necesario renovar la autorización.

[Descargar CSS del botón](https://crm.begraffic.com/brand/integrations/be-crm-connect.css)

```html
<link rel="stylesheet" href="/assets/becrm/be-crm-connect.css" />
<button type="button" class="becrm-connect" id="connect-becrm">
  <img class="becrm-connect__symbol"
       src="/assets/becrm/be-crm-symbol-white.svg" alt="" width="30" height="30" />
  <span>Conectar con Be CRM</span>
</button>
```

Para la variante clara añade `becrm-connect--outline` y utiliza `be-crm-symbol-color.svg`.
Para la variante oscura añade `becrm-connect--dark` y conserva el símbolo blanco.
El lienzo de los archivos del símbolo mide 30px en el botón; con su margen de seguridad, el símbolo visible mide 20px.
El CSS incluye estados hover, foco visible y disabled; deshabilita el botón durante una operación en curso y muestra el resultado con texto accesible.
El estilo solo aporta la presentación: conecta el clic al flujo real de [BeCRM Connect](https://crm.begraffic.com/docs#becrm-connect):

```js
import { connectBeCRM } from './becrm-connect-browser.mjs';
document.querySelector('#connect-becrm').addEventListener('click', async () => {
  try {
    await connectBeCRM({ startUrl: '/api/becrm/start' });
  } catch {
    // Muestra un mensaje visible de cancelación o error; permite volver a intentarlo.
  } finally {
    // Consulta TU backend para mostrar el estado confirmado de la integración.
    await reloadIntegrationStatus();
  }
});
```

El SDK no incluye credenciales en el botón. Tu backend realiza el canje y guarda los tokens.

## Tarjeta y estado de integración

En tu catálogo muestra el logotipo y una descripción concreta, por ejemplo: «Conecta tu workspace de Be CRM para enviar mensajes y sincronizar contactos»; ajusta el texto a las funciones que realmente implementas.

Después de autorizar, muestra el **nombre del workspace conectado**, el estado y acciones para administrar o desconectar.
Usa «Conectado» solo después de que tu servidor confirme el guardado y la configuración necesaria.
Si falta configurar los canales o webhooks, muestra «Configuración pendiente».
Evita usar el logotipo como indicador de éxito o como sustituto del estado en texto.

Estos materiales identifican la integración con Be CRM. El registro autoservicio de una aplicación no implica que Be Graffic la haya verificado ni autoriza presentarla como un producto oficial de Be Graffic.

## Comprobación antes de publicar

1. Comprueba el logotipo en fondos claros y oscuros, sin deformación ni recorte.
2. Prueba el botón con teclado: foco visible, etiqueta legible y estado de carga/error.
3. Verifica popup, redirección, cancelación y reconexión con el flujo documentado.
4. Confirma que el workspace y el estado mostrado coinciden con los guardados en tu servidor.

---

Fuente: https://crm.begraffic.com/docs#whatsapp-sms
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# API externa: WhatsApp

Esta sección conserva el enlace `#whatsapp-sms` para integraciones existentes.
Consulta por separado [SMS](https://crm.begraffic.com/docs#sms) y [Telegram](https://crm.begraffic.com/docs#telegram).

La mensajería usa `/api/v1`. La referencia de endpoints está en [/docs/api](https://crm.begraffic.com/docs/api)
y la especificación OpenAPI en [`/api/v1/openapi.json`](https://crm.begraffic.com/api/v1/openapi.json).
Las rutas de correo y leads existentes siguen funcionando.

## Autenticación y disponibilidad

Crear una clave v1 (`bcrm_...`) en **Ajustes → API** y enviar la cabecera
`Authorization: Bearer <API_KEY_DEL_WORKSPACE>` en todas las llamadas. Las claves antiguas
`bmkt_...` y los tokens de sesión no sirven para estas rutas. Usar la clave solo desde tu servidor.
Alcances: `conversations:read` para consultar; `conversations:write` para enviar;
`webhooks:manage` para suscribir el receptor. La clave decide el workspace: nunca se acepta un
`tableId` del cliente. Se mantienen bloqueo por límite de contactos, rate limit, cuota de
WhatsApp del CRM.

`GET /api/v1/messaging/channels` devuelve números propios y tipos de mensaje. No expone tokens,
secretos de Meta ni credenciales AWS. `configured=false` implica que no hay número utilizable.
Un envío sin número configurado/activo, con token vencido o solicitando un número ajeno se
rechaza con **403 `feature_disabled`** antes de contactar al proveedor. Las respuestas a
conversaciones antiguas también verifican el número original. El historial se conserva consultable.

Cada número incluye `canSend`, `canReceive` y `reason`. **`canReceive=null` significa desconocido**:
un token válido o un número activo no prueban que el webhook o la recepción bidireccional estén
configurados. La API externa no habilita ni compra números automáticamente.

## Ventana de WhatsApp y plantillas

`GET /api/v1/conversations/{id}/capabilities` para WhatsApp:

```json
{
  "data": {
    "conversationId": "whatsapp__18095551234",
    "channel": "whatsapp",
    "checkedAt": "2026-10-01T16:00:00.000Z",
    "phoneNumberId": "123456789",
    "configured": true,
    "canSend": true,
    "reason": null,
    "canSendDirect": true,
    "canSendTemplate": true,
    "supportedMessageTypes": [
      "text",
      "template",
      "image",
      "audio",
      "interactive.button",
      "interactive.list",
      "interactive.cta_url"
    ],
    "window": {
      "lastInboundAt": "2026-10-01T15:00:00.000Z",
      "expiresAt": "2026-10-02T15:00:00.000Z",
      "open": true,
      "remainingSeconds": 82800,
      "templateRequired": false
    }
  }
}
```

La ventana dura 24 horas desde el último mensaje del **contacto**. Un envío nuestro, incluida una
plantilla, no la abre ni renueva. Al vencer exactamente las 24 horas, `open=false`, quedan cero
segundos y se necesita plantilla. Sin mensaje entrante, la ventana está cerrada. Botones, listas,
CTA, imágenes y audio libres requieren ventana abierta.

Las capacidades son una foto informativa: no reservan cuota/saldo ni garantizan aceptación por
Meta. El envío vuelve a comprobar las condiciones, incluida la baja del contacto en WhatsApp.
Meta puede rechazar un destinatario dado de baja aunque la consulta previa permita enviar.

`GET /api/v1/messaging/whatsapp/templates?phoneNumberId=123456789&limit=50` lista el espejo local
de la **WABA de ese número**, paginado con `nextCursor`. Devuelve `name`, `language`, `status`,
`category` y `components` (cabecera, cuerpo y botones). Solo usar `APPROVED`; Meta decide el estado
vigente al enviar. El idioma es el código exacto de Meta, por ejemplo `es`, `es_MX` o `en_US`.
Las plantillas legacy sin WABA se omiten para no ofrecer una plantilla de otra cuenta.
Sin `phoneNumberId`, se consulta el número predeterminado. Sin WABA identificada se rechaza la consulta.
Admite filtros exactos `name` y `status` (p. ej. `?name=mi_plantilla&status=APPROVED`) y devuelve
además `rejectedReason`, `qualityRating`, `createdVia` (`api` o `panel`), `createdBy` y `description`.
El estado, la categoría y la calidad se actualizan solos con los avisos de Meta (si la app de Meta
del CRM está suscrita a `message_template_status_update`, `template_category_update` y
`message_template_quality_update`); si no, al pulsar «Sincronizar» en el panel.

### Crear plantillas por API

`PUT /api/v1/messaging/whatsapp/templates/{name}` (alcance `whatsapp_templates:write`, recomendada
`Idempotency-Key`) crea la plantilla en la WABA del `phoneNumberId` indicado (o del predeterminado) y
la envía a revisión de Meta:

```json
{
  "language": "es",
  "category": "UTILITY",
  "description": "Aviso de un evento",
  "components": [
    { "type": "BODY", "text": "Hola {{1}}, {{2}} te invita", "example": { "body_text": [["Ana", "Iglesia Central"]] } },
    { "type": "FOOTER", "text": "Responde BAJA si no quieres recibir más mensajes." },
    { "type": "BUTTONS", "buttons": [
      { "type": "QUICK_REPLY", "text": "Sí, asistiré" },
      { "type": "URL", "text": "Entrar", "url": "https://example.com/{{1}}", "example": ["https://example.com/panel"] }
    ] }
  ]
}
```

- No existe (nombre + idioma en esa WABA): **201** con `status: "PENDING"`. Queda en el panel como
  «Creada por API · {aplicación}».
- Existe con el mismo contenido: **200** con la actual (repetir es seguro).
- Existe con otro contenido: **409 `conflict`**. Nunca se edita sola: editar una aprobada la devuelve
  a revisión y deja de poder enviarse.
- Meta la rechaza al crearla: **400 `invalid_request`** con el motivo de Meta en `message`.
- Los `example` son obligatorios para cada variable del cuerpo y para la parte variable de una URL.
  Cabecera solo de texto; `parameterFormat: "NAMED"` para parámetros con nombre.
- Al enviar, manda siempre `language`: sin él se usa `es_ES`, y `es` y `es_ES` son plantillas distintas.
- Meta puede cambiarle la categoría (al crearla o después; Utilidad → Marketing cambia el precio de
  cada mensaje). La plantilla lo dice en `requestedCategory` (la pedida, solo si ya no coincide) y
  `pendingCategory` (`{category, effectiveAt}`, cuando Meta avisa de que la va a cambiar). Para
  enterarte sin consultar, suscríbete a `whatsapp_template.updated` (abajo).

## Enviar e iniciar conversaciones

`POST /api/v1/messaging/messages`. Cabeceras:

```http
Authorization: Bearer <API_KEY>
Content-Type: application/json
Idempotency-Key: pedido-1042-confirmacion
```

**Idempotency-Key es obligatoria en esta ruta.** Reutilizar la misma clave para el mismo cuerpo
reproduce la respuesta, sin reenviar. Usar una clave distinta por operación lógica, nunca por
cada reintento. La reserva tiene retención TTL de 24 horas; después de su eliminación la clave puede reenviar.

Texto con vista previa de enlace, dentro de la ventana:

```json
{
  "channel": "whatsapp",
  "to": "+18095551234",
  "phoneNumberId": "123456789",
  "message": {
    "type": "text",
    "text": "Consulta https://example.com/pedido/1042",
    "previewUrl": true
  }
}
```

Plantilla (dentro o fuera de la ventana), con variables de cuerpo y botón URL:

```json
{
  "channel": "whatsapp",
  "to": "+18095551234",
  "phoneNumberId": "123456789",
  "message": {
    "type": "template",
    "template": {
      "name": "pedido_confirmado",
      "language": "es",
      "components": [
        { "type": "body", "parameters": [{ "type": "text", "text": "Ana" }] },
        {
          "type": "button",
          "sub_type": "url",
          "index": "0",
          "parameters": [{ "type": "text", "text": "1042" }]
        }
      ]
    }
  }
}
```

Los componentes deben coincidir con la plantilla aprobada en Meta. Las plantillas con parámetros
nombrados necesitan `parameter_name` en cada parámetro. También se admiten componentes de
cabecera y botones quick reply/copy code según la plantilla.

Botones de respuesta rápida:

```json
{
  "channel": "whatsapp",
  "to": "+18095551234",
  "phoneNumberId": "123456789",
  "message": {
    "type": "interactive",
    "interactive": {
      "type": "button",
      "body": { "text": "¿Confirmas tu cita?" },
      "action": {
        "buttons": [
          { "type": "reply", "reply": { "id": "confirmar", "title": "Confirmar" } },
          { "type": "reply", "reply": { "id": "cambiar", "title": "Cambiar fecha" } }
        ]
      }
    }
  }
}
```

Hasta tres botones, título máximo de 20 caracteres e identificadores únicos. Para listas usar:

```json
{
  "type": "interactive",
  "interactive": {
    "type": "list",
    "body": { "text": "Elige un servicio" },
    "action": {
      "button": "Ver servicios",
      "sections": [
        {
          "title": "Servicios",
          "rows": [{ "id": "consulta", "title": "Consulta", "description": "Agenda una llamada" }]
        }
      ]
    }
  }
}
```

Hasta diez filas **en total**, ids únicos, títulos de fila de hasta 24 caracteres y descripciones
de hasta 72. Si hay varias secciones, todas requieren título.

Enlace con botón CTA:

```json
{
  "type": "interactive",
  "interactive": {
    "type": "cta_url",
    "body": { "text": "Puedes agendar aquí" },
    "action": {
      "name": "cta_url",
      "parameters": { "display_text": "Agendar", "url": "https://example.com/agenda" }
    }
  }
}
```

Imagen: `{"type":"image","image":{"link":"https://example.com/foto.jpg","caption":"Tu pedido"}}`.
Audio: `{"type":"audio","audio":{"link":"https://example.com/audio.ogg"}}`.
Los enlaces de medios deben ser accesibles para Meta y cumplir la validación del servicio de envío.
No se aceptan `media_id` externos, que podrían pertenecer a otro número.

`leadId` es opcional: si se incluye debe existir en el workspace y su teléfono debe coincidir con
el destino. No se reasigna una conversación que ya pertenezca a otro contacto. En WhatsApp,
una conversación existente por otro número devuelve **409 `conflict`**; la API no cambia su
origen silenciosamente. La estructura actual del CRM tiene un chat WhatsApp por destinatario.

Éxito: **201** con `data: {conversationId, channel, messageId, status:"sent"}`. `sent` indica que
el proveedor aceptó el envío, no que lo haya entregado. El índice de conversaciones se proyecta
asíncronamente: una conversación recién creada puede tardar en aparecer en lecturas.

## Responder y recibir

La ruta existente `POST /api/v1/conversations/{id}/reply` conserva `{"text":"..."}` y
`{"template":{...}}`; ahora también admite `{"message":{...}}` para los mensajes WhatsApp descritos.
Debe enviarse exactamente uno de `text`, `template` o `message`. Recomendada `Idempotency-Key`
también en esta ruta. Sale por el número por el que llegó la conversación.

Para recibir en tiempo real, registrar un destino con el mecanismo existente:

```http
POST /api/v1/webhooks
Authorization: Bearer <CLAVE_CON_WEBHOOKS_MANAGE>
Content-Type: application/json
```

```json
{
  "name": "Mi plataforma",
  "url": "https://example.com/webhooks/becrm",
  "events": ["message.received"],
  "active": true
}
```

Guardar el secreto devuelto al crear. El CRM entrega el sobre de evento existente con `id`,
`event`, `timestamp` y `data`; filtrar `data.channel` por `whatsapp`. Verificar
`X-Webhook-Signature` como HMAC-SHA256 de **`timestamp + "." + cuerpo HTTP original`**, con el
secreto del destino y prefijo `sha256=`. Usar comparación en tiempo constante, validar el timestamp del evento considerando los retrasos de los reintentos y deduplicar por id de evento. Persistir el evento y
responder 2xx rápidamente; procesarlo después. El sistema existente proporciona reintentos durables.

El catálogo efectivo se consulta en `GET /api/v1/webhooks/events` (`available` indica emisión
habilitada). La cuenta conectada debe recibir mensajes en el CRM.
Confirma la recepción en el CRM antes de conectar el webhook de tu plataforma.

Después del evento, consultar `GET /api/v1/conversations/{id}/messages`. El `conversationId`
de `data` del webhook conserva el contrato histórico: es el id del chat **sin** prefijo de canal;
para la API usar `whatsapp__{conversationId}`.
Los mensajes se paginan del más reciente al más antiguo y excluyen notas internas. Los DTO
WhatsApp incorpora `messageType`, `deliveryStatus`, `replyToMessageId` e `interaction`.
Una selección de botón/lista devuelve, por ejemplo:

```json
{
  "interaction": {
    "type": "button_reply",
    "id": "confirmar",
    "title": "Confirmar",
    "description": null
  }
}
```

`deliveryStatus` refleja el estado guardado por el webhook de Meta; puede ser null o cambiar a
`delivered`, `read`, `failed`, etc.
La conservación de ids de botones se aplica a los mensajes recibidos tras desplegar Functions.
El historial antiguo que no guardó estos campos no puede reconstruirlos.

### Botón pulsado en el webhook

Desde el 09-oct-2026, `message.received` de WhatsApp trae también el botón u opción pulsada y el
mensaje al que responde, sin tener que pedir los mensajes de la conversación:

```json
{
  "interaction": { "type": "button_reply", "id": "evento:42:si", "title": "Sí, asistiré" },
  "replyToMessageId": "wamid.HBgM…"
}
```

`interaction` es `null` si el mensaje no es una respuesta interactiva. El `id` es el `payload` del
botón de la plantilla o el id de la opción de la lista.

### Estado de entrega: `message.status`

Suscribe el destino a `message.status` para saber si un WhatsApp **enviado por la API v1** se
envió, entregó, leyó o falló. Solo se genera si algún destino lo escucha; un evento por estado:

```json
{
  "channel": "whatsapp",
  "messageId": "wamid.HBgM…",
  "conversationId": "573001112233",
  "phoneNumberId": "123456789",
  "status": "failed",
  "timestamp": "2026-10-09T12:00:00.000Z",
  "error": { "code": "131026", "title": "Mensaje no entregable" }
}
```

`messageId` es el que devolvió el envío: relaciona el estado por él (a Meta no viaja tu
`Idempotency-Key` tal cual). Los mensajes enviados desde el panel o por campañas no generan este
evento.

### Cambios de una plantilla: `whatsapp_template.updated`

Suscribe el destino a `whatsapp_template.updated` para saber cuándo Meta aprueba, rechaza, pausa
o desactiva una plantilla, le cambia la categoría (o avisa de que se la va a cambiar) o cambia su
calidad. Llega por el aviso de Meta en segundos y, si ese aviso se pierde, la revisión que hace el
CRM cada 6 h lo emite igual. Vale para todas las plantillas de la WABA, no solo las creadas por API:

```json
{
  "channel": "whatsapp",
  "templateId": "1398310945751302",
  "name": "bechurch_convocatoria",
  "language": "es",
  "wabaId": "123456789",
  "changes": ["category"],
  "status": "APPROVED",
  "previousStatus": "APPROVED",
  "category": "MARKETING",
  "previousCategory": "UTILITY",
  "requestedCategory": "UTILITY",
  "pendingCategory": null,
  "qualityRating": "GREEN",
  "previousQualityRating": "GREEN",
  "rejectionReason": null
}
```

- `changes`: qué cambió, entre `status`, `category`, `pending_category` (Meta anunció un cambio de
  categoría: `pendingCategory.category` y, si lo dijo, `pendingCategory.effectiveAt`) y `quality`.
- `status`: el de Meta (`APPROVED`, `PENDING`, `REJECTED`, `PAUSED`, `DISABLED`, `IN_APPEAL`…).
  Solo se puede enviar con `APPROVED`. `rejectionReason` (p. ej. `INCORRECT_CATEGORY`,
  `PROMOTIONAL`) solo viene si está rechazada.
- `templateId` es el id de Meta (el `metaTemplateId` de la plantilla); relaciona por él o por
  `name` + `language`.
- La revisión de una categoría o de un rechazo se pide en el Inicio de ayuda para empresas de Meta;
  no se puede pedir por API.

## Errores

- 400 `invalid_request`: formato, combinación de mensajes, falta de idempotencia.
- 401/403: clave inválida, permisos insuficientes o 403 `feature_disabled` por integración no disponible.
- 402 `quota_exceeded`: cuota WhatsApp; `contact_limit_exceeded` si el workspace está bloqueado.
- 404 `not_found`: conversación oculta/ajena o contacto inexistente.
- 409 `invalid_state`: ventana vencida, baja o rechazo permanente del proveedor; revisar `details.reason`.
- 409 `conflict`: número/contacto no coincide con la conversación.
- 409 `unknown_outcome`: pudo enviarse, pero no hubo respuesta. No cambiar de clave ni reenviar automáticamente.
- 429 `rate_limited`: respetar `Retry-After`; 503 `unavailable`: fallo recuperable conocido sin envío.

## Verificar tu integración

Con números de prueba propios, comprobar la recepción WhatsApp, el tiempo restante de la ventana,
una plantilla aprobada de cada cuenta, respuestas a botones/listas.
Comprobar también los reintentos con la misma clave de idempotencia y la validación de firma del
webhook. Consultar los canales disponibles antes de enviar y tratar `canReceive=null` como
recepción sin confirmar.

Referencias: [mensajes interactivos de Meta](https://whatsapp.github.io/WhatsApp-Nodejs-SDK/api-reference/messages/interactive/).

---

Fuente: https://crm.begraffic.com/docs#sms
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# API externa: SMS

Envía texto y recibe mensajes con los números SMS del workspace. Todas las rutas usan
`/api/v1` y `Authorization: Bearer <ACCESS_TOKEN>` desde tu servidor, mediante
BeCRM Connect o una clave v1. Consulta por separado [WhatsApp](https://crm.begraffic.com/docs#whatsapp-sms)
y [Telegram](https://crm.begraffic.com/docs#telegram).

## Configuración y permisos

Activa un número en **Ajustes → Integraciones → SMS** del CRM. `PENDING` no es activo.
Para recibir necesita capacidad bidireccional habilitada por el proveedor y entrada conectada
al CRM. La API no compra números ni activa la recepción automáticamente.

Permisos: `conversations:read` para consultar; `conversations:write` para enviar;
`webhooks:manage` para registrar tu URL. La clave/token determina el workspace.

`GET /api/v1/messaging/channels` devuelve el canal `sms`, sus números, `balanceCents` y
`canSend`, `canReceive`, `reason` por número. `canReceive=null` significa recepción sin confirmar.
`GET /api/v1/me` incluye `data.integrations.sms`, permisos y enlace de configuración.

## Enviar e iniciar conversaciones

```http
POST /api/v1/messaging/messages
Authorization: Bearer <ACCESS_TOKEN>
Content-Type: application/json
Idempotency-Key: sms-cita-1042
```

```json
{
  "channel": "sms",
  "to": "+18095551234",
  "phoneNumberId": "sms-number-id-del-crm",
  "text": "Tu cita: https://example.com/cita/1042",
  "messageType": "TRANSACTIONAL"
}
```

Selecciona `phoneNumberId` del workspace para fijar el origen. Si lo omites se usa un número
activo. `to` debe ser E.164. `leadId` opcional debe pertenecer al workspace y coincidir con el
número del contacto; no se reasigna un chat de otro contacto.

Texto de hasta 1600 caracteres, con enlaces; no botones nativos, adjuntos ni plantillas.
`messageType`: `TRANSACTIONAL` por defecto o `PROMOTIONAL`. No hay ventana de 24 horas.
El coste depende del contenido, segmentación y destino; el envío consume saldo y valida
el límite de contactos. El proveedor puede rechazar un destinatario dado de baja.

Idempotency-Key es obligatoria: usa una clave por operación, no por reintento. La misma clave y
cuerpo reproducen la respuesta durante 24 h; otro cuerpo devuelve 422. Éxito 201 con
`data.conversationId`, `messageId` y `status="sent"` indica aceptación, no entrega.

## Consultar y responder

- `GET /api/v1/conversations?channel=sms`: conversaciones compartidas, paginadas con nextCursor.
- `GET /api/v1/conversations/{id}/messages`: mensajes más recientes primero; continúa hasta null.
- `GET /api/v1/conversations/{id}/capabilities`: disponibilidad y saldo; window=null,
  canSendTemplate=false y tipos `["text"]`.
- `POST /api/v1/conversations/{id}/reply` con `{"text":"…"}` y clave de idempotencia:
  responde por el número original del chat.

Usa el ID devuelto por el CRM, como `sms__18095550000__18095551234`.
Las conversaciones ajenas o privadas no son accesibles. Para todo el historial usa la
[importación inicial o manual](https://crm.begraffic.com/docs#historial-workspace) con el canal `sms`.

## Recibir mediante webhook

Después de autorizar OAuth, registra tu destino con `POST /api/v1/webhooks`:

```json
{
  "name": "Mi plataforma SMS",
  "url": "https://example.com/webhooks/becrm",
  "events": ["message.received"],
  "active": true
}
```

Guarda el secreto y verifica `X-Webhook-Signature`: HMAC-SHA256 de
`timestamp + "." + cuerpo HTTP original`, prefijo `sha256=`, comparación en tiempo constante.
Valida antigüedad considerando los reintentos. Filtra `data.channel === "sms"`, deduplica por
`id`, persiste y responde 2xx rápidamente. Consulta `GET /api/v1/webhooks/events` para comprobar
`available`. El webhook incluye `from`, `to`, `body`, `conversationId`, `messageId`, `leadId`
y `ticketId`; los IDs opcionales pueden ser null.

El ID del chat del webhook no lleva prefijo: para leer la API usa
`sms__${data.conversationId}`. Confirma primero la recepción en BeCRM.
No existe un evento público de confirmación de entrega SMS en este contrato.

## Errores y verificación

400 cuerpo inválido/idempotencia ausente; 403 `feature_disabled` número no disponible;
402 `quota_exceeded` saldo insuficiente o `contact_limit_exceeded` workspace bloqueado;
404 conversación inaccesible; 409 `unknown_outcome` proveedor sin respuesta;
409 `invalid_state` rechazo del proveedor; 503 `unavailable` fallo temporal.
No reintentes un 402 hasta resolver el saldo/límite. Tras resultado incierto conserva la clave.

Comprueba envío y recepción con números propios, el origen elegido, deduplicación por clave,
privacidad entre workspaces y revocación del webhook.

---

Fuente: https://crm.begraffic.com/docs#telegram
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# API externa: Telegram

Usa el bot conectado al workspace en BeCRM para consultar conversaciones, enviar texto y
recibir nuevos mensajes en tu plataforma. El token del bot permanece en BeCRM: nunca se entrega
al cliente externo. Autentica desde tu servidor con BeCRM Connect o una clave v1 (`bcrm_…`).

## Configuración y permisos

1. El administrador conecta su bot en **Ajustes → Integraciones → Telegram** del CRM.
2. Comprueba que un mensaje al bot llega a la bandeja de BeCRM.
3. Autoriza `conversations:read` para canales, capacidades e historial;
   `conversations:write` para enviar; y `webhooks:manage` para registrar tu receptor.
4. Consulta `GET /api/v1/me`: `data.integrations.telegram` informa `botConnected`,
   `webhookRegistered`, permisos y enlace de configuración.

`GET /api/v1/messaging/channels` incluye un elemento:

```json
{
  "channel": "telegram",
  "configured": true,
  "bot": { "username": "mi_bot", "canSend": true, "canReceive": null },
  "numbers": [],
  "supportedMessageTypes": ["text"],
  "balanceCents": null
}
```

`canReceive=null` indica que la configuración guardada no confirma el registro del webhook.
`configured` no garantiza que Telegram acepte cada envío: el usuario puede haber bloqueado el bot.
No necesitas un número telefónico. El cliente externo no debe registrar un webhook directamente
con Telegram: BeCRM recibe del bot y reenvía los eventos a tu URL.

## Consultar conversaciones y capacidades

Lista `GET /api/v1/conversations?channel=telegram&limit=100` y continúa usando
`cursor=nextCursor` hasta `null`. Conserva el `id` devuelto (por ejemplo, `telegram__telegram__123456789`):
no lo construyas a partir de un teléfono o nombre de usuario.

- `GET /api/v1/conversations/{id}/messages`: mensajes paginados, más recientes primero.
- `GET /api/v1/conversations/{id}/capabilities`: disponibilidad del bot y del chat.
  Devuelve `window=null`, `phoneNumberId=null`, `canSendTemplate=false` y tipos `["text"]`.
- Solo se exponen conversaciones compartidas del workspace autorizado; privadas o ajenas dan 404.

## Enviar texto

```http
POST /api/v1/messaging/messages
Authorization: Bearer <ACCESS_TOKEN>
Content-Type: application/json
Idempotency-Key: telegram-respuesta-1042
```

```json
{
  "channel": "telegram",
  "conversationId": "telegram__telegram__123456789",
  "text": "Tu cita está confirmada: https://example.com/cita/1042"
}
```

También puedes usar `POST /api/v1/conversations/{id}/reply` con `{"text":"…"}`.
Usa una clave por operación en ambas rutas; en `/messaging/messages` es obligatoria.
La misma clave y cuerpo reproducen la respuesta durante la retención de 24 h, sin reenviar;
otro cuerpo con esa clave devuelve 422. El texto admite hasta 4096 caracteres y enlaces,
sin HTML/Markdown interpretado, adjuntos, botones ni plantillas en este contrato.

Éxito: 201 con `data.messageId` y `data.status="sent"`: Telegram aceptó el mensaje;
no confirma lectura o entrega. BeCRM guarda el mensaje en la conversación del workspace.
No consume saldo SMS ni cuota WhatsApp/correo, pero aplican rate limit, bloqueo por límite de
contactos y límites del proveedor.

La API usa **conversaciones ya existentes en BeCRM**, no inicia chats con teléfonos o usernames.
El usuario debe haber contactado al bot o el bot debe tener acceso al chat. Consulta las
[reglas de la plataforma de bots](https://core.telegram.org/bots/features#botfather).
No hay ventana de 24 horas ni plantillas de WhatsApp.

## Recibir en tu plataforma

Registra después del callback un destino con `POST /api/v1/webhooks`:

```json
{
  "name": "Mensajes de mi plataforma",
  "url": "https://example.com/webhooks/becrm",
  "events": ["message.received"],
  "active": true
}
```

Guarda el secreto devuelto. Verifica `X-Webhook-Signature` como HMAC-SHA256 de
`timestamp + "." + cuerpo HTTP original`, con el secreto y prefijo `sha256=`;
compara en tiempo constante. Valida la antigüedad considerando los reintentos,
deduplica por `id`, persiste el evento y devuelve 2xx antes de procesarlo.
Filtra `data.channel === "telegram"`. El evento trae:

```json
{
  "channel": "telegram",
  "from": "123456789",
  "body": "Hola",
  "messageType": "text",
  "leadId": null,
  "ticketId": null,
  "conversationId": "telegram__123456789",
  "messageId": "42"
}
```

El `data.conversationId` del webhook es el ID del documento del chat, sin el prefijo adicional
del índice de la API. Para consultar por API, usa `telegram__${data.conversationId}`.
En Telegram el documento del chat ya puede empezar por `telegram__`: por eso el ID de la API
puede ser `telegram__telegram__123456789`. No elimines prefijos ni uses `data.from` como ID de conversación. `body` puede ser null para mensajes sin texto.
Consulta `GET /api/v1/webhooks/events` y comprueba `available` para `message.received`.
El evento depende de la recepción real en BeCRM y del emisor de servidor habilitado;
autorizar OAuth no registra tu webhook automáticamente.

## Historial y errores

La [importación inicial o manual](https://crm.begraffic.com/docs#historial-workspace) permite seleccionar `telegram` y
reanudar con checkpoints. Recupera lo almacenado en BeCRM y no vuelve a emitir webhooks.

- 400 `invalid_request`: cuerpo inválido, texto demasiado largo o falta de Idempotency-Key.
- 403 `feature_disabled`: bot no conectado en `/messaging/messages`.
- 403 `forbidden`: falta el permiso solicitado.
- 404 `not_found`: conversación ajena, privada o inexistente.
- 402 `contact_limit_exceeded`: resuelve el bloqueo del workspace.
- 409 `unknown_outcome`: no se confirmó la respuesta del proveedor; no reenvíes con otra clave.
- 409 `invalid_state`: chat inválido o rechazo del proveedor, como bot bloqueado.
- 503 `unavailable`: Telegram limitó o falló el envío; conserva la clave de idempotencia.

Antes de publicar: recibe un mensaje real, responde y verifica que aparece en el CRM;
repite con la misma clave para comprobar que no se duplica; confirma que otro workspace no
puede consultar ni responder la conversación y que revocar la conexión detiene su webhook.

---

Fuente: https://crm.begraffic.com/docs#leads
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# Instrucciones para administrar leads

## Endpoints

- GET `/api/leads?email=correo@example.com` para buscar un lead por email.
- POST `/api/leads` para crear un nuevo lead.
- PATCH `/api/leads/{id}` o PUT `/api/leads/{id}` para actualizar un lead existente.

## Autenticación

Incluye el header:

```
Authorization: Bearer <token>
```

El token puede ser un ID token de Firebase o un token pre-registrado en la colección `tokens`. En ambos casos se validará que esté activo y asociado a una `tableId`.

## Asignación por defecto del token (API key)

Una API key puede tener configurada una asignación por defecto (en Ajustes › API keys: organización, etiquetas y/o listas). Al **crear** un lead nuevo con esa key:

- La **organización por defecto** se asigna solo si el request NO envía `organizationId` ni `organizationName` (lo explícito del request siempre manda).
- Las **etiquetas** y **listas** por defecto se **suman** a las que envíes en `tags`/`groups`.
- Solo aplica al **crear** un lead nuevo; al actualizar un lead existente, los defaults del token no se aplican.

## Campos del body (creación)

- `name` (string, opcional)
- `secondName` (string, opcional)
- `lastname` (string, opcional)
- `secondLastname` (string, opcional)
- `email` (string, opcional): se normaliza a minúsculas y sin espacios. Debe tener formato válido si se envía.
- `phone` (objeto, opcional): `{ phone_code, phone }` para código de área y número fijo. Si envías `phone`, `phone_code` es requerido y el número solo acepta dígitos y símbolos `+ ( ) -`.
- `movil` (objeto, opcional): `{ movil_code, movil }` para código y número móvil. Si envías `movil`, `movil_code` es requerido y el número solo acepta dígitos y símbolos `+ ( ) -`.
- `address` (objeto, opcional): `{ address1, address2, city, state, country, zip }`.
- `gender` (string, opcional): género del lead; usa `Masculino` o `Femenino` (no sensible a mayúsculas/minúsculas). Se guarda recortado.
- `birthdate` (string, Date o null, opcional): fecha de nacimiento. Acepta formato `YYYY-MM-DD` o un objeto Date; se guarda como timestamp. Envía `null` o cadena vacía para borrar.
- `financialActivities` (array<object>, opcional): movimientos financieros iniciales. Cada elemento acepta `kind` (requerido), `amount` (number), `currency` (string), `description` (string), `status` (string), `date` (string o Date; `YYYY-MM-DD`).
- `tags` (string[], opcional): etiquetas del lead. Se eliminan duplicados y espacios.
- `groups` (string[], opcional): **IDs de listas** a las que se vincula el lead. Se escriben en el campo `groups` del lead (es así como un lead "pertenece" a una lista). Se deduplican.
- `organizationId` (string, opcional): ID de la organización (empresa) a la que se asigna el lead. Si el ID no existe, se responde `400`. Al asignarla se setean `organizationId` y `organizationName`, y además se agregan **las listas de la organización a `groups`** y **sus etiquetas a `tags`**, para que el lead realmente aparezca bajo esa organización.
- `organizationName` (string, opcional): nombre de la organización. Normalmente se infiere de `organizationId`; envíalo solo si quieres fijarlo manualmente, o para guardar el nombre sin un ID.
- `active` (boolean, opcional): por defecto `true`.
- `image` (string, opcional): URL o referencia a la foto del lead.
- `note` (objeto, opcional): nota que se añade al lead, con los mismos campos y reglas que en la actualización (ver abajo). Se guarda tanto si el lead se crea como si ya existía; la respuesta añade `noteCreatedId` al final. Desde el 29-sep-2026 (antes se ignoraba); una nota sin `title` responde `400` y no crea nada.

## Campos del body (actualización)

Se aceptan los mismos campos que en creación. Solo se actualizan las propiedades enviadas. `tags` se **combina** con las etiquetas que ya tiene el lead (desde el 29-sep-2026; antes reemplazaba la lista completa, al contrario de lo que decía este documento). `email`, `tags`, `phone`, `movil`, `address`, `gender` y `birthdate` también se normalizan (se recortan y omiten cuando quedan vacíos); `gender` debe ser `Masculino` o `Femenino` o se rechazará con 400. Para borrar `phone`, `movil`, `address`, `gender` o `birthdate`, envía el campo como `null` o con sus valores vacíos. Además puedes enviar `removeTags` (string[]) para eliminar etiquetas existentes sin necesidad de enviar el arreglo completo.

Para la asignación a listas y organización en actualización:

- `groups` (string[]): IDs de listas que se **agregan** a las existentes del lead (merge).
- `removeGroups` (string[]): IDs de listas que se **quitan** del lead.
- `organizationId` (string): asigna la organización (con el mismo merge de listas/etiquetas descrito en creación). Envía `organizationId: null` o `""` para **desasignar** la organización (borra `organizationId` y `organizationName`).
- `organizationName` (string): fija el nombre de la organización sin tocar el ID.

También puedes agregar una nota al lead con el campo `note`:

- `note.title` (string, requerido)
- `note.content` (string, opcional)
- `note.dueDate` (string o Date, opcional): formato `YYYY-MM-DD`.
- `note.reminderAt` (string o Date, opcional): formato `YYYY-MM-DD`.
- `note.tags` (string[], opcional): etiquetas de la nota; se deduplican.
- `note.type` (string, opcional): `note` o `reminder` (por defecto `note`).

Las notas se guardan en la subcolección `tables/{tableId}/leads/{leadId}/notes` con `createdAt` y `createdBy` automáticos.

Para agregar movimientos financieros, envía `financialActivities` (array de objetos):

- `kind` (string, requerido)
- `amount` (number, opcional)
- `currency` (string, opcional; ej. USD, DOP, EUR)
- `description` (string, opcional)
- `status` (string, opcional)
- `date` (string o Date, opcional; formato `YYYY-MM-DD`)

Se guardan en `tables/{tableId}/leads/{leadId}/financialActivities` con `createdAt`, `updatedAt` y `createdBy` automáticos. En la respuesta se incluye `financialCreatedIds` cuando se crean desde PATCH/PUT.

## Respuestas

- 200 (GET): retorna el lead encontrado.
- 201 (POST): retorna el lead creado con su `id`.
- 200 (POST): el lead ya existía (mismo `email`) y se actualizó; `created: false`.
- 200 (PATCH/PUT): retorna el lead actualizado.
- 400: body inválido o falta el `id` en la ruta.
- 401/403: problemas con el token o falta de permisos.
- 404: el lead no existe (en actualizaciones).
- 405: método no permitido.

## Reglas y comportamiento

- Debes enviar al menos un método de contacto: `email`, `phone.phone` o `movil.movil`.
- Si envías `phone.phone` o `movil.movil`, debes incluir su código (`phone_code` o `movil_code`) y los números solo aceptan dígitos y `+ ( ) -`.
- Si no envías email pero sí teléfono/móvil, `name` se vuelve requerido para mostrar el contacto.
- El `email` es opcional, pero si se envía debe tener formato válido; se guarda en minúsculas y recortado.
- Al crear se incrementan los contadores de leads y se actualizan métricas relacionadas (etiquetas, grupos, organizaciones) cuando aplican.
- Si no envías `active`, se marca como `true`.
- `tags` se guardan sin duplicados, ignorando mayúsculas/minúsculas.
- `groups` contiene **IDs de listas** (no nombres); son los que determinan la pertenencia del lead a una lista.
- Asignar `organizationId` busca la empresa en el workspace; si no existe se responde `400`. Al asignarla se guardan `organizationId` y `organizationName`, y se replican las señales de pertenencia de la organización: sus listas se agregan a `groups` y sus etiquetas a `tags` (igual que cuando un lead entra por formulario).
- En actualización (POST con un `email` existente y PATCH/PUT), `groups` y `tags` hacen **merge** con los valores existentes; usa `removeGroups`/`removeTags` para quitar elementos puntuales. Si una etiqueta está en `tags` y en `removeTags`, se quita.
- Desactivar un lead (`active: false` sobre uno activo) deja además `deactivationReason: "api"`; la fecha de la baja (`deactivatedAt`) la sella el servidor.
- Un lead creado con un ID token del panel (no con API key) queda con origen `manual`; con API key, origen `api`.
- La API v1 (`/api/v1`, claves nuevas con alcances y documentación en `/docs/api`) no acepta estas claves ni estas rutas aceptan las v1.
- Se registra `createdBy` con el email del usuario autenticado cuando está disponible.

## Ejemplo Node.js (crear lead)

```js
const fetch = require("node-fetch");

const body = {
  name: "Juan",
  secondName: "Carlos",
  lastname: "Pérez",
  secondLastname: "Gómez",
  email: "juan.perez@example.com",
  phone: { phone_code: "+52", phone: "5551234567" },
  movil: { movil_code: "+52", movil: "5512345678" },
  address: {
    address1: "Av. Reforma 123",
    address2: "Piso 4",
    city: "Ciudad de México",
    state: "CDMX",
    country: "México",
    zip: "01000",
  },
  gender: "Masculino",
  birthdate: "1990-05-10",
  image: "https://cdn.example.com/avatars/juan.png",
  tags: ["Clientes", "Newsletter"],
  groups: ["listId_abc", "listId_def"], // IDs de listas
  organizationId: "companyId_123", // asigna la organización (agrega sus listas y tags)
  active: true,
  financialActivities: [
    {
      kind: "Compra",
      amount: 2500,
      currency: "MXN",
      description: "Primer pedido",
      status: "Pagado",
      date: "2025-03-12",
    },
  ],
};

fetch("https://crm.begraffic.com/api/leads", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer TU_TOKEN",
  },
  body: JSON.stringify(body),
})
  .then((res) => res.json())
  .catch(console.error);
```

## Ejemplo Node.js (actualizar lead)

```js
const fetch = require("node-fetch");

const body = {
  tags: ["Clientes", "VIP"],
  active: false,
  birthdate: "1990-05-10",
  phone: { phone_code: "+1", phone: "3051234567" },
  address: {
    city: "Miami",
    country: "USA",
  },
  note: {
    title: "Llamada de seguimiento",
    content: "Preguntar por presupuesto y disponibilidad.",
    dueDate: "2025-03-15",
    tags: ["followup"],
  },
  financialActivities: [
    {
      kind: "Donación",
      amount: 100,
      currency: "USD",
      status: "Pendiente",
    },
  ],
};

fetch("https://crm.begraffic.com/api/leads/ID_DEL_LEAD", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer TU_TOKEN",
  },
  body: JSON.stringify(body),
})
  .then((res) => res.json())
  .catch(console.error);
```

## Ejemplo Node.js (quitar tags)

```js
const fetch = require("node-fetch");

const body = {
  removeTags: ["Clientes"],
};

fetch("https://crm.begraffic.com/api/leads/ID_DEL_LEAD", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer TU_TOKEN",
  },
  body: JSON.stringify(body),
})
  .then((res) => res.json())
  .catch(console.error);
```

## Ejemplo Node.js (asignar organización y listas)

```js
const fetch = require("node-fetch");

const body = {
  organizationId: "companyId_123", // agrega también las listas y tags de la organización
  groups: ["listId_abc"], // vincular a listas adicionales por su ID
};

fetch("https://crm.begraffic.com/api/leads/ID_DEL_LEAD", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer TU_TOKEN",
  },
  body: JSON.stringify(body),
})
  .then((res) => res.json())
  .catch(console.error);
```

## Ejemplo Node.js (quitar listas y desasignar organización)

```js
const fetch = require("node-fetch");

const body = {
  removeGroups: ["listId_abc"], // quita estas listas del lead
  organizationId: null, // desasigna la organización (borra organizationId y organizationName)
};

fetch("https://crm.begraffic.com/api/leads/ID_DEL_LEAD", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer TU_TOKEN",
  },
  body: JSON.stringify(body),
})
  .then((res) => res.json())
  .catch(console.error);
```

## Qué se puede y no se puede hacer

- ✔️ Crear leads con tags, imagen y estado personalizado.
- ✔️ Asignar el lead a listas (`groups`) y a una organización (`organizationId`), igual que desde un formulario.
- ✔️ Actualizar solo los campos necesarios usando PATCH o PUT.
- ✔️ Quitar elementos puntuales con `removeTags` / `removeGroups`, o desasignar la organización con `organizationId: null`.
- ✔️ Usar tokens de Firebase o tokens gestionados en la colección `tokens`.
- ❌ Omitir el header `Authorization`.
- ❌ Enviar bodies que no sean JSON válido.
- ❌ Asignar una `organizationId` que no exista en el workspace (responde 400).
- ❌ Usar métodos distintos de POST, PATCH o PUT.

---

Fuente: https://crm.begraffic.com/docs#correos
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# Instrucciones para enviar correos (sencillos y templates)

## Endpoint

POST `/api/emails/sendEmail`

## Autenticación

Incluye el header:

```
Authorization: Bearer <token>
```

## Campos del body

### Envío sencillo (custom)

- `to` (string, obligatorio): destinatario principal (solo uno)
- `subject` (string, obligatorio)
- `html` o `text` (string, al menos uno obligatorio)
- `from` (string, opcional): correo remitente específico (ver "Remitente")
- `fromName` (string, opcional)
- `replyTo`, `cc`, `bcc` (string o array, opcional)
- `idCustomer` (string, opcional)

### Envío con template

- `to` (string, obligatorio): destinatario principal (solo uno)
- `template` (objeto, obligatorio):
  - `name` (string, obligatorio): nombre del template en tu proveedor de correo
  - `data` (objeto, opcional): datos para el template
- `from` (string, opcional): correo remitente específico (ver "Remitente")
- `fromName` (string, opcional)
- `replyTo`, `cc`, `bcc`, `idCustomer`: igual que en el envío sencillo

## Remitente

- Sin `from`, el correo sale desde el primer remitente **verificado** del workspace.
- Con `from`, el correo sale desde esa dirección **solo si** está registrada como
  remitente del workspace (o pertenece a un dominio verificado). También puedes usar
  `senderId` con el id de la identidad registrada.
- Si el remitente solicitado no está configurado, la API responde **404** con
  `{"error": "El remitente <correo> no está configurado en este workspace. ..."}` y
  **no** envía el correo (nunca se sustituye por otro remitente).
- Si está registrado pero aún sin verificar, responde **409**.

## Restricciones

- Solo se permite un destinatario principal en `to`
- Máximo 50 destinatarios en `cc`, `bcc`, `replyTo`
- Los correos deben ser válidos
- Si usas `template`, no incluyas `subject`, `html` ni `text`
- Si usas envío sencillo, no incluyas `template`
- El remitente debe estar verificado en el proveedor de correo configurado
- En envíos sencillos (custom), se agrega automáticamente un pie con el nombre de la empresa, un enlace a Be CRM y un link de baja

## Ejemplo Node.js (fetch)

### Sencillo

```js
const fetch = require("node-fetch");

const body = {
  to: "destinatario@ejemplo.com",
  subject: "Asunto de prueba",
  html: "<h1>Hola mundo</h1>",
  fromName: "Mi Empresa",
};

fetch("https://crm.begraffic.com/api/emails/sendEmail", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer TU_TOKEN",
  },
  body: JSON.stringify(body),
})
  .then((res) => res.json())
  .catch(console.error);
```

### Template

```js
const fetch = require("node-fetch");

const body = {
  to: "destinatario@ejemplo.com",
  template: {
    name: "MiTemplateCorreo",
    data: {
      nombre: "Juan",
      producto: "CRM",
    },
  },
  // Opcional: remitente específico (debe estar registrado en el workspace,
  // si no la API responde 404 sin enviar).
  from: "ventas@miempresa.com",
  fromName: "Mi Empresa",
};

fetch("https://crm.begraffic.com/api/emails/sendEmail", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer TU_TOKEN",
  },
  body: JSON.stringify(body),
})
  .then((res) => res.json())
  .catch(console.error);
```

## Qué se puede y no se puede hacer

- ✔️ Puedes enviar correos personalizados o usando templates de tu proveedor de correo.
- ✔️ Puedes incluir cc, bcc, replyTo, idCustomer si lo necesitas.
- ✔️ Puedes elegir el remitente con `from` (o `senderId`) entre los remitentes registrados del workspace.
- ❌ No puedes enviar a más de un destinatario principal.
- ❌ No puedes enviar sin asunto y cuerpo (en modo sencillo).
- ❌ No puedes enviar sin template.name (en modo template).
- ❌ No puedes enviar si el remitente no está verificado en tu proveedor de correo.
- ❌ No puedes enviar desde un `from` que no esté configurado en el workspace (la API responde 404 y no sustituye el remitente).

---

Fuente: https://crm.begraffic.com/docs#baja
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# API de baja de correos (unsubscribe)

## Endpoint

POST `/api/unsubscribe`

## Uso

Permite dar de baja a un lead desde un enlace público. Debes enviar el `tableId` y al menos `leadId` o `email`.

## Campos del body

- `tableId` (string, obligatorio): ID del workspace.
- `leadId` (string, opcional): ID del lead.
- `email` (string, opcional): email del lead (normalizado a minúsculas).
- `reason` (string, opcional): motivo de baja (se guarda como nota del lead).

## Respuesta exitosa

```json
{
  "status": "unsubscribed",
  "leadId": "abc123",
  "alreadyInactive": false,
  "noteAdded": true
}
```

## Ejemplo

```js
const body = {
  tableId: "workspace-123",
  email: "lead@ejemplo.com",
  reason: "Ya no deseo recibir mensajes",
};

fetch("https://crm.begraffic.com/api/unsubscribe", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(body),
});
```

---

Fuente: https://crm.begraffic.com/docs#webhook-verificacion
Índice para agentes: [llms.txt](https://crm.begraffic.com/llms.txt)

# Webhook de verificación de integraciones

Este directorio contiene los endpoints que se exponen públicamente como webhooks dentro de la API de Pages de Next.js. Actualmente incluye el webhook de verificación de conexión para que plataformas externas puedan validar que su token está correctamente configurado y asignado al workspace indicado.

## `POST /api/webhooks/connection` (también disponible como `GET`)

### Propósito

Permite realizar un "ping" desde otra plataforma para verificar en tiempo real que la integración funciona. La respuesta incluye:

- Estado general de la verificación (`status`, `message`, `timestamp`).
- Información del workspace (ID, nombre y plan cuando están disponibles).
- El tipo de autenticación que se usó: token de API o sesión de usuario, con la misma metadata que ves en la consola (nombre, descripción, estado, contador de usos y fechas).

### Autenticación

Debes enviar el encabezado `Authorization: Bearer <token>` donde `<token>` puede ser:

1. Un **ID token de Firebase** (sesión de usuario autenticada).
2. Un **token API** generado en la consola (formato `bmkt_...`). El servidor calcula internamente el hash SHA‑256 y lo compara con la colección de tokens, por lo que jamás se expone el secreto.

Si el token está inactivo o no existe, recibirás `401 Unauthorized`.

### Solicitud de ejemplo

```bash
curl -X POST https://tu-dominio/api/webhooks/connection \
  -H "Authorization: Bearer bmkt_ejemplo123"
```

### Respuesta exitosa

```json
{
  "status": "ok",
  "message": "Integración verificada correctamente.",
  "timestamp": "2024-02-18T23:09:11.038Z",
  "workspace": {
    "id": "workspaceId",
    "name": "Marketing Team",
    "plan": "Pro"
  },
  "auth": {
    "type": "token",
    "tokenId": "f3d2b7...",
    "name": "Webhook CRM",
    "description": "Integración externa",
    "active": true,
    "createdBy": "admin@example.com",
    "uses": 42,
    "dateCreated": "2024-02-15T21:01:12.000Z",
    "lastUsed": "2024-02-18T23:07:55.311Z"
  }
}
```

Cuando la autenticación proviene de un usuario, la sección `auth` cambia a:

```json
{
  "type": "user",
  "email": "usuario@example.com"
}
```

### Errores comunes

| Código | Causa                               | Observaciones                                |
| ------ | ----------------------------------- | -------------------------------------------- |
| 401    | Falta el encabezado `Authorization` | Incluye siempre `Bearer <token>`             |
| 401    | Token inválido o deshabilitado      | Revisa que el token exista y siga activo     |
| 403    | Usuario autenticado sin `tableId`   | La cuenta debe estar asociada a un workspace |
| 405    | Método no permitido                 | Solo se aceptan `GET` o `POST`               |

### Implementaciones recomendadas

- Usa este webhook como parte del flujo de alta de integraciones: después de guardar un token en otra plataforma, llama al endpoint y valida que `status === "ok"`.
- Registra el `tokenId` (hash) que devuelve la respuesta para facilitar auditorías sin almacenar el secreto completo.

---

Cualquier webhook adicional debe documentarse en este mismo archivo para mantener una referencia centralizada de los endpoints públicos.
