ChatFlowy Orchestration API v1

API REST server-to-server para integrar sistemas externos (ERP, cobranza, CRM) con la plataforma de mensajería masiva WhatsApp de ChatFlowy. Permite crear plantillas, gestionar audiencias, lanzar campañas y recibir notificaciones en tiempo real mediante webhooks.

dns
Base URL
https://us-central1-{project}.cloudfunctions.net/orchestrationApi
route
Prefijo de endpoints
/orchestration/v1
data_object
Formato
application/json
language
Región
Google Cloud us-central1

Ejemplo de URL completa

BASE_URL = https://us-central1-{project}.cloudfunctions.net/orchestrationApi
PREFIX   = /orchestration/v1

Ejemplo completo de endpoint:
  https://us-central1-{project}.cloudfunctions.net/orchestrationApi/orchestration/v1/templates

Resumen de endpoints

MétodoPathDescripciónScope
POST/templatesCrear plantillatemplates:write
GET/templatesListar plantillastemplates:read
GET/templates/:idObtener plantillatemplates:read
POST/audiencesCrear audienciaaudiences:write
GET/audiences/:idObtener audienciaaudiences:read
POST/campaignsCrear campañacampaigns:write
GET/campaignsListar campañascampaigns:read
GET/campaigns/:idObtener campañacampaigns:read
POST/campaigns/:id/cancelCancelar campañacampaigns:write
POST/whitelistUpsert whitelistwhitelist:write
POST/webhooks/subscriptionsCrear suscripciónwebhooks:write
GET/webhooks/subscriptionsListar suscripcioneswebhooks:read
GET/webhooks/subscriptions/:idObtener suscripciónwebhooks:read
DELETE/webhooks/subscriptions/:idEliminar suscripciónwebhooks:write
GET/operations/:idEstado de operaciónoperations:read

Autenticación y Headers

Todos los endpoints requieren autenticación Bearer + headers de tenant. Las credenciales se obtienen en el panel de ChatFlowy → Integraciones → API Clients.

Headers requeridos en cada request

HeaderTipoDescripción
AuthorizationstringBearer token. Formato: Bearer {token}
X-ChatFlowy-Company-IdstringID de la empresa. Debe coincidir con el companyId del token.
X-ChatFlowy-Account-IdstringID de la cuenta WhatsApp Business. Debe pertenecer al companyId.
Content-TypestringRequerido en POST: application/json
idempotency-keystringUUID único por operación. Ver sección Idempotencia.
Authorization: Bearer eyJhbGciOi...
X-ChatFlowy-Company-Id: comp_abc123
X-ChatFlowy-Account-Id: acc_xyz789
Content-Type: application/json

Scopes disponibles

Cada cliente de integración tiene asignados uno o más scopes. El scope * otorga acceso total.

ScopeDescripción
templates:readListar y obtener plantillas
templates:writeCrear plantillas y enviarlas a Masiv
campaigns:readListar y obtener campañas
campaigns:writeCrear y cancelar campañas
audiences:readObtener audiencias y su estado de validación
audiences:writeCrear audiencias (todos los modos)
whitelist:writeInsertar y actualizar contactos en whitelist
webhooks:readListar y obtener suscripciones de webhook
webhooks:writeCrear y eliminar suscripciones de webhook
operations:readConsultar estado de operaciones asíncronas
*Acceso total a todos los scopes
info
Verificación de tenant: Si el companyId del token no coincide con el header X-ChatFlowy-Company-Id, el servidor responde 403 TENANT_MISMATCH. Los headers de tenant siempre deben corresponder a las credenciales del Bearer token.

Manejo de Errores

Todos los errores retornan JSON con la estructura {error: {code, message, detail?} }. El campo detail es opcional y provee contexto adicional.

Estructura de respuesta de error

{
  "error": {
    "code": "AUDIENCE_NOT_READY",
    "message": "Audience is not yet validated (status != ready)",
    "detail": "Optional additional context"
  }
}

Catálogo de códigos de error

CódigoHTTPDescripción
INVALID_REQUEST400Campo faltante, tipo incorrecto o valor fuera de rango.
UNAUTHORIZED401Bearer token ausente, inválido o inactivo.
FORBIDDEN403Acceso denegado al recurso solicitado.
NOT_FOUND404El recurso no existe o no pertenece a este tenant.
CONFLICT409Nombre duplicado, estado inválido para la operación, etc.
RATE_LIMITED429Demasiadas solicitudes en un periodo corto.
INTERNAL_ERROR500Error inesperado del servidor. Reportar con timestamp.
TENANT_MISMATCH403El companyId del token no coincide con X-ChatFlowy-Company-Id.
SCOPE_DENIED403El token no tiene el scope requerido para este endpoint.
IDEMPOTENCY_CONFLICT409Misma Idempotency-Key enviada con payload diferente.
AUDIENCE_NOT_READY409La audiencia no ha terminado de validarse (status ≠ READY).
TEMPLATE_NOT_APPROVED409La plantilla no está aprobada (status ≠ APPROVED).
OPERATION_LOCKED409La operación ya está siendo procesada por otro worker.
MASIV_ERROR502El proveedor Masiv rechazó la solicitud. Solo aplica a POST /templates.
MASIV_MISSING_ID502Masiv respondió OK pero no devolvió un templateId válido.

