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 y template.
  • 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 en queue=whatsapp.
  • GET /webhook/whatsapp
    • Verificación con hub.verify_token y devolución de hub.challenge.
  • 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 (o client_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 🚀

About Author

Giss Trejo

0 0 votos
Article Rating
Suscribir
Notificar de
guest
0 Comments
La mas nueva
Más antiguo Más votada
Comentarios.
Ver todos los comentarios
0
¿Te gusta este articulo? por favor comentax