Search by

funnelchat20 / wapi-gateway

marcelo.marinhaacho

WAPI Gateway: librería Composer para integrar ZApi, UAZAPI, Funapi y Meta con webhooks automáticos

Package info

github.com/Funnelchat20/wapi-gateway

pkg:composer/funnelchat20/wapi-gateway

Statistics

Installs: 7 895

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 7

v0.23.9 2026-10-30 13:10 UTC

README

Latest Version on Packagist Total Downloads

SDK puro para integrar proveedores de WhatsApp soportados por Funnelchat: Z-API, UAZAPI, Funapi (Whatsmeow Bridge) y Meta (WhatsApp Cloud), sin rutas ni middlewares. La app host consume métodos programáticos mediante un Facade y decide si expone endpoints propios.

⚠️ Estado: Esta librería está en desarrollo activo (v0.x.x). La API puede cambiar entre versiones menores. Se recomienda fijar versiones específicas en composer.json hasta v1.0.0.

Requisitos

  • PHP ^8.2
  • Laravel ^11.0 | ^12.0

Instalación

composer require funnelchat20/wapi-gateway

Publicar configuración

php artisan vendor:publish --tag=wapi-config

Configuración

Variables de entorno por proveedor

Global (Todos los proveedores)

# URL base para webhooks (requerida para recibir notificaciones)
WEBHOOK_BASE_URL=https://tu-app.com

Z-API

ZAPI_TOKEN=tu_token_aqui
ZAPI_CLIENT_TOKEN=tu_client_token
ZAPI_TIMEOUT=29
ZAPI_MAX_ATTEMPTS=2
ZAPI_RETRY_DELAY=500

UAZAPI

UAZAPI_BASE_URL=https://funnelchat.uazapi.com
UAZAPI_ADMIN_TOKEN=tu_admin_token
UAZAPI_TIMEOUT=120

# Opcional: deshabilitar configuración automática de webhooks
UAZAPI_AUTO_CONFIGURE_WEBHOOKS=true

Funapi (Whatsmeow Bridge)

FUNAPI_BASE_URL=https://api.funapi.example.com
FUNAPI_TOKEN=tu_token
FUNAPI_CLIENT_TOKEN=tu_client_token
FUNAPI_TIMEOUT=29
FUNAPI_MAX_ATTEMPTS=2
FUNAPI_RETRY_DELAY=500

Meta (WhatsApp Cloud)

META_APP_ID=tu_app_id
AWS_BUCKET_URL=https://tu-bucket.s3.amazonaws.com

Uso básico

use Funnelchat\WapiGateway\Facades\WapiGateway;
use Funnelchat\WapiGateway\Enums\ProviderEnum;

// Enviar mensaje de texto
$response = WapiGateway::messages(ProviderEnum::ZApi)
    ->sendText($uid, $token, $phone, 'Hola mundo', [
        'delayMessage' => 0,
        'delayTyping' => 0,
        'retry' => true,  // Habilita reintentos automáticos
    ]);

if (isset($response['error'])) {
    // Manejar error
}

// Crear instancia
$data = WapiGateway::instances(ProviderEnum::ZApi)->create($userId, $deviceId);
// $data = ['uid' => '...', 'token' => '...'] o ['error' => '...']

// Obtener estado
$status = WapiGateway::instances(ProviderEnum::ZApi)->status($uid, $token);
// Incluye 'accountStatus' y 'qrCode' automáticamente

// Configurar (o deshabilitar) el proxy de una instancia existente
$res = WapiGateway::instances(ProviderEnum::ZApi)->configureProxy($uid, $token, $proxyUrl);
// $proxyUrl = 'http://user:pass@host:port' (esquemas: http/https/socks4/socks5)
// $proxyUrl = null | '' deshabilita y limpia el proxy
// $res = ['proxy_configured' => true] | ['proxy_configured' => false, 'error' => '...']
// Nunca lanza: los fallos se loguean y se devuelven en 'error'.
// Soportado en zapi/zapilite/funapi; Meta/Uazapi devuelven proxy_configured=false.
// zapi/zapilite requieren la clave config `proxy_url` (endpoint integrator configure-proxy).

Typing "escribiendo…" provider-agnóstico

sendText/sendFile/sendButtons/sendOptionList/sendLink/sendButtonLink aceptan la opción typing (opt-in):

$response = WapiGateway::messages($provider)->sendText($uid, $token, $phone, $msg, [
    'typing' => [
        'lastInboundId' => $wamid,   // wamid del último inbound del contacto (puede ser null)
        'delaySeconds'  => 3,        // duración deseada del typing
    ],
]);
  • Z-API / Funapi / Z-API Lite: se traduce a delayTyping — el provider retiene el mensaje server-side y muestra typing. Un solo request, cero bloqueo. Un delayTyping explícito en options tiene precedencia sobre typing.
  • Meta (WhatsApp Cloud): si hay lastInboundId, el SDK dispara el indicador (status: read + typing_indicator) inmediatamente antes del send, best-effort. Sin lastInboundId (o fuera de la ventana de 24h) envía normal. El typing nunca falla el send: el indicador corre acotado por un timeout propio de 5s (peor caso agrega esa espera antes del send) y si falla, el resultado incluye 'typing_result' => ['error' => ...] para loguearlo.