Idempotencia

El header idempotency-key previene operaciones duplicadas en caso de timeouts, reintentos de red o errores transitorios. Recomendado para todos los endpoints POST.

timer
TTL del caché
24 horas
tag
Header
idempotency-key
verified
Soportado en
POST /templates, /audiences, /campaigns

Comportamiento

check_circle
Misma key + mismo payload: Retorna la respuesta cacheada. No se ejecuta la operación de nuevo.
warning
Misma key + payload diferente: Retorna 409 IDEMPOTENCY_CONFLICT.
info
Sin header: La solicitud se procesa normalmente sin protección de idempotencia.
POST /orchestration/v1/campaigns
idempotency-key: 550e8400-e29b-41d4-a716-446655440000

// Primera llamada → 202 + campaña creada
// Misma key, mismo payload → 202 + respuesta cacheada (no se crea otra campaña)
// Misma key, payload diferente → 409 IDEMPOTENCY_CONFLICT

Plantillas

Las plantillas son mensajes predefinidos y aprobados por Meta/WhatsApp. Una plantilla debe tener estado APPROVED antes de poder usarse en campañas. La aprobación la gestiona el proveedor Masiv y típicamente toma 24–48 horas hábiles.

POST/orchestration/v1/templates
templates:writeIdempotente

Crea una nueva plantilla y la envía a Masiv para su revisión y aprobación por Meta/WhatsApp. Solo se guarda en Firestore si Masiv retorna un ID válido.

Body — Campos requeridos

CampoTipoDescripción
hsmNamestringNombre técnico único de la plantilla. Se normaliza automáticamente: minúsculas, sin acentos, espacios→guiones bajos. Máx. 512 chars.
hsmTextstringCuerpo del mensaje. Variables con doble llave: {{NOMBRE}}. No puede empezar ni terminar con una variable.
languageenum"Español" | "Inglés"
categoryenum"Marketing" | "Utility"

Body — Campos opcionales

CampoTipoDescripción
headerstringEncabezado visible del mensaje. Sin emojis ni markdown.
footerstringPie de página del mensaje.
variablesstring[]Lista de nombres de variables. Si se omite, se auto-extrae del hsmText.
buttonsobjectBotones interactivos. Ver tabla de botones abajo.

Ejemplo — Con Quick Reply

POST /orchestration/v1/templates
Authorization: Bearer {token}
X-ChatFlowy-Company-Id: {companyId}
X-ChatFlowy-Account-Id: {accountId}
Content-Type: application/json

{
  "hsmName": "pago_vencido_v1",
  "hsmText": "Hola {{NOMBRE}}, tu pago de ${{MONTO}} está vencido desde {{FECHA}}. Regularízalo para evitar cargos adicionales.",
  "language": "Español",
  "category": "Marketing",
  "header": "Aviso de Pago",
  "footer": "Autopartes Orión • No responder a este mensaje",
  "buttons": {
    "type": "QUICK_REPLY",
    "items": [
      { "type": "QUICK_REPLY", "text": "Ya pagué" },
      { "type": "QUICK_REPLY", "text": "Necesito más tiempo" }
    ]
  }
}

Ejemplo — Con Call To Action

// Ejemplo con botones CALL_TO_ACTION
{
  "hsmName": "bienvenida_con_portal_v1",
  "hsmText": "Hola {{NOMBRE}}, bienvenido a Autopartes Orión. Accede a tu portal de cliente.",
  "language": "Español",
  "category": "Utility",
  "buttons": {
    "type": "CALL_TO_ACTION",
    "items": [
      { "type": "URL",          "text": "Abrir portal",  "url": "https://portal.empresa.com" },
      { "type": "PHONE_NUMBER", "text": "Llamar soporte", "phoneNumber": "5512345678" }
    ]
  }
}

Respuesta — 201 Created

HTTP 201 Created
{
  "id": "tpl_9kX2mN8pQr",
  "hsmName": "pago_vencido_v1",
  "hsmTemplateId": "16641",
  "status": "PENDING",
  "message": "Template submitted to Masiv and saved."
}

Especificación de botones

