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: registro, botón, callback, renovación y desconexión.

Mensajería — consulta las guías de WhatsApp, SMS y 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. 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

RecursoPara qué sirve
BeCRM ConnectConectar aplicaciones Be o plataformas externas mediante login y selección de workspace.
LeadsCrear, buscar y actualizar contactos, sus listas y organización.
CorreosEnviar correos personalizados o por plantilla desde tu remitente verificado.
WhatsAppMensajes, ventana de respuesta, plantillas y mensajes interactivos.
SMSTexto, números de origen, saldo y recepción bidireccional.
TelegramBot del workspace, conversaciones, respuestas y eventos de entrada.
Baja (unsubscribe)Dar de baja a un contacto desde un enlace público.
Webhook de verificaciónConfirmar que una integración externa quedó conectada.

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

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.

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

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

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:

PiezaResponsabilidad de tu plataforma
Botón de conexiónAbrir la ruta de inicio en popup o página completa.
Ruta de inicioComprobar al administrador local y guardar la transacción con state y PKCE.
Callback registradoValidar y consumir la transacción, canjear el código y guardar los tokens cifrados.
ConfiguraciónAsociar el workspace recibido a la organización local y configurar los webhooks necesarios.
Renovación y desconexiónRotar 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 y el helper del botón y popup, 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 → 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:

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.

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

{
  "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 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

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 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).

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

{
  "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ónUso
GET /.well-known/oauth-authorization-serverDescubrir endpoints, permisos y métodos admitidos.
GET /connectAbrir el login y consentimiento con los parámetros del paso 2.
POST /api/oauth/tokenCanjear códigos y renovar tokens desde tu servidor.
POST /api/oauth/revokeRevocar credenciales desde tu servidor.
GET /api/v1/meConsultar 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.

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:

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);
<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:

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.

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

CanalPermisoLectura
Correo compartidoemails:readGET /api/v1/emails/history
WhatsApp, SMS y Telegramconversations:readGET /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

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 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. La renovación debe estar serializada por conexión y guardar ambos tokens nuevos de forma atómica.

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.

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.

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

Qué mostrar en tu panel

Campo de cada canalSignificado
availableEl plan del workspace incluye este canal.
configuredBeCRM tiene una configuración utilizable: identidad verificada, número activo o bot conectado.
activeEl canal está configurado y disponible según el plan.
statusactive, not_configured, configuration_pending o plan_required.
reasonMotivo de configuración o plan cuando el canal no está activo.
apiCanSendEl canal está activo y la integración tiene el permiso de envío. Cuotas y proveedor se comprueban al enviar.
canReceiveRecepción configurada: true o false; null significa sin confirmar.
configurationUrlEnlace 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.

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.

Ejemplo de panel

// 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».

Sobre fondo claro

Be CRM · logotipo color

Ejemplo del botón de conexión

Conectar con Be CRM

Sobre fondo oscuro

Be CRM · logotipo blanco

Ejemplo del botón de conexión

Conectar con Be CRM

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 · Guía Markdown · Tokens de diseño JSON

Logotipos y símbolos

RecursoSVG editablePNG transparente
Logotipo para fondo claroSVG colorPNG color
Logotipo para fondo oscuroSVG blancoPNG blanco
Símbolo para fondo claroSVG símbolo colorPNG símbolo color
Símbolo para fondo oscuroSVG símbolo blancoPNG símbolo blanco

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

ElementoValorAplicación
Azul relación#2558D9Símbolo sobre fondo claro y botón primario.
Cian contacto#18A4C9Acento secundario; conserva el color original del logotipo.
Naranja cierre#FF7A45Acento puntual.
Tinta#18181BTexto sobre fondo claro.
Blanco#FFFFFFLogotipo y texto sobre fondos oscuros.
Radio del botón8pxBotones 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

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

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.

API externa: WhatsApp

Esta sección conserva el enlace #whatsapp-sms para integraciones existentes. Consulta por separado SMS y Telegram.

La mensajería usa /api/v1. La referencia de endpoints está en /docs/api y la especificación OpenAPI en /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:

{
  "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:

{
  "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:

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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

POST /api/v1/webhooks
Authorization: Bearer <CLAVE_CON_WEBHOOKS_MANAGE>
Content-Type: application/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:

{
  "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:

{
  "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:

{
  "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:

{
  "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.

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 y 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

POST /api/v1/messaging/messages
Authorization: Bearer <ACCESS_TOKEN>
Content-Type: application/json
Idempotency-Key: sms-cita-1042
{
  "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 con el canal sms.

Recibir mediante webhook

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

{
  "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.

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:

{
  "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

POST /api/v1/messaging/messages
Authorization: Bearer <ACCESS_TOKEN>
Content-Type: application/json
Idempotency-Key: telegram-respuesta-1042
{
  "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. 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:

{
  "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:

{
  "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 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.

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)

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)

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)

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)

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)

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.

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

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

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).

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

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

Ejemplo

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),
});

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

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

Respuesta exitosa

{
  "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:

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

Errores comunes

CódigoCausaObservaciones
401Falta el encabezado AuthorizationIncluye siempre Bearer <token>
401Token inválido o deshabilitadoRevisa que el token exista y siga activo
403Usuario autenticado sin tableIdLa cuenta debe estar asociada a un workspace
405Método no permitidoSolo 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.