Objetivo: Tener un servicio propio (Laravel + MySQL) que encole, envíe y rastree mensajes de WhatsApp usando la Cloud API oficial. Incluye colas, reintentos, webhook y métricas básicas.
Arquitectura mínima
- API interna (REST):
POST /messages
para encolar;GET/POST /webhook/whatsapp
para verificación y eventos. - Worker de colas (Redis Queue): desacopla el envío y maneja reintentos/backoff.
- Cliente HTTP a WhatsApp (Graph API): envío de
text
ytemplate
. - Persistencia: MySQL (trazabilidad), Redis (colas/rate-limit).
- Observabilidad: logs estructurados, métricas por estado, alertas.
Esquema de datos
Tabla messages
(campos clave):
to_phone
,type (text|template)
,body_text
|template_name/lang/vars
wa_message_id
,status (queued|sending|sent|delivered|read|failed)
attempt_count
,error_code
,error_detail
dedup_hash
(idempotencia),created_at
,updated_at
Índices: status
, wa_message_id
, to_phone
, uk(dedup_hash)
.
Endpoints
POST /messages
- Valida entrada, calcula
dedup_hash
, crea registro y publica Job enqueue=whatsapp
.
- Valida entrada, calcula
GET /webhook/whatsapp
- Verificación con
hub.verify_token
y devolución dehub.challenge
.
- Verificación con
POST /webhook/whatsapp
- Procesa statuses (sent/delivered/read/failed) y opcionalmente mensajes entrantes.
Envío (Laravel HTTP client)
.env
:
WHATSAPP_GRAPH_VERSION=v20.0
WHATSAPP_PHONE_NUMBER_ID=xxxxxxxxxxxxxxx
WHATSAPP_ACCESS_TOKEN=EAAG...
Envío de texto (resumen):
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('WHATSAPP_ACCESS_TOKEN'))
->post("https://graph.facebook.com/".env('WHATSAPP_GRAPH_VERSION')."/".env('WHATSAPP_PHONE_NUMBER_ID')."/messages", [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'text',
'text' => ['body' => $text],
])->json();
Envío de plantilla:
Http::withToken(env('WHATSAPP_ACCESS_TOKEN'))
->post("https://graph.facebook.com/".env('WHATSAPP_GRAPH_VERSION')."/".env('WHATSAPP_PHONE_NUMBER_ID')."/messages", [
'messaging_product'=>'whatsapp',
'to'=>$to,
'type'=>'template',
'template'=>[
'name'=>$tpl, 'language'=>['code'=>$lang],
'components'=>[['type'=>'body','parameters'=>array_map(fn($v)=>['type'=>'text','text'=>$v], $vars)]]
]
]);
Job con reintentos y backoff
class SendWhatsAppMessage implements ShouldQueue {
public $tries = 5;
public function backoff(): array { return [2,5,15,60,180]; }
public function handle(WhatsAppClient $wa) {
$m = Message::findOrFail($this->messageId);
if (!in_array($m->status, ['queued','failed'])) return;
$m->update(['status'=>'sending','attempt_count'=>$m->attempt_count+1]);
try {
$resp = $m->type==='text'
? $wa->sendText($m->to_phone, $m->body_text ?? '')
: $wa->sendTemplate($m->to_phone, $m->template_name, $m->template_lang, $m->metadata ?? []);
$m->update(['status'=>'sent','wa_message_id'=>$resp['messages'][0]['id'] ?? null,'error_code'=>null,'error_detail'=>null]);
} catch (\Throwable $e) {
$m->update(['status'=>$this->attempts()>= $this->tries ? 'failed':'queued','error_code'=> (string)$e->getCode(),'error_detail'=>substr($e->getMessage(),0,900)]);
if ($this->attempts() < $this->tries) $this->release($this->backoff()[$this->attempts()-1] ?? 300);
}
}
}
Webhook (verificación + estados)
// GET /webhook/whatsapp
if ($req->query('hub.mode')==='subscribe' && $req->query('hub.verify_token')===config('services.whatsapp.verify_token')) {
return response($req->query('hub.challenge'), 200);
}
return response('forbidden', 403);
// POST /webhook/whatsapp (fragmento)
foreach ($payload['entry'] ?? [] as $e)
foreach (($e['changes'] ?? []) as $c)
foreach (($c['value']['statuses'] ?? []) as $st)
Message::where('wa_message_id', $st['id'] ?? '')
->update([
'status'=> ['sent'=>'sent','delivered'=>'delivered','read'=>'read','failed'=>'failed'][$st['status']] ?? 'sent',
'error_code'=>$st['errors'][0]['code'] ?? null,
'error_detail'=>$st['errors'][0]['message'] ?? null
]);
Reglas clave de negocio
- Ventana de 24 h: fuera de 24 h solo plantillas aprobadas.
- Idempotencia:
dedup_hash
(oclient_dedup
) para evitar duplicados por reintentos del cliente. - Rate-limit/429: exponential backoff + “circuit breaker” si suben los fallos.
- Privacidad: no loguear cuerpo completo ni teléfonos en claro; enmascarar y definir retención.
Métricas rápidas (SQL)
- Estados últimos 7 días:
SELECT status, COUNT(*) FROM messages
WHERE created_at >= NOW() - INTERVAL 7 DAY
GROUP BY status;
- Top errores 30 días:
SELECT error_code, COUNT(*) FROM messages
WHERE status='failed' AND created_at >= NOW() - INTERVAL 30 DAY
GROUP BY error_code ORDER BY COUNT(*) DESC LIMIT 10;
Checklist de despliegue
- TLS válido en el webhook.
- Variables en
.env
; tokens rotados. QUEUE_CONNECTION=redis
y workers supervisados (Supervisor/Horizon).- Migraciones + índices listos.
- Webhook verificado (GET y POST).
- Alertas en
failed
y picos de 429/5xx.
Ejemplo de solicitud a tu API
POST /api/messages
{
"to": "5215555555555",
"type": "template",
"template": { "name": "bienvenida_clientes", "lang": "es_MX", "vars": ["Gisselle","Sorteos Express"] },
"client_dedup": "req-12345"
}
¿Listo para dominar la API de WhatsApp?
Empieza gratis hoy en WhatzMeApi.com 🚀