QUICK_REPLY — hasta 10 botones
CampoTipoDescripción
typestring"QUICK_REPLY"
textstringTexto del botón (máx. 20 chars recomendado)
CALL_TO_ACTION — hasta 2 URL + 1 PHONE_NUMBER
CampoTipoDescripción
typestring"URL" | "PHONE_NUMBER"
textstringEtiqueta del botón
urlstringURL del botón (requerido para tipo URL)
phoneNumberstringNúmero de teléfono (requerido para tipo PHONE_NUMBER). Ej: "5512345678"
201 Plantilla creada y enviada a Masiv.
400 Campo inválido (INVALID_REQUEST). hsmText empieza/termina con variable, variables inconsistentes, etc.
409 Nombre duplicado (CONFLICT). Ya existe una plantilla con ese hsmName en la empresa.
502 Masiv rechazó la solicitud (MASIV_ERROR). El body incluye el detalle de Masiv.
GET/orchestration/v1/templates
templates:read

Retorna todas las plantillas de la empresa ordenadas por fecha de creación descendente.

Respuesta — Objeto ExternalTemplate

CampoTipoDescripción
idstringID único Firestore
hsmNamestringNombre técnico normalizado
hsmTextstringCuerpo del mensaje con variables
statusenumPENDING | SUBMITTED | APPROVED | REJECTED | DISABLED
variablesstring[]Nombres de variables extraídos del texto
categorystring"Marketing" | "Utility"
languagestring"Español" | "Inglés"
headerstringEncabezado del mensaje
footerstringPie de página del mensaje
providerIdstring | nullID numérico de Masiv. Null mientras PENDING.
accountobject{id, name} — cuenta WhatsApp Business
createdAtstringISO 8601
updatedAtstring | nullISO 8601

Respuesta — 200 OK

HTTP 200 OK
{
  "data": [
    {
      "id": "tpl_9kX2mN8pQr",
      "hsmName": "pago_vencido_v1",
      "hsmText": "Hola {{NOMBRE}}, tu pago de ${{MONTO}} está vencido...",
      "status": "APPROVED",
      "variables": ["NOMBRE", "MONTO", "FECHA"],
      "category": "Marketing",
      "language": "Español",
      "header": "Aviso de Pago",
      "footer": "Autopartes Orión • No responder",
      "isMedia": false,
      "providerId": "16641",
      "companyId": "comp_abc123",
      "account": { "id": "acc_xyz789", "name": "WhatsApp Principal" },
      "createdAt": "2026-02-15T18:30:00.000Z",
      "updatedAt": "2026-02-16T09:15:00.000Z"
    }
  ],
  "total": 1
}
GET/orchestration/v1/templates/{templateId}
templates:read

Retorna una plantilla específica. Útil para polling del estado de aprobación.

200 Objeto ExternalTemplate (misma estructura que en el listado).
404 La plantilla no existe o no pertenece a esta empresa.

Audiencias

Una audiencia es la lista de destinatarios para una campaña. Soporta 3 modos de creación. Tras la creación, el sistema valida automáticamente los teléfonos, deduplica y verifica variables de plantilla. La audiencia debe alcanzar el estado READY antes de usarse en una campaña.

list_alt
INLINE_NUMBERS
Array JSON de filas con teléfono y variables. Ideal para integración directa desde código.
folder_open
FILE_BASE64
Archivo CSV o XLSX codificado en Base64. Ideal cuando el archivo ya existe.
link
EXISTING_AUDIENCE
Vincular una audiencia ya existente y validada. No crea un nuevo documento.
POST/orchestration/v1/audiences
audiences:writeIdempotente

Crea una nueva audiencia o vincula una existente. El campo mode determina qué campos adicionales se requieren.

Campos por modo

CampoTipoModoDescripción
modeenumTodosINLINE_NUMBERS | FILE_BASE64 | EXISTING_AUDIENCE
namestringINLINE, FILENombre único de la audiencia en la empresa.
existingAudienceIdstringEXISTINGID de la audiencia ya validada a vincular.
rowsobject[]INLINEArray de filas. Cada fila necesita un campo de teléfono + variables de plantilla.
fileBase64stringFILEContenido del archivo CSV/XLSX en Base64. Acepta data URI: data:text/csv;base64,...
fileNamestringFILENombre del archivo con extensión. Ayuda a detectar XLSX vs CSV.
templateIdstringINLINE, FILESi se provee, valida que la plantilla sea APPROVED y que las columnas de sus variables existan en el archivo.
warning
Columnas de teléfono aceptadas:telefono, phone, numero, celular, whatsapp (sin importar mayúsculas ni acentos).
Normalización de encabezados: todos los headers se convierten a MAYÚSCULAS, sin acentos, espacios→guiones_bajos. Ej: "Número de Teléfono" → "NUMERO_DE_TELEFONO".

Ejemplo — INLINE_NUMBERS

// Modo INLINE_NUMBERS
POST /orchestration/v1/audiences
Content-Type: application/json

{
  "mode": "INLINE_NUMBERS",
  "name": "Cartera Vencida Marzo 2026",
  "templateId": "tpl_9kX2mN8pQr",
  "rows": [
    { "telefono": "5512345678", "NOMBRE": "Juan Pérez",  "MONTO": "1500.00", "FECHA": "2026-02-01" },
    { "telefono": "5598765432", "NOMBRE": "Ana García",  "MONTO": "2300.50", "FECHA": "2026-01-28" },
    { "telefono": "5556789012", "NOMBRE": "Luis Torres", "MONTO": "875.00",  "FECHA": "2026-02-10" }
  ]
}