Para orquestar la espera en providers que no la resuelven server-side, branchear una sola vez sobre la capability (no sobre identidad de provider):

if (WapiGateway::messages($provider)->handlesTypingDelayServerSide()) {
    // Un solo call con la opción 'typing'.
} else {
    // Meta: sendTypingIndicator($uid, $token, $wamid) + job diferido propio para el send.
}

UAZAPI (deprecado) ignora la opción typing: su delay server-side sólo se cablea vía delayMessage. En sendButtonLink() sobre Z-API/Funapi el delayTyping se manda igual, pero el endpoint send-button-actions no lo documenta: ahí el typing es best-effort.

Marcar como leído y «escribiendo…» (ReadReceiptsContract)

Dos métodos para el visto (los tildes azules) de un mensaje recibido. Viven en una interfaz aparte, ReadReceiptsContract, y no en MessagesContract: así nadie que implemente MessagesContract tiene que cambiar.

use Funnelchat\WapiGateway\Contracts\ReadReceiptsContract;

$client = WapiGateway::messages($provider);

if ($client instanceof ReadReceiptsContract) {
    // $chat: teléfono o id de grupo (Z-API lo exige; Meta lo ignora).
    $res = $client->markMessageAsRead($uid, $token, $chat, $messageId);

    // Sólo Cloud API: el mismo visto, con «escribiendo…».
    $res = $client->markMessageAsReadWithTyping($uid, $token, $chat, $wamid);
}

// ['success' => true]
// ['error' => ReadReceiptsContract::UNSUPPORTED, 'message' => '...']  (no se mandó nada)
// ['error' => ...]                                                     (el proveedor lo rechazó)
Método Meta Z-API Z-API Lite FunApi
markMessageAsRead() ✅ ✅ ✅ ❌ unsupported
markMessageAsReadWithTyping() ✅ ❌ unsupported ❌ unsupported ❌ unsupported
  • Meta: POST /{phone_number_id}/messages con {"messaging_product":"whatsapp","status":"read","message_id":"<WAMID>"}. Con «escribiendo…» se agrega "typing_indicator":{"type":"text"}. Sólo cuenta como éxito si Meta contesta {"success": true}. Meta marca también los mensajes anteriores de la conversación, acepta mensajes de hasta 30 días y quita el indicador cuando el negocio responde o a los 25 segundos. Fuentes: mark-message-as-read y typing-indicators.
  • Z-API / Z-API Lite: POST .../read-message con {"phone", "messageId"}, que responde {"value": true} (read-message). En QR no hay «escribiendo…» suelto: sólo existe como delayTyping de un envío (la opción typing de arriba).
  • FunApi: la colección del bridge no publica un endpoint de visto, así que se rechaza sin llamar.
  • Nunca lanzan excepción. Si falla la conexión, el error que devuelven tiene el token de la instancia tapado.

sendTypingIndicator() sigue igual (en QR devuelve success sin hacer nada).

Nota de voz (sendFile con voiceNote)

$res = WapiGateway::messages($provider)->sendFile($uid, $token, $phone, $audioUrl, [
    'voiceNote' => true,
]);
Provider Qué se manda Archivos
Meta audio.voice = true sólo .ogg / .opus (OGG con OPUS, regla de Meta)
Z-API / Z-API Lite waveform: true en send-audio cualquier audio
FunApi — ['error' => 'unsupported']

