@botmesh/meta) un negocio solo puede enviar
mensajes libres (texto, media, botones…) dentro de las 24 horas
siguientes al último mensaje del usuario: la ventana de atención al cliente.
Fuera de ella solo pasan las plantillas aprobadas; Meta rechaza todo lo
demás con el error de Graph 131047, que el provider traduce a
OutsideSessionWindowError.
@botmesh/meta incluye dos ayudas opcionales para esto:
sessionWindow: sigue la ventana de cada chat, reabre los chats cerrados con una plantilla y reintenta el mensaje original cuando el usuario responde.usage: cuenta la información de facturación que Meta adjunta a los webhooks de estado, por chat y por categoría de precio.
@botmesh/meta. Otros providers con una ventana
parecida (error 63016 de Twilio, Gupshup, TikTok) rechazan con
OutsideSessionWindowError, pero no tienen gestor.
Sin el gestor
Por defecto no se sigue nada. Un envío libre fuera de la ventana rechaza y tú decides qué hacer:Activar el gestor
sessionWindow, provider.send() (y por tanto ctx.reply,
bot.send…) pasa por un SessionWindowManager:
- Cada mensaje o interacción entrante (pulsación de botón o de lista) registra la hora del último mensaje del chat. Las reacciones y los estados no abren la ventana.
- Un envío libre a un chat cuya ventana se sabe cerrada (último mensaje
entrante de hace más de 24 h, o Meta informó
131047desde entonces) se salta la petición condenada al fracaso y pasa directamente al paso 4 (proactive, activado por defecto). Un chat que nunca se vio no se considera cerrado: se intenta el envío. - Si un envío libre falla con
OutsideSessionWindowError, la ventana se marca como cerrada. - El gestor envía la plantilla de reapertura, encola el contenido
original y rechaza (o resuelve, ver
onReopened). - Cuando el usuario responde, el contenido encolado se envía solo, en orden, antes de cualquier envío nuevo a ese chat. Tus handlers también se ejecutan con esa respuesta, como siempre.
failed con código 131047 sobre un mensaje libre que
envió el gestor (Meta puede informarlo de forma asíncrona) dispara la misma
reapertura y encolado, notificados por onReport porque send() ya había
resuelto.
Las plantillas (kind: 'template') no pasan por el gestor: siempre se envían
directamente.
Qué hace send() tras reabrir
onReopened: 'reject'(por defecto):send()rechaza conSessionWindowReopenedError, una subclase deOutsideSessionWindowErrorcontemplate,queuedytemplateResult. El código que ya manejaOutsideSessionWindowErrorsigue funcionando.onReopened: 'resolve':send()resuelve con el resultado del envío de la plantilla, yrawes{ queued, template, result }. Úsalo cuando quien envía no deba ver un error (por ejemplo,ctx.replydentro de los handlers).
send() rechaza (con el error
131047 original cuando lo hubo), el resultado es reopen-failed y el
contenido no queda en cola. Si no se resuelve ninguna plantilla para el
contenido, el resultado es no-template y se lanza el error original.
Elegir la plantilla
reopenTemplate: una plantilla para cualquier contenido, o un resolver(ctx) => SessionTemplate | undefined(síncrono o asíncrono).ctxtraechatId,content,reason('error','proactive'o'status') ylastInboundAt. Devuelveundefinedpara no reabrir.reopenTemplateByKind: una plantilla por tipo de contenido (text,image,document…); tiene prioridad sobrereopenTemplate.
Notificaciones
onReport(report) recibe cada resultado (los errores lanzados dentro se
ignoran). report trae chatId, outcome y, según el caso, reason,
content, template, result y error.
Opciones
Persistir el estado de la ventana
Por el store solo pasa el estado de la ventana: por chat,{ lastInboundAt?, closedAt? } (epoch ms). El MemorySessionWindowStore
por defecto está acotado (maxChats, por defecto 10 000) y se pierde al
reiniciar. Implementa SessionWindowStore sobre Redis o SQL para
conservarlo entre reinicios y procesos; los métodos pueden ser síncronos o
asíncronos:
OutboundContent,
así que siempre viven en memoria (acotados) y se pierden al reiniciar. Antes
de entregarle media al gestor, el provider la sube una sola vez (Buffer, ruta
o { stream }) y la cola solo guarda el id de media: los reintentos nunca
vuelven a leer un stream ya consumido ni retienen bytes.
Varios números: forScope
La ventana pertenece a un par número de negocio + usuario. El provider
acota su gestor por phoneNumberId y las claves del store pasan a ser
<phoneNumberId>:<chatId>, así que un mismo store sirve a varios números.
Para compartir un gestor (mismas opciones y store) entre providers, pasa la
instancia; cada provider obtiene su propia vista con
forScope(phoneNumberId), con sus propias claves de ventana, su cola y sus
envíos seguidos:
Consultar un chat
provider.sessionWindow es el gestor (o undefined si no configuraste
sessionWindow):
SESSION_WINDOW_MS (24 h) y SESSION_WINDOW_ERROR_CODE
(131047).
Estabilidad de la API
La API de la ventana de sesión tiene dos partes:- Pública, cubierta por semver: todo lo de esta página que una app configura
o consulta.
SessionWindowManager(constructor,forScope,state,isOpen,expiresAt,pending,clearPending),SessionWindowOptions, el contratoSessionWindowStorey su implementación de referenciaMemorySessionWindowStore,SessionTemplate,ReopenContext,ReopenReason,ReopenTemplateResolver,SessionReport,SessionReportOutcome,SessionWindowReopenedError,SESSION_WINDOW_MSySESSION_WINDOW_ERROR_CODE. @experimental, fuera de semver: los ganchos que mueveCloudAPIProvider,send(),noteInbound()ynoteFailedStatus(), y su tipoRawSend. Pueden cambiar en una versión menor. Llamarlos a mano se salta la subida única de media del provider y su seguimiento de estados: envía a través del provider o del bot.
Costos y uso
Cada eventostatus de la Cloud API trae la información de facturación de
Meta normalizada en DeliveryStatus.platformData:
platformData.pricing:{ billable, pricingModel?, category?, type? }.pricingModelesPMP(precio por mensaje, desde el 2025-07-01) oCBP(el modelo antiguo por conversación);type(PMP) esregular,free_customer_serviceofree_entry_point.platformData.conversation(cuando Meta lo envía):{ id?, originType?, expiresAt? }(expiresAten epoch ms).
provider.usage (un UsageTracker, activado por defecto) agrega ese pricing
por chat y por categoría. Meta repite el pricing en sent, delivered y
read; cada id de mensaje se cuenta una sola vez.
usage: false desactiva el conteo (el pricing sigue llegando a
platformData). También puedes pasar tu propia instancia de UsageTracker,
por ejemplo para compartirla entre providers. estimatedCost es solo la suma
de las tarifas que configuraste: la factura de Meta es la fuente de verdad.
Ver también
- Canales: todos los providers y sus capacidades.
- README de
@botmesh/meta