Ejemplo — FILE_BASE64

// Modo FILE_BASE64
{
  "mode": "FILE_BASE64",
  "name": "Cartera Vencida Marzo 2026",
  "fileName": "cartera_marzo.csv",
  "templateId": "tpl_9kX2mN8pQr",
  "fileBase64": "dGVsZWZvbm8sTk9NQlJFLE1PVlRPCjU1MTIzNDU2NzgsSk..."
}

// También acepta XLSX:
{
  "mode": "FILE_BASE64",
  "name": "Cartera Vencida Marzo 2026",
  "fileName": "cartera_marzo.xlsx",
  "fileBase64": "UEsDBBQABgAIAAAAIQD..."
}

Ejemplo — EXISTING_AUDIENCE

// Modo EXISTING_AUDIENCE — vincular audiencia ya validada
{
  "mode": "EXISTING_AUDIENCE",
  "existingAudienceId": "aud_7vBnKp3mRx"
}

Respuesta — 202 Accepted (INLINE / FILE)

HTTP 202 Accepted
{
  "id": "aud_7vBnKp3mRx",
  "status": "UPLOADED",
  "message": "Audience uploaded. Validation will begin automatically."
}

Respuesta — 200 OK (EXISTING)

HTTP 200 OK  (modo EXISTING_AUDIENCE)
{
  "id": "aud_7vBnKp3mRx",
  "name": "Cartera Vencida Febrero 2026",
  "status": "READY",
  "templateId": "tpl_9kX2mN8pQr",
  "stats": {
    "totalRows": 3,
    "validRows": 3,
    "invalidRows": 0,
    "uniquePhones": 3,
    "droppedDuplicates": 0
  },
  "companyId": "comp_abc123",
  "createdAt": "2026-02-18T10:00:00.000Z",
  "linked": true
}

Normalización de teléfonos mexicanos

Entrada              →  Resultado interno
──────────────────────────────────────────────────────
"5512345678"         →  525512345678    (10 dígitos, agrega 52)
"525512345678"       →  525512345678    (ya correcto)
"5215512345678"      →  525512345678    (521 + 10 dígitos, quita el 1)
"+52 55 1234-5678"   →  525512345678    (separadores ignorados)
"0052 55 1234 5678"  →  525512345678    (prefijo 00 eliminado)
"1234"               →  INVÁLIDO (no cumple /^52\d{10}$/)
202 Audiencia creada (INLINE / FILE). Validación asíncrona iniciada automáticamente.
200 Audiencia vinculada (EXISTING). Retorna estado actual + linked: true.
400 Sin columna de teléfono, columnas de variables faltantes, archivo inválido.
409 Nombre de audiencia ya existe en la empresa (CONFLICT).
409 La plantilla no está APPROVED (TEMPLATE_NOT_APPROVED).
GET/orchestration/v1/audiences/{audienceId}
audiences:read

Retorna el estado actual de una audiencia incluyendo las estadísticas de validación. Usar para polling hasta que status === "READY".

Respuesta — 200 OK

HTTP 200 OK
{
  "id": "aud_7vBnKp3mRx",
  "name": "Cartera Vencida Marzo 2026",
  "status": "READY",
  "templateId": "tpl_9kX2mN8pQr",
  "stats": {
    "totalRows": 150,
    "validRows": 147,
    "invalidRows": 3,
    "uniquePhones": 147,
    "droppedDuplicates": 0
  },
  "companyId": "comp_abc123",
  "createdAt": "2026-02-20T14:30:00.000Z",
  "updatedAt": "2026-02-20T14:32:15.000Z"
}
totalRows — Filas procesadas por el validador
validRows — Filas con teléfono válido + variables completas
invalidRows — Filas descartadas por teléfono inválido o variables faltantes
uniquePhones — Teléfonos distintos tras deduplicación
droppedDuplicates — Filas eliminadas por teléfono duplicado

Campañas

Una campaña envía una plantilla aprobada a todos los destinatarios de una audiencia. Puede ejecutarse de forma inmediata o programarse para una fecha futura. La plantilla debe ser APPROVED y la audiencia debe ser READY.

POST/orchestration/v1/campaigns
campaigns:writeIdempotente

Crea y lanza una campaña. Si se provee scheduledAt, la campaña queda en estado SCHEDULED y se ejecuta automáticamente en la fecha indicada (precisión ±5 min).

Body

CampoTipoDescripción
namestringNombre descriptivo de la campaña.
templateIdstringID de plantilla APPROVED.
audienceIdstringID de audiencia READY.
scheduledAtstringFecha/hora de ejecución programada. Ver tabla de formatos.