Es la misma opción, con los mismos códigos de error, que send-file de wapi (Funnelchat20/wapi#393). Si el archivo no sirve como nota de voz, devuelve ['error' => 'invalid_voice_note_file', 'message' => ...]. Todo rechazo pasa antes de cualquier request: en Meta tampoco se dispara el typing. Sin la opción, el payload queda byte a byte igual que antes. Z-API no documenta un flag de push-to-talk, y waveform («Whether the audio will be sent with sound waves or not») es lo más cercano.

Archivos por URL (sendFile con type)

El integrador pasa una URL y el SDK la manda tal cual al proveedor: una URL firmada (S3, CloudFront, etc.) conserva su firma.

$res = WapiGateway::messages($provider)->sendFile($uid, $token, $phone, $fileUrl, [
    'type'     => 'document',      // opcional: image | video | audio | document | sticker
    'fileName' => 'Contrato.pdf',  // opcional
]);
  • Con type el SDK no adivina el tipo por la URL. Vacío o null cuenta como ausente; mayúsculas y espacios se ignoran. Cualquier otro valor devuelve ['error' => 'Invalid file type: expected one of image, video, audio, document, sticker'] sin hacer ningún request.
  • Sin type el tipo sale de la extensión del path de la URL, sin query ni fragmento y en minúsculas: https://b.s3.amazonaws.com/a/foto.JPG?X-Amz-Signature=... → imagen. Si el path no tiene extensión se usa la de fileName. Si nada da una extensión conocida: ['error' => 'Invalid file extension'], como antes.
  • Compatibilidad: una URL que ya funcionaba arma el mismo request, incluso la que traía la extensión sólo en la query (.../download?file=foto.jpg). Esa lectura se conserva cuando el path no da una extensión conocida. Sólo cambia una URL cuyo path y cuya query terminan en extensiones conocidas distintas (doc.pdf?v=foto.png): ahora manda la del path.
  • Z-API / Z-API Lite necesitan la extensión para send-document/{ext}. Un documento sin extensión en el path ni en fileName devuelve ['error' => 'Z-API needs the file extension to send a document: ...'] sin request. Con type => 'document' sirve cualquier extensión (.odt, etc.). FunApi, UAZAPI y Meta no la necesitan.
  • El async automático de Z-API / FunApi se aplica a todo video, también al de type => 'video'.
  • En Meta, la nota de voz (voiceNote) sigue pidiendo .ogg / .opus: la extensión del path, o la de fileName.

Extensiones que reconoce cada proveedor sin type:

Tipo Meta Z-API / Z-API Lite / FunApi UAZAPI
image jpg, jpeg, png jpg, jpeg, png jpg, jpeg, png, gif
video mp4, 3gp, mov, gif mp4, mov, gif mp4, mov, avi, wmv
audio aac, amr, mp3, m4a, ogg, opus, wav aac, oga, ogg, mp3, m4a, opus, wav aac, oga, ogg, mp3, m4a, opus, wav
document pdf, txt, doc, docx, xls, xlsx, ppt, pptx pdf, doc, docx, xls, xlsx, ppt, pptx, csv, txt pdf, doc, docx, xls, xlsx, ppt, pptx, csv, txt
sticker webp webp, svg webp, svg

Preview de links (sendLink)

Provider Tarjeta de preview Título / descripción / imagen custom
Z-API / Z-API Lite / Funapi ✅ vía send-link ✅ (title, linkDescription, image)
UAZAPI ✅ vía linkPreview ✅
Meta (WhatsApp Cloud) ⚠️ best-effort vía preview_url ❌ no soportado por la API

En Meta el cuerpo y la URL viajan como un único mensaje de texto con text.preview_url = true. Dos techos propios de la Cloud API:

  1. La tarjeta la arma Meta scrapeando las etiquetas Open Graph de la página destino. No se pueden pasar título, descripción ni imagen custom en un mensaje de texto — a diferencia de send-link de Z-API/Funapi, esas opciones se ignoran.
  2. Meta sólo renderiza el preview cuando ya existe relación previa con el número (plantilla enviada antes, click-to-chat, o el negocio guardado en la agenda del contacto). Aun con el flag es best-effort.

Además, el cliente previsualiza la primera URL del body: si $message ya trae una URL, esa gana sobre $linkUrl (que se concatena al final, separado por un espacio — misma composición que persiste conversations en el historial).

El contrato que sí se garantiza en los cuatro providers: el link llega y es clickeable. Para una tarjeta con contenido controlado por el emisor en Meta, usar sendButtonLink() (interactive.cta_url, nativo).

Envío a BSUID (Business-Scoped User ID) en Meta

Desde el rollout de Meta del 2026-07-29, los contactos que llegan por username sin interacción reciente no traen teléfono: el inbound sólo trae un Business-Scoped User ID. Para poder contestarles, MetaClient detecta el destino y arma el payload en consecuencia:

Destino Campo en el payload
Teléfono (5491123456789) to
BSUID (CO.1021346770783737) recipient
BSUID enterprise (US.ENT.11815799212886844830) recipient
BSUID con otros segmentos (US.XYZ.118157992128868) recipient

Nunca se mandan los dos. Si to y recipient viajan juntos, Meta resuelve el teléfono e ignora el BSUID, y la respuesta termina en la conversación equivocada.

El destino se pasa igual que siempre — no hay parámetro ni flag nuevo:

// Anda idéntico con teléfono o con BSUID.
$meta->sendText($wabaId, $token, 'CO.1021346770783737', 'Hola!');

La detección vive en WhatsAppCloudHelper::isBsuid(): código de país alpha-2 + punto + id alfanumérico de hasta 128 chars, con cualquier cantidad de segmentos intermedios (ENT es el que se vio hasta ahora, no el único posible).

La regla tiene que quedar al menos tan permisiva como ContactService::isNonPhoneIdentifier() en conversations, que es la que decide que el contacto no tiene teléfono y guarda el BSUID en whatsapp_id. Si algo que allá se guarda como no-teléfono acá se lee como teléfono, sale en to y el envío falla — justo el bug que esto viene a arreglar. Por eso se aceptan también el código de país en minúsculas y los segmentos arbitrarios. La única diferencia buscada es el sufijo @lid de Z-API, sin equivalente en la Cloud API.

Ser permisivo no cuesta nada en el eje peligroso: un teléfono no puede matchear el patrón porque no tiene ni letras ni puntos. La seguridad viene de exigir dos letras y un punto, no de apretar el alfabeto. Un test diferencial (test_is_at_least_as_permissive_as_the_conversations_mirror) lleva el patrón del espejo copiado y falla si las dos reglas se separan.

Cobertura: sendText, sendFile, sendLocation, sendButtons, sendButtonLink, sendOptionList, sendLink, sendTemplate y sendContact. sendPoll y sendPtv siguen siendo Not supported en Meta, y sendTypingIndicator no toma destino (va contra el wamid del inbound).

Dos límites que impone Meta, no el SDK:

  1. Un destino BSUID acepta menos mensajes que un teléfono. Cuando el mensaje no aplica, Meta responde 131062 ("Business-scoped User ID (BSUID) recipients are not supported for this message") y el SDK lo marca en el resultado para que el caller no reintente el mismo payload en vano:

    $res = $meta->sendTemplate($wabaId, $token, $bsuid, 'otp_login', 'es', []);
    if ($res['unsupported_for_bsuid'] ?? false) {
        // $res['error_code'] === 131062 — reintentar igual nunca va a andar:
        // hay que reenviar algo que el destinatario sí soporte, o conseguir el
        // teléfono (ver el punto 2, que es el caso documentado hoy).
    }

    El error crudo de Meta se preserva intacto en $res['error']; las claves extra sólo aparecen con 131062 y cuando el destino que se armó fue un BSUID, así que los envíos a teléfono no cambian.

  2. Las plantillas de autenticación one-tap, zero-tap y copy-code siguen exigiendo teléfono por regla de Meta — hoy es el único caso documentado que dispara el 131062 del punto anterior. El SDK no puede detectarlo por adelantado (la categoría no se deduce de los argumentos de sendTemplate()), así que evitar esa combinación queda del lado del caller.

Compatibilidad de Proveedores

📨 Mensajería

Método Z-API UAZAPI Funapi Meta
sendText() ✅ ✅ ✅ ✅
sendFile() ✅ ✅ ✅ ✅
sendLocation() ✅ ✅ ✅ ✅
sendButtons() ✅ ✅ ⚠️ ✅
sendButtonLink() ✅ ✅ ⚠️ ✅
sendOptionList() ✅ ✅ ⚠️ ✅
sendPoll() ✅ ✅ ⚠️ ✅
sendLink() ✅ ✅ ⚠️ ✅
sendEvent() ✅ ✅ ⚠️ ✅
sendTemplate() ✅ ✅ ⚠️ ✅
pinMessage() ✅ ✅ ⚠️ ❌

🔧 Instancias

Método Z-API UAZAPI Funapi Meta
create() ✅ ✅ ✅ ✅
status() ✅ ✅ ✅ ✅
qrCode() ✅ ✅ ✅ ✅
logout() ✅ ✅ ✅ ✅
reboot() ✅ ✅ ✅ ✅
me() ✅ ✅ ✅ ✅
checkPhone() ✅ ✅ ❌ ✅
subscribe() ✅ ✅ ✅ ✅
unsubscribe() ✅ ✅ ✅ ✅

👥 Grupos

Método Z-API UAZAPI Funapi Meta
groups() ✅ ✅ ✅ ❌
group() ✅ ✅ ✅ ❌
createGroup() ✅ ✅ ✅ ❌
updateGroupName() ✅ ✅ ✅ ❌
updateGroupDescription() ✅ ✅ ✅ ❌
updateGroupSettings() ✅ ✅ ✅ ❌
updateGroupPhoto() ✅ ✅ ✅ ❌
addParticipants() ✅ ✅ ✅ ❌
addAdmins() ✅ ✅ ✅ ❌
removeParticipants() ✅ ✅ ✅ ❌
removeAdmins() ✅ ✅ ✅ ❌
leaveGroup() ✅ ✅ ✅ ❌
adGroups() ✅ ✅ ✅ ❌
groupInvitationMetadata() ✅ ✅ ✅ ❌
groupInvitationLink() ✅ ✅ ✅ ❌
lightGroupMetadata() ✅ ✅ ✅ ❌
groupMetadata() ✅ ✅ ✅ ❌

🌐 Grupos API oficial — WhatsApp Cloud API (CloudApiGroupsContract)

Solo Meta: la feature nativa de Grupos del Business Platform (Official Business Account, tope de 8 participantes, join-request en vez de add-participant directo). Contrato separado de GroupsContract — no comparte métodos con Z-API/UAZAPI/Funapi porque el modelo de capacidades no es compatible (sin add-admin, sin comunidades/newsletters, con join requests).

Método Meta
createGroup() ✅
deleteGroup() ✅
group() ✅
groups() ✅
updateGroupSettings() ✅
getGroupInviteLink() ✅
resetGroupInviteLink() ✅
sendGroupInviteTemplate() ✅
removeGroupParticipant() ✅
getGroupJoinRequests() ✅
approveGroupJoinRequest() ✅
rejectGroupJoinRequest() ✅

⚠️ El endpoint/método existe y está implementado contra la referencia pública de Meta, pero el body exacto de removeGroupParticipant(), approveGroupJoinRequest() y rejectGroupJoinRequest() (array participants con user/user_id) no está confirmado contra la referencia completa del Graph API — validar contra un número Cloud API Groups real antes de depender de esto en producción.

📇 Contactos

Método Z-API UAZAPI Funapi Meta
contact() ✅ ✅ ✅ ❌
contacts() ✅ ✅ ✅ ❌
sendContact() ✅ ✅ ✅ ✅
addContacts() ✅ ✅ ✅ ❌

🏘️ Comunidades

Método Z-API UAZAPI Funapi Meta
community() ✅ ✅ ⚠️ ❌
communities() ✅ ✅ ⚠️ ❌
communitiesMetadata() ✅ ✅ ⚠️ ❌

📢 Newsletters / Canales

Método Z-API UAZAPI Funapi Meta
createNewsletter() ✅ ✅ ⚠️ ❌
updateNewsletterName() ✅ ✅ ⚠️ ❌
updateNewsletterDescription() ✅ ✅ ⚠️ ❌
updateNewsletterPicture() ✅ ✅ ⚠️ ❌
newsletters() ✅ ✅ ⚠️ ❌
newsletterMetadata() ✅ ✅ ⚠️ ❌

📋 Cola de Mensajes

Método Z-API UAZAPI Funapi Meta
showQueue() ✅ ✅ ✅ ❌
queueCount() ✅ ✅ ✅ ❌
deleteQueueMessage() ✅ ✅ ✅ ❌
clearQueue() ✅ ✅ ✅ ❌

📝 Templates (Solo Meta)

Método Meta
listTemplates() ✅
getTemplate() ✅
createTemplate() ✅
updateTemplate() ✅
deleteTemplate() ✅
uploadHeaderHandle() ✅

Leyenda:

  • ✅ Completamente funcional
  • ⚠️ En desarrollo (retorna error descriptivo)
  • ❌ No soportado por el proveedor

Funcionalidades Avanzadas

Administración de Templates (Meta/WhatsApp Cloud)

use Funnelchat\WapiGateway\Facades\WapiGateway;
use Funnelchat\WapiGateway\Enums\ProviderEnum;

// Listar templates
$templates = WapiGateway::templates(ProviderEnum::WhatsAppCloud)
    ->listTemplates($wabaId, $token, [
        'limit' => 20,
        'after' => 'cursor123'  // Para paginación
    ]);

// Crear template
$template = WapiGateway::templates(ProviderEnum::WhatsAppCloud)
    ->createTemplate($wabaId, $token, [
        'name' => 'welcome_message',
        'language_code' => 'es',
        'category' => 'MARKETING',
        'header' => 'Bienvenido',
        'body' => 'Hola {{1}}, gracias por contactarnos',
        'footer' => 'Equipo de soporte',
        'buttons' => [
            ['type' => 'URL', 'text' => 'Visitar sitio', 'url' => 'https://example.com']
        ]
    ]);

// Eliminar template
$result = WapiGateway::templates(ProviderEnum::WhatsAppCloud)
    ->deleteTemplate($wabaId, $token, 'welcome_message', $templateUid);

Eliminación Concurrente de Mensajes

$deleteRequests = [
    ['messageId' => 'msg1', 'phone' => '123456789', 'owner' => true],
    ['messageId' => 'msg2', 'phone' => '987654321', 'owner' => true],
    // ... más mensajes
];

$results = WapiGateway::groups(ProviderEnum::ZApi)
    ->deleteMessagesConcurrently($uid, $token, $deleteRequests);

// Performance: ~2s para 100 mensajes vs ~30s+ secuencial
foreach ($results as $messageId => $result) {
    if (isset($result['error'])) {
        echo "Error eliminando {$messageId}: {$result['error']}";
    }
}

Procesamiento Asíncrono de Videos

// Automático para videos: .mp4, .mov, .gif, o `type => 'video'`
$response = WapiGateway::messages(ProviderEnum::ZApi)
    ->sendFile($uid, $token, $phone, 'https://example.com/video.mp4', [
        'caption' => 'Mi video',
        'retry' => true,
        // 'async' => false  // Desactivar si es necesario
    ]);

Retry Automático

$response = WapiGateway::messages(ProviderEnum::ZApi)
    ->sendText($uid, $token, $phone, 'Mensaje importante', [
        'retry' => true  // Reintenta automáticamente en errores de conexión
    ]);

// El SDK:
// - Solo reintenta en ConnectionException
// - NO reintenta en timeouts (evita duplicados)
// - Configurable vía ZAPI_MAX_ATTEMPTS y ZAPI_RETRY_DELAY

Configuración Automática de Webhooks

El SDK configura webhooks automáticamente al crear instancias para recibir notificaciones en tiempo real de mensajes, conexión/desconexión, y otros eventos.

⚙️ Configuración

Variable requerida:

WEBHOOK_BASE_URL=https://tu-app.com

Si no defines WEBHOOK_BASE_URL, el SDK usará APP_URL como fallback. Si ninguna está configurada, no se configurarán webhooks automáticamente.

📡 Webhooks por Proveedor

Z-API

Configurados en el payload de creación de instancia:

  • /webhooks/zapi/received - Mensajes recibidos
  • /webhooks/zapi/received-and-delivery - Mensajes y confirmaciones de entrega
  • /webhooks/zapi/disconnected - Instancia desconectada
  • /webhooks/zapi/connected - Instancia conectada
  • /webhooks/zapi/message-status - Estados de mensajes enviados
  • /webhooks/zapi/block - Bloqueos/desbloqueos

UAZAPI

Configurados vía POST /webhook después de crear la instancia:

  • /webhooks/uazapi/messages - Mensajes recibidos
  • /webhooks/uazapi/messages_update - Actualizaciones de mensajes
  • /webhooks/uazapi/connection - Cambios de conexión
  • /webhooks/uazapi/messages - Eventos de grupos (comparte ruta con messages)

Opciones:

# Deshabilitar webhooks automáticos para UAZAPI
UAZAPI_AUTO_CONFIGURE_WEBHOOKS=false

Funapi

Configurados en el payload de creación de instancia:

  • /webhooks/funapi/received - Mensajes recibidos
  • /webhooks/funapi/received-and-delivery - Mensajes y confirmaciones de entrega
  • /webhooks/funapi/disconnected - Instancia desconectada
  • /webhooks/funapi/connected - Instancia conectada
  • /webhooks/funapi/message-status - Estados de mensajes enviados
  • /webhooks/funapi/block - Bloqueos/desbloqueos

💡 Ejemplo de Uso

// 1. Configurar .env
// WEBHOOK_BASE_URL=https://mi-app.com

// 2. Crear instancia (webhooks se configuran automáticamente)
$instance = WapiGateway::instances(ProviderEnum::Uazapi)->create($userId, $deviceId);
// Los webhooks ya están configurados en UAZAPI

// 3. La app host debe tener rutas para recibir webhooks
Route::post('/webhooks/uazapi/messages', [WebhookController::class, 'uazapiMessages']);
Route::post('/webhooks/uazapi/connection', [WebhookController::class, 'uazapiConnection']);
// etc...

🔍 Logging

El SDK registra automáticamente el éxito/error de la configuración de webhooks:

// Logs en UAZAPI
Log::info('UAZAPI webhook configured successfully', [...]);
Log::warning('UAZAPI webhook configuration failed', [...]);
Log::warning('UAZAPI webhook configuration skipped: WEBHOOK_BASE_URL not configured', [...]);

Device Logging (Request/Response a DB)

El SDK puede loggear cada request HTTP a los proveedores en la tabla device_logs, incluyendo el payload enviado y la respuesta recibida. El logging está deshabilitado por defecto — solo se activa explícitamente en el proyecto que lo necesite.

Configuración

# Habilitar logging a DB (deshabilitado por defecto)
WAPI_GATEWAY_LOGGING_ENABLED=true

# Conexión de base de datos a usar (por defecto: mysql)
WAPI_GATEWAY_DATABASE_CONNECTION=mysql

Importante: No habilites WAPI_GATEWAY_LOGGING_ENABLED=true sin antes publicar y correr la migración. Con queue: sync (común en desarrollo), el job fallará directamente si la tabla no existe.

Migración

La migración no se ejecuta automáticamente al instalar el paquete. Solo debe publicarse en el proyecto que será dueño de la tabla device_logs (actualmente user-api):

# Solo en user-api:
php artisan vendor:publish --tag=wapi-migrations
php artisan migrate

Los demás proyectos que usen el paquete (new-groups, sync-group-api, etc.) no necesitan publicar la migración — solo configurar WAPI_GATEWAY_DATABASE_CONNECTION apuntando a la misma base de datos donde user-api creó la tabla.

Estructura de la tabla device_logs

Campo Tipo Descripción
instance_uid string UID de la instancia de WhatsApp
provider string Proveedor usado (zapi, uazapi, funapi, meta)
method string Método ejecutado (sendText, status, createGroup, etc.)
url text URL completa del request HTTP
request_payload json Body enviado al proveedor (ej: {"phone": "549...", "message": "Hola"})
response_body json Respuesta del proveedor
status_code smallint Código HTTP de respuesta
duration_ms int Tiempo de respuesta en milisegundos
is_error boolean Si el request resultó en error
sent_at timestamp Fecha y hora real del request HTTP (distinto de created_at que es cuando se persistió el registro)

Queue

Los logs se despachan vía el job StoreDeviceLogJob en la cola device-logs. Asegurate de tener un worker procesando esa cola:

php artisan queue:work --queue=device-logs

El dispatch del job está protegido con try-catch — si la infraestructura de cola falla (Redis caído, etc.), el error se loggea pero no interrumpe la operación de WhatsApp que lo originó.

Protección contra payloads grandes

Los payloads de request y response se truncan automáticamente si superan 200KB (para evitar exceder el límite de 256KB de SQS). Cuando esto ocurre, el campo almacena {"_truncated": true, "original_size": 350000}.

Notas

  • Los métodos GET (status, qrCode, me, contact, etc.) registran request_payload como [] ya que no envían body.
  • Los métodos POST (sendText, sendFile, createGroup, addContacts, etc.) registran el payload completo enviado al proveedor.
  • El logging es asíncrono y no impacta el tiempo de respuesta de las operaciones.
  • El job usa 3 reintentos con backoff progresivo (5s, 15s, 30s) y timeout de 10s.

Sistema de Resources

El SDK transforma las respuestas de cada proveedor para mantener compatibilidad con WAPI original:

// Ejemplo: status() con fetch automático de QR
$status = WapiGateway::instances(ProviderEnum::ZApi)->status($uid, $token);
// Retorna: ['accountStatus' => 'authenticated', 'qrCode' => null]
// o: ['accountStatus' => 'got qr code', 'qrCode' => 'data:image/png;base64,...']

// checkPhone() normalizado
$result = WapiGateway::instances(ProviderEnum::ZApi)->checkPhone($uid, $token, $phone);
// Retorna: ['result' => true] (no 'exists', sino 'result')

// me() con campos adicionales
$me = WapiGateway::instances(ProviderEnum::ZApi)->me($uid, $token);
// Retorna: ['phone' => '...', 'name' => '...', 'avatar' => '...', 'locale' => 'es', 'isBusiness' => true]

Limitaciones de Funapi

Funapi (Whatsmeow Bridge) tiene las siguientes limitaciones conocidas:

❌ No Soportado

  • checkPhone() - No existe endpoint equivalente

⚠️ En Desarrollo

Los siguientes métodos retornan error descriptivo indicando que están en desarrollo:

  • sendButtons()
  • sendButtonLink()
  • sendOptionList()
  • sendPoll()
  • sendLink()
  • sendEvent()
  • sendTemplate()

✅ Totalmente Funcional

  • Mensajería básica: texto, archivos, ubicación, contactos
  • Gestión de instancias completa
  • Operaciones de grupos completas
  • Cola de mensajes completa

Manejo de Errores

Todos los métodos retornan arrays con:

  • Éxito: ['messageId' => '...', 'status' => 'queued', ...]
  • Error: ['error' => 'mensaje descriptivo']
$response = WapiGateway::messages(ProviderEnum::ZApi)->sendText(...);

if (isset($response['error'])) {
    // Error normalizado del proveedor
    Log::error('Error enviando mensaje', ['error' => $response['error']]);
    return response()->json(['error' => $response['error']], 409);
}

// Éxito
return response()->json($response);

Integración en Controladores

use Funnelchat\WapiGateway\Facades\WapiGateway;
use Funnelchat\WapiGateway\Enums\ProviderEnum;

class WhatsAppController extends Controller
{
    public function send(Request $request)
    {
        $provider = match($request->provider) {
            'zapi' => ProviderEnum::ZApi,
            'uazapi' => ProviderEnum::Uazapi,
            'funapi' => ProviderEnum::Funapi,
            'meta' => ProviderEnum::WhatsAppCloud,
        };

        $response = WapiGateway::messages($provider)
            ->sendText(
                $request->uid,
                $request->header('token'),
                $request->phone,
                $request->message,
                ['retry' => true]
            );

        return isset($response['error'])
            ? response()->json(['error' => $response['error']], 409)
            : response()->json($response);
    }
}

Changelog

Sin publicar (pendiente de asignar versión)

Added:

  • ✨ CloudApiGroupsContract — WhatsApp Cloud API Groups nativo (Meta, Official Business Account): createGroup(), deleteGroup(), group(), groups(), updateGroupSettings(), getGroupInviteLink(), resetGroupInviteLink(), sendGroupInviteTemplate(), removeGroupParticipant(), getGroupJoinRequests(), approveGroupJoinRequest(), rejectGroupJoinRequest(). Implementado en MetaClient. Contrato deliberadamente separado de GroupsContract (ver sección "Grupos API oficial" arriba) — no lo implementan Z-API/UAZAPI/Funapi.
  • ⚠️ El body de removeGroupParticipant()/approveGroupJoinRequest()/rejectGroupJoinRequest() sigue la convención habitual de Meta (participants: [{user|user_id}]) pero no está confirmado contra la referencia completa del Graph API — validar contra un número real antes de depender de esto en producción.
  • ✨ sendFile() acepta $options['type'] (image, video, audio, document, sticker) para URLs sin extensión — ver Archivos por URL
  • ✨ Meta reconoce todas las extensiones que acepta la Cloud API: 3gp, amr, txt, xls, xlsx, ppt, pptx y webp (sticker)

Fixed:

  • 🐛 sendFile() lee la extensión del path de la URL, no de la URL entera: las URLs firmadas (foto.jpg?X-Amz-Signature=...) ya no fallan con Invalid file extension. Si el path no tiene, usa la de fileName
  • 🐛 Z-API / Z-API Lite: un documento sin extensión devuelve un error claro en vez de armar send-document/ mal
  • 🐛 Meta: el header de sendButtons() también lee la extensión del path de la URL
  • 🐛 Z-API, Z-API Lite, FunApi y UAZAPI mandan como documento .xls, .xlsx, .ppt, .pptx, .csv y .txt sin necesitar type (#82). Meta no adivina .csv: la Cloud API no lo lista entre sus tipos de documento

v0.3.0 - 2025-XX-XX (En desarrollo)

Added:

  • ✨ Soporte completo para Newsletters/Canales: createNewsletter(), updateNewsletterName(), updateNewsletterDescription(), updateNewsletterPicture(), newsletters(), newsletterMetadata()
  • ✨ Soporte para Comunidades: community(), communities(), communitiesMetadata()
  • ✨ Sección de Contactos: contact(), contacts(), sendContact(), addContacts()
  • ✨ Métodos de grupos: adGroups(), groupInvitationMetadata(), groupInvitationLink(), lightGroupMetadata(), groupMetadata()
  • ✨ Método pinMessage() en MessagesContract
  • ✨ Device Logging: StoreDeviceLogJob registra request payload y response en tabla device_logs vía cola asíncrona
  • ✨ Campo sent_at en device_logs para capturar fecha/hora real del request HTTP
  • ✨ Migración publicable vía --tag=wapi-migrations (solo para el proyecto dueño de la tabla)
  • ✨ Trait LogsDeviceRequests — elimina ~160 líneas duplicadas entre los 4 clientes

Fixed:

  • 🐛 request_payload en device_logs ahora contiene el body HTTP real enviado al proveedor en todos los métodos POST (antes se guardaba [])
  • 🐛 StoreDeviceLogJob::dispatch() envuelto en try-catch para no interrumpir operaciones de WhatsApp si la cola falla
  • 🐛 Payloads >200KB se truncan automáticamente para no exceder límite de SQS (256KB)
  • 🐛 ZApiClient::create() ahora pasa instance_uid correctamente (antes pasaba string vacío)
  • 🐛 Variable de entorno WAPI_GATEWAY_DATABASE_CONNECTION consistente entre config y README
  • 🐛 $backoff corregido de [5, 15] a [5, 15, 30] para matchear $tries = 3
  • 🐛 Métodos sendFile, sendLocation, y newsletter updates en FunapiClient/ZApiClient ahora logean el payload del request

Changed:

  • 🔧 Default de logging_enabled cambiado de true a false (opt-in explícito)
  • 🔧 Migración no se auto-carga — solo se publica en el proyecto que la necesite (user-api)
  • 🔧 Casts de DeviceLog ampliados: status_code => integer, duration_ms => integer

v0.2.0 - 2025-01-XX

Added:

  • ✨ Configuración automática de webhooks al crear instancias
  • ✨ Variable de entorno WEBHOOK_BASE_URL para configuración centralizada
  • ✨ Método privado configureWebhooks() en UazapiClient con logging completo
  • ✨ Soporte para deshabilitar webhooks con UAZAPI_AUTO_CONFIGURE_WEBHOOKS=false

Changed:

  • 🔧 ZApiClient, UazapiClient y FunapiClient ahora usan WEBHOOK_BASE_URL en lugar de APP_URL
  • 🔧 Webhooks solo se configuran si WEBHOOK_BASE_URL está definida

v0.1.0 - 2025-01-07

Added:

  • ✨ Sistema completo de Resources para transformación de respuestas
  • ✨ Soporte para Funapi (Whatsmeow Bridge) - 36/44 métodos funcionales
  • ✨ Fetch automático de QR code en método status() cuando la instancia está desconectada
  • ✨ Normalización de respuestas entre proveedores (accountStatus, result, locale, etc.)
  • ✨ 20 Resources implementados (10 Z-API + 10 UAZAPI)
  • ✨ Administración completa de templates para Meta/WhatsApp Cloud
  • ✨ Método deleteMessagesConcurrently() para eliminación paralela de mensajes
  • ✨ Retry automático configurable en métodos de envío
  • ✨ Procesamiento asíncrono automático para videos
  • ✨ Timeouts configurables por proveedor

Fixed:

  • 🐛 Corregidos endpoints de Funapi (/send-image y /send-document sin sufijos)
  • 🐛 Métodos no soportados de Funapi ahora retornan errores descriptivos

Changed:

  • 🔧 Timeout por defecto reducido de 120s a 29s (configurable)
  • 🔧 Retry inteligente: no reintenta en timeouts, solo en ConnectionException

Performance:

  • ⚡ Eliminación de 100 mensajes: ~2s con HTTP Pool vs ~30s+ secuencial
  • ⚡ Reducción de timeouts en envíos de videos gracias a procesamiento async

Roadmap

  • Completar endpoints faltantes de Funapi (botones, listas, encuestas)
  • Tests automatizados para todos los proveedores
  • Documentación de DTOs y respuestas por operación
  • Soporte para webhooks (configuración automática implementada)
  • v1.0.0 - Release estable cuando todos los proveedores estén 100% completos

Contribución

Esta es una librería privada de Funnelchat. Para reportar bugs o solicitar features, contacta a: soporte@funnelchat.io

Licencia

Privado (Funnelchat). Todos los derechos reservados.