Iniciar sesiónEmpezar prueba gratis
Meta Tech Provider

Enviar y recibir WhatsApp desde PHP y Laravel

Recibir es una ruta de Laravel que responde al GET de verificación de Meta y acepta POST, leyendo los parámetros como hub_mode, hub_verify_token y hub_challenge porque PHP convierte los puntos en guiones bajos. Enviar es un POST con Http::withToken() al endpoint de mensajes de la Cloud API, con el phone number id y el token del número. No hace falta ningún SDK.

hub_challenge

PHP convierte los puntos de los nombres de variables entrantes en guiones bajos, así que hub.challenge se lee como hub_challenge.

routes/api.php

Las rutas API sin estado de Laravel, creadas por install:api, se sirven bajo /api y fuera de la protección CSRF del grupo web.

v25.0

La versión de Graph API de los ejemplos actuales de Meta para enviar mensajes, en POST /{phone-number-id}/messages.

¿Cómo se reciben mensajes de WhatsApp en Laravel?

Con un GET y un POST en la misma ruta de routes/api.php. Ejecuta primero php artisan install:api si el archivo no existe. Las rutas API no tienen estado y quedan fuera del grupo web, así que el POST de Meta no se rechaza por falta de token CSRF, que es lo que ocurre con una ruta en routes/web.php.

El GET responde a la verificación de Meta. Meta envía hub.mode, hub.verify_token y hub.challenge, pero PHP reescribe los puntos de los nombres de variables entrantes como guiones bajos, así que Laravel ve hub_mode, hub_verify_token y hub_challenge. El código que lee los nombres con punto obtiene null y nunca verifica. Devuelve el challenge como texto plano, no con response()->json().

El POST entrega el payload a un job en cola y devuelve 200 de inmediato. Pon una clave larga y aleatoria en la ruta y compruébala en ambas rutas: Meta firma los envíos con el secreto de la app del Tech Provider, así que la cabecera de firma no se puede validar de tu lado.

routes/api.php: verification and event intake
use App\Jobs\HandleWhatsAppEvent;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/whatsapp/{key}', function (Request $request, string $key) {
    abort_unless(hash_equals(config('services.whatsapp.url_key'), $key), 404);

    // PHP turns hub.mode into hub_mode: read the underscored names
    if ($request->query('hub_mode') === 'subscribe'
        && hash_equals(config('services.whatsapp.verify_token'),
                       (string) $request->query('hub_verify_token'))) {
        return response((string) $request->query('hub_challenge'), 200)
            ->header('Content-Type', 'text/plain');
    }

    abort(403);
});

Route::post('/whatsapp/{key}', function (Request $request, string $key) {
    abort_unless(hash_equals(config('services.whatsapp.url_key'), $key), 404);

    HandleWhatsAppEvent::dispatch($request->json()->all());

    return response('', 200);
});

¿Cómo se envía un mensaje de WhatsApp desde Laravel?

Con el cliente HTTP de Laravel: un POST al endpoint de mensajes del phone number id, con el token de acceso como bearer token. Ambos valores salen de la conexión, se muestran en el panel y los devuelve get_api_credentials, y van en config/services.php respaldados por variables de entorno en lugar de en el código.

La forma del cuerpo depende del momento. Dentro de las 24 horas del último mensaje del cliente, sirve un objeto text, como en el ejemplo. Fuera de esa ventana Meta solo acepta un objeto template que nombre una plantilla aprobada y su idioma, y el texto libre falla con el error 131047. La respuesta es el lugar para verlo: una llamada fallida devuelve un objeto error cuyo código dice qué salió mal, así que regístralo en lugar de descartarlo.

Nada en la petición es específico de EasyCoexistence. Es la llamada de la documentación de Meta, así que cualquier cliente HTTP de PHP, Guzzle o cURL puro, funciona igual.

Send a text reply inside the 24 hour window
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.whatsapp.token'))
    ->post('https://graph.facebook.com/v25.0/'
        . config('services.whatsapp.phone_number_id') . '/messages', [
        'messaging_product' => 'whatsapp',
        'to' => '14155550188', // digits only, with country code
        'type' => 'text',
        'text' => ['body' => 'Thanks, we are on it.'],
    ]);

if ($response->failed()) {
    // error.code 131047: outside the window, send a template instead
    logger()->warning('WhatsApp send failed', $response->json('error', []));
}

¿Qué necesita un handler de Laravel en producción además de eso?

Una cola real, deduplicación y una comprobación de la ventana.

La cola importa porque Meta reintenta una respuesta lenta. HandleWhatsAppEvent::dispatch() solo vuelve rápido cuando la conexión de cola es real y hay un worker en marcha. Con el driver sync el job se ejecuta dentro de la petición, y una llamada lenta a un modelo o una escritura en la base de datos acaban en una entrega duplicada.

Deduplica por el id del mensaje, el wamid de cada mensaje, porque los reintentos son lo normal y no la excepción. Basta un índice único en esa columna, o un bloqueo de caché con ella como clave.

Y guarda cuándo escribió por última vez cada contacto. Con esa marca de tiempo, la ruta de envío elige sola entre texto y plantilla, en lugar de enterarse por el error 131047 cuando el cliente ya estaba esperando. Las respuestas escritas desde el teléfono en la app de WhatsApp Business llegan como eventos echo a la misma ruta, así que el handler también puede ver cuándo una persona ha tomado una conversación.

Errores comunes

  • Leer hub.challenge con punto. PHP lo renombró a hub_challenge, así que el handler devuelve un cuerpo vacío y Meta nunca verifica.
  • Poner el webhook en routes/web.php. El POST de Meta no lleva token CSRF y se rechaza con 419.
  • Despachar a la cola sync. El job se ejecuta antes del 200, el procesamiento lento se reintenta y los mensajes se procesan dos veces.
  • Devolver el challenge con response()->json(). Las comillas alrededor del valor hacen fallar la comprobación de Meta.
Hacerlo con EasyCoexistence

Conecta el número en easycoexistence.com, configura el destino del webhook en tu ruta de Laravel con su clave en la ruta, y lee el phone number id y el token en el panel. Desde US$ 9 por número al mes, bajando hasta US$ 1 a medida que se agregan números, con los primeros 3 días gratis.

Preguntas frecuentes

¿Hay un SDK de PHP?

Nosotros no ofrecemos ninguno, y no hace falta. La API es de Meta, la documenta Meta, y el cliente HTTP de Laravel o Guzzle la alcanzan directamente.

¿Funciona sin Laravel?

Sí. En PHP puro lee $_GET['hub_challenge'], devuélvelo con echo como text/plain y envía con cURL. La regla de los guiones bajos se aplica a cualquier framework de PHP.

¿Por qué la URL necesita una clave en la ruta?

Porque es la autenticación que funciona. La firma de Meta usa el secreto de la app del Tech Provider, que nunca se comparte, así que el secreto de la URL es lo que impide que desconocidos envíen eventos falsos.

¿El equipo puede seguir usando el teléfono?

Sí. Coexistencia mantiene en funcionamiento la app de WhatsApp Business en el número, y los mensajes enviados desde el teléfono llegan a tu ruta como eventos echo.

Sigue leyendo

¿Listo para empezar?

Configura WhatsApp Coexistence en minutos, no en meses. La app sigue funcionando en el teléfono.

Empezar prueba gratis

Verificado el

Enviar WhatsApp desde PHP y Laravel