Formatos de scheduledAt aceptados

EjemploInterpretación
"2026-03-15 22:00"10:00 PM México City (UTC-6)
"2026-03-15 10:00pm"10:00 PM México City
"15/03/2026 22:00"DD/MM/YYYY, México City
"2026-03-15"Medianoche México City
"2026-03-15T22:00:00.000Z"ISO 8601 UTC explícito
"2026-03-15T16:00:00-06:00"ISO 8601 con offset explícito
schedule
El campo scheduledAt debe ser al menos 10 minutos en el futuro. Fechas sin timezone se asumen como México City (UTC-6).

Ejemplo — Campaña inmediata

// Campaña inmediata
POST /orchestration/v1/campaigns
Content-Type: application/json

{
  "name": "Recordatorio Pago Marzo 2026",
  "templateId": "tpl_9kX2mN8pQr",
  "audienceId": "aud_7vBnKp3mRx"
}

Ejemplo — Campaña programada

// Campaña programada — múltiples formatos de fecha aceptados
{
  "name": "Promoción Semana Santa",
  "templateId": "tpl_9kX2mN8pQr",
  "audienceId": "aud_7vBnKp3mRx",
  "scheduledAt": "2026-03-15 10:00"
}

// Otros formatos válidos (todos asumen México City si no hay timezone):
// "2026-03-15 10:00pm"           → 10:00 PM CDMX
// "15/03/2026 10:00"             → DD/MM/YYYY HH:MM CDMX
// "2026-03-15"                   → Medianoche CDMX
// "2026-03-15T10:00:00.000Z"     → UTC explícito
// "2026-03-15T10:00:00-06:00"    → Offset explícito
// Mínimo: 10 minutos en el futuro

Respuesta — 202 Accepted (inmediata)

HTTP 202 Accepted
{
  "id": "cmp_4dLnRk9wTy",
  "name": "Recordatorio Pago Marzo 2026",
  "status": "RUNNING",
  "templateId": "tpl_9kX2mN8pQr",
  "audienceId": "aud_7vBnKp3mRx",
  "companyId": "comp_abc123",
  "account": { "id": "acc_xyz789", "name": "WhatsApp Principal" },
  "sent": 0,
  "rejected": 0,
  "failed": 0,
  "createdAt": "2026-02-21T22:00:00.000Z",
  "message": "Campaign created and queued for execution."
}

Respuesta — 202 Accepted (programada)

HTTP 202 Accepted
{
  "id": "cmp_8xMpQv2nFg",
  "status": "SCHEDULED",
  "scheduledAt": "2026-03-15T16:00:00.000Z",
  "message": "Campaign scheduled successfully."
}
202 Campaña creada. status=RUNNING (inmediata) o SCHEDULED (programada).
409 Plantilla no aprobada (TEMPLATE_NOT_APPROVED) o audiencia no lista (AUDIENCE_NOT_READY).
400 scheduledAt inválido o menor a 10 minutos en el futuro.
GET/orchestration/v1/campaigns
campaigns:read

Lista las campañas de la empresa, ordenadas por fecha de creación descendente.

Query params

ParamTipoDefaultDescripción
limitnumber50Máximo de campañas a retornar. Máximo absoluto: 200.

Respuesta — 200 OK

HTTP 200 OK
{
  "data": [
    {
      "id": "cmp_4dLnRk9wTy",
      "name": "Recordatorio Pago Marzo 2026",
      "status": "COMPLETED",
      "templateId": "tpl_9kX2mN8pQr",
      "audienceId": "aud_7vBnKp3mRx",
      "account": { "id": "acc_xyz789", "name": "WhatsApp Principal" },
      "sent": 145,
      "rejected": 2,
      "failed": 0,
      "createdAt": "2026-02-21T22:00:00.000Z",
      "updatedAt": "2026-02-21T22:04:30.000Z"
    }
  ],
  "total": 1
}
GET/orchestration/v1/campaigns/{campaignId}
campaigns:read

Obtiene el estado actual de una campaña específica. Usar para polling de sent, rejected y failed.

200 Objeto ExternalCampaign con contadores de envío actualizados.
404 Campaña no encontrada o no pertenece a esta empresa.
POST/orchestration/v1/campaigns/{campaignId}/cancel
campaigns:write

Cancela una campaña. No se puede cancelar una campaña en estado COMPLETED o COMPLETED_WITH_ERRORS.

// POST /orchestration/v1/campaigns/{campaignId}/cancel
HTTP 200 OK
{
  "id": "cmp_8xMpQv2nFg",
  "status": "CANCELLED"
}
200 Campaña cancelada exitosamente.
409 No se puede cancelar una campaña ya completada (CONFLICT).
404 Campaña no encontrada.

Whitelist

Inserta o actualiza contactos en la whitelist. Los contactos de la whitelist son los únicos que pueden recibir mensajes de la cuenta. La operación es un upsert idempotente — repetir la misma fila no crea duplicados (el DocId es determinístico: phone_companyId).

POST/orchestration/v1/whitelist
whitelist:write

Upsert de hasta 20,000 contactos por llamada. Los teléfonos se normalizan automáticamente.

Estructura de cada fila en rows[]

CampoAlias aceptadosReq.Descripción
phonetelefono, numero, numeroTelefonoTeléfono del contacto.
nombrename, nombreDeudor, deudorNoNombre del contacto.
identificadoridentifier, id_cliente, idClienteNoID del cliente en tu sistema.
labelslabel, etiquetas, tagsNoEtiquetas. Array o string separado por "/". Se convierten a CamelCase. Ej: ["ClienteVIP"]
urllink, enlaceNoURL de perfil o portal del cliente.

Ejemplo

POST /orchestration/v1/whitelist
Content-Type: application/json

{
  "rows": [
    {
      "phone": "5512345678",
      "nombre": "Juan Pérez",
      "identificador": "CLI-00123",
      "labels": ["ClienteVIP", "CarteraActiva"],
      "url": "https://portal.empresa.com/clientes/00123"
    },
    {
      "telefono": "+52 55 9876-5432",
      "nombre": "Ana García",
      "identificador": "CLI-00456"
    }
  ]
}

Respuesta — 200 OK

HTTP 200 OK
{
  "upsertedCount": 2,
  "skippedCount": 0,
  "skippedReturned": 0,
  "skipped": []
}

// Ejemplo con un teléfono inválido:
{
  "upsertedCount": 1,
  "skippedCount": 1,
  "skippedReturned": 1,
  "skipped": [
    {
      "reason": "Invalid phone. Expected 52 + 10 digits (received: "123")",
      "row": { "phone": "123", "nombre": "Dato inválido" }
    }
  ]
}

Webhooks

Los webhooks eliminan la necesidad de polling. ChatFlowy envía un HTTP POST a tu endpoint registrado cuando ocurren eventos (cambio de estado de plantilla, audiencia o campaña). Los intentos fallidos se reintenten con backoff exponencial.

POST/orchestration/v1/webhooks/subscriptions
webhooks:write

Crea una suscripción de webhook. El secret solo se retorna en este momento — guárdalo de forma segura.

Body

CampoTipoDescripción
urlstringURL HTTPS de tu endpoint que recibirá los eventos.
eventsstring[]Lista de eventos. Usa ["*"] para todos los eventos.
lock
Secret único: El campo secret de la respuesta solo se muestra una vez. Guárdalo en un gestor de secretos seguro (AWS Secrets Manager, GCP Secret Manager, etc.). No se puede recuperar después.

Ejemplo

POST /orchestration/v1/webhooks/subscriptions
Content-Type: application/json

{
  "url": "https://mi-servidor.com/hooks/chatflowy",
  "events": [
    "template.status.changed",
    "campaign.validation.ready",
    "campaign.execution.completed",
    "campaign.execution.completed_with_errors"
  ]
}

// Para recibir todos los eventos:
{ "url": "https://mi-servidor.com/hooks/chatflowy", "events": ["*"] }

Respuesta — 201 Created

HTTP 201 Created
{
  "id": "whs_3fRnKm9pBx",
  "url": "https://mi-servidor.com/hooks/chatflowy",
  "events": ["template.status.changed", "campaign.validation.ready", "campaign.execution.completed"],
  "status": "ACTIVE",
  "secret": "a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
  "message": "Subscription created. Store the secret securely – it will not be shown again."
}
GET/orchestration/v1/webhooks/subscriptions
webhooks:read

Lista las suscripciones activas del tenant. El secret no se incluye en la respuesta.

GET/orchestration/v1/webhooks/subscriptions/{subscriptionId}
webhooks:read

Obtiene una suscripción específica. Retorna {id, url, events, status, createdAt}.

DELETE/orchestration/v1/webhooks/subscriptions/{subscriptionId}
webhooks:write

Desactiva la suscripción (status → INACTIVE). Retorna {id, status: INACTIVE}.

Catálogo de eventos

EventoDisparadorPayload (resumen)
template.status.changedEl estado de una plantilla cambia (cualquier transición)templateId, statusBefore, statusAfter, template.{id, hsmName, status, companyId, accountId}
campaign.validation.readyAudiencia validada exitosamente (status → READY)audienceId, statusBefore, statusAfter, audience.{id, name, status, stats, companyId, accountId}
campaign.validation.failedValidación de audiencia fallida (status → INVALID | ERROR)audienceId, statusBefore, statusAfter, audience.{id, name, status, stats, ...}
campaign.validation.changedCualquier cambio de estado en audienciaaudienceId, statusBefore, statusAfter, audience{...}
campaign.execution.startedCampaña comienza ejecución (status → RUNNING)campaignId, statusBefore, statusAfter, campaign.{id, name, status, sent, rejected, failed, ...}
campaign.execution.completedCampaña completada (status → COMPLETED)campaignId, statusBefore, statusAfter, campaign{...}
campaign.execution.completed_with_errorsCompletada con errores (status → COMPLETED_WITH_ERRORS)campaignId, statusBefore, statusAfter, campaign{...}
campaign.execution.failedCampaña falló (status → ERROR)campaignId, statusBefore, statusAfter, campaign{...}
campaign.execution.changedCualquier cambio de estado en campañacampaignId, statusBefore, statusAfter, campaign{...}
*Wildcard — recibe todos los eventos anterioresDepende del evento

Ejemplo de payload recibido

// Evento: campaign.execution.completed
// POST a tu endpoint:
{
  "event": "campaign.execution.completed",
  "campaignId": "cmp_4dLnRk9wTy",
  "statusBefore": "RUNNING",
  "statusAfter": "COMPLETED",
  "campaign": {
    "id": "cmp_4dLnRk9wTy",
    "name": "Recordatorio Pago Marzo 2026",
    "status": "COMPLETED",
    "companyId": "comp_abc123",
    "accountId": "acc_xyz789",
    "sent": 145,
    "rejected": 2,
    "failed": 0
  }
}

Verificación de firma

Cada evento incluye el header X-ChatFlowy-Signature: t={t},v1={sig} donde sig = HMAC-SHA256(secret, t + "." + rawBody). Verifica siempre la firma antes de procesar el evento.

// Header recibido en tu endpoint:
X-ChatFlowy-Signature: t=1708555200,v1=a3f8b2c1d4e...

// Verificación (Node.js):
const [tPart, vPart] = signature.split(',');
const t  = tPart.split('=')[1];
const v1 = vPart.split('=')[1];

const rawBody    = JSON.stringify(body);
const expected   = crypto
  .createHmac('sha256', YOUR_SECRET)
  .update(t + '.' + rawBody)
  .digest('hex');

const isValid = crypto.timingSafeEqual(
  Buffer.from(v1),
  Buffer.from(expected)
);

Operaciones Asíncronas

Las operaciones asíncronas rastrean el estado de trabajos de larga duración. Disponible para consulta de estado.

GET/orchestration/v1/operations/{operationId}
operations:read

Retorna el estado de una operación asíncrona.

HTTP 200 OK
{
  "id": "op_5kRmQn7vXz",
  "type": "TEMPLATE_SUBMIT",
  "status": "COMPLETED",
  "payload": { "templateId": "tpl_9kX2mN8pQr" },
  "result": { "hsmTemplateId": "16641" },
  "attempts": 1,
  "createdAt": "2026-02-15T18:30:00.000Z",
  "updatedAt": "2026-02-15T18:30:05.000Z",
  "completedAt": "2026-02-15T18:30:05.000Z"
}

Catálogo de Estados

Plantillas

PENDINGEnviada a Masiv. Aún sin ID numérico asignado.
SUBMITTEDTiene ID de Masiv. Esperando aprobación de Meta/WhatsApp.
APPROVEDAprobada. Lista para usar en campañas.
REJECTEDRechazada por Meta/Masiv. Requiere correcciones.
DISABLEDPausada temporalmente. No disponible para campañas.

Campañas

SCHEDULEDProgramada para ejecución futura.
RUNNINGEn ejecución activa. Mensajes siendo enviados.
COMPLETEDCompletada. Todos los mensajes procesados.
COMPLETED_WITH_ERRORSCompletada pero algunos mensajes fallaron.
ERRORFalló por error del sistema.
CANCELLEDCancelada manualmente antes de completar.

Audiencias

UPLOADEDArchivo subido. Validación pendiente.
VALIDATINGValidación en proceso (dedup, normalización, vars).
READYValidada. Lista para usar en campañas.
INVALIDSin filas válidas tras la validación.
ERRORError del sistema durante la validación.

Operaciones

PENDINGEn cola, esperando ser procesada.
IN_PROGRESSSiendo procesada actualmente.
COMPLETEDCompletada exitosamente.
FAILEDFalló tras los reintentos disponibles.

Flujos de Integración

Guías paso a paso para los escenarios más comunes de integración.

1

Flujo 1 — Primera campaña (plantilla + audiencia nuevas)

Flujo completo desde cero para enviar una campaña por primera vez.

1
POST/templates

Crear la plantilla. El nombre se normaliza automáticamente (minúsculas, sin acentos). Retorna status: PENDING.

2
WEBHOOKtemplate.status.changed

Esperar hasta que statusAfter === APPROVED (24–48h hábiles). Alternativa: poll GET /templates/:id.

3
POST/audiences

Subir audiencia (INLINE_NUMBERS o FILE_BASE64). Retorna status: UPLOADED.

4
WEBHOOKcampaign.validation.ready

Esperar statusAfter === READY. Alternativa: poll GET /audiences/:id cada 5s.

5
POST/campaigns

Crear campaña con templateId + audienceId. La ejecución comienza de inmediato.

6
WEBHOOKcampaign.execution.completed

Recibir resultado con sent/rejected/failed. Alternativa: poll GET /campaigns/:id.

2

Flujo 2 — Campaña con audiencia existente

Reusar una audiencia ya validada para una nueva campaña.

1
POST/audiences (EXISTING_AUDIENCE)

Pasar existingAudienceId. Retorna estado actual + linked: true. La audiencia debe tener status READY.

2
POST/campaigns

Usar el mismo audienceId. No se crea un documento de audiencia nuevo.

3

Flujo 3 — Campaña programada

Programar una campaña para ejecutarse automáticamente en una fecha futura.

1
POST/campaigns (con scheduledAt)

scheduledAt mínimo 10 min en el futuro. Fechas sin timezone = México City (UTC-6). Status inicial: SCHEDULED.

2
CRONWorker automático (±5 min)

El sistema ejecuta la campaña automáticamente. Precisión: ±5 minutos.

3
POST/campaigns/:id/cancel (opcional)

Cancelar antes de la fecha programada si es necesario.

4

Flujo 4 — Setup de Webhooks (recomendado para producción)

Eliminar el polling y recibir notificaciones automáticas en tiempo real.

1
POST/webhooks/subscriptions

Registrar endpoint con los eventos necesarios (ej: ["template.status.changed","campaign.validation.ready","campaign.execution.completed"]). Guardar el secret de forma segura — solo se muestra una vez.

2
IMPLVerificar firma en tu servidor

Extraer t y v1 del header X-ChatFlowy-Signature. Calcular HMAC-SHA256(secret, t+"."+rawBody) y comparar con v1.

3
READYOperar sin polling

Los eventos llegarán automáticamente. Reintentos con backoff exponencial si tu servidor no responde 2xx.

Glosario

Definición de términos usados en la API y la documentación.

accountId
Identificador de la cuenta WhatsApp Business dentro de una empresa. Debe pertenecer al companyId del tenant (validado contra customer_accounts).
Audiencia
Lista de destinatarios (teléfonos + variables de plantilla) para una campaña. Pasa por un proceso de validación asíncrona automática.
Bearer Token
Credencial de autenticación enviada en el header Authorization. Se almacena como hash SHA-256 en la colección integrationClients.
BSP
Business Solution Provider. Intermediario autorizado por Meta para enviar mensajes de WhatsApp. ChatFlowy usa Masiv como BSP.
Campaña
Envío masivo de un mensaje (plantilla aprobada) a todos los destinatarios de una audiencia.
CDMX / UTC-6
Zona horaria por defecto (America/Mexico_City) para fechas sin timezone explícito en el campo scheduledAt.
companyId
Identificador único de la empresa en ChatFlowy. Usado para aislar datos entre clientes (multi-tenant).
hsmName
Nombre técnico de la plantilla. Solo a-z, 0-9 y guiones bajos. Se normaliza automáticamente (minúsculas, sin acentos). Debe ser único por empresa.
hsmText
Cuerpo del mensaje de la plantilla. Puede contener variables con doble llave: {{NOMBRE}}, {{MONTO}}. No puede empezar ni terminar con una variable.
Idempotencia
Garantía de que una operación produce el mismo resultado si se ejecuta múltiples veces. Habilitada enviando el header idempotency-key con un UUID único.
Job
Unidad interna de trabajo que ejecuta el envío masivo. Se crea en la colección jobs y activa el trigger onJobCreated del sistema legacy.
Masiv
BSP (Business Solution Provider) autorizado por Meta. Gestiona la aprobación de plantillas y el enrutamiento de mensajes WhatsApp.
NDJSON.GZ
Formato interno: Newline-Delimited JSON comprimido con gzip. Usado para almacenar audiencias validadas en Google Cloud Storage.
orqStatus
Campo canónico de estado para el módulo de orquestación en documentos de campaña. Tiene precedencia sobre el campo status legacy.
Plantilla
Mensaje predefinido y aprobado por Meta que define el contenido WhatsApp. Puede contener variables, header, footer y botones interactivos.
scheduledAt
Fecha y hora de ejecución programada de una campaña. Acepta múltiples formatos. Sin timezone → México City (UTC-6).
scope
Permiso granular asignado al cliente de integración. Determina a qué endpoints tiene acceso.
Tenant
Contexto de operación identificado por companyId + accountId. Se envía en los headers X-ChatFlowy-Company-Id y X-ChatFlowy-Account-Id.
Webhook
Notificación HTTP POST enviada a tu servidor cuando ocurre un evento (cambio de estado de plantilla, audiencia o campaña).
Whitelist
Lista de números autorizados para recibir mensajes. Almacenada en la colección clients con listType: "whitelist".

Preguntas Frecuentes

support_agent
¿No encontraste tu respuesta?

Contacta al equipo de ChatFlowy para soporte técnico de integraciones.