Índice
Fundamentos
Conectar y enviar
Producción
Escala, Coste y Preguntas Frecuentes
Guía de la API de WhatsApp en PHP
API de WhatsApp con PHPLa Guía de Integración Completa
Llamadas con Guzzle y cURL para conectar una cuenta de WhatsApp, enviar y recibir mensajes, administrar grupos y ejecutar todo esto dentro de un Laravel aplicación, sin registrar una aplicación de Meta Business. Esta es la API de WhatsApp en PHP de extremo a extremo: conexión, webhooks, SaaS multiinquilino y el costo real por mensaje.
Sin verificación de Meta Business
Guzzle y cURL, sin SDKs abandonados
usar GuzzleHttp\Client;
$client = new Cliente([
'base_uri' => 'https://api1.unipile.com:13111',
'headers' => ['X-API-KEY' => $apiKey],
]);
// Proveedor WHATSAPP: sin aplicación Meta, sin WABA
$res = $client->Correo electrónico:('/api/v1/accounts', [
'json' => ['provider' => 'WHATSAPP'],
]);201 Creado, account_id devuelto
Mismo patrón para
Definición
¿Qué es la API de WhatsApp en PHP?
En API de WhatsApp en PHP es la práctica de llamar a una API REST de WhatsApp desde un entorno de ejecución de PHP, PHP-FPM, un script CLI o un trabajador de colas, utilizando un cliente HTTP como Guzzle o cURL. No existe una API de WhatsApp creada específicamente para PHP: Meta y los proveedores de API exponen JSON plano sobre HTTPS, y PHP es simplemente el lenguaje que realiza la solicitud, de la misma manera que lo haría con cualquier otra API web.
En la práctica, una integración de la API de WhatsApp con PHP es una llamada HTTP normal: un método, una URL, un cuerpo JSON o multipart, y un
X-API-KEY header. Nada aquí depende de una función exclusiva de PHP ni de un endpoint exclusivo de PHP. Lo que cambia entre Cloud API, un Proveedor de Soluciones Empresariales (BSP) y una cuenta vinculada no es el idioma, sino quién es el propietario del número de WhatsApp y si la verificación de Meta Business se encuentra entre usted y su primer mensaje. Para ver el desglose completo de ese ecosistema, consulte la Guía de integración de la API de WhatsApp.JSON simple a través de HTTPS
Cada llamada es un ciclo de solicitud/respuesta que Guzzle o cURL ya saben cómo realizar.
La autenticación es una cabecera estática
En
X-API-KEY valor, sin baile de OAuth, ni bucle de actualización de tokens que escribir.Independiente del motor de ejecución
Funciona igual en PHP-FPM, un comando de consola de Symfony o un trabajo de cola de Laravel.
Decisión
¿Qué API de WhatsApp debes usar desde PHP?
Hay tres formas de llamar a WhatsApp desde un backend de PHP: API en la nube de Meta directamente, a Proveedor de soluciones empresariales reventa de acceso al mismo, o un cuenta vinculada a través de una API unificada como Unipile. Las tres devuelven JSON que tu código PHP procesa de la misma manera; lo que cambia es el tiempo de configuración, si la verificación de Meta Business se interpone antes de tu primer mensaje, y si obtienes un historial de conversación existente یا empiezas desde cero.
Criterios
API en la nube (directa)
BSP
Cuenta vinculada
Verificación comercial de Meta
Requerido
Requerido
No es necesario
Tiempo hasta el primer mensaje
Días a algunas semanas
Incorporación más rápida, mismos requisitos subyacentes
Actas
Número de teléfono
Nuevo número registrado en una WABA
Nuevo número proporcionado por el BSP
El propio número de WhatsApp del usuario
Acceso a las conversaciones existentes
No, empieza vacío
No, empieza vacío
Sí, historial completo
Mejor ajuste
Un número de empresa que transmite a muchos clientes
Lo mismo, con una capa de incorporación gestionada
Un producto en el que cada usuario vincula la cuenta de WhatsApp que ya tiene
Si tu aplicación PHP es un producto SaaS donde cada cliente conecta su propio WhatsApp, una cuenta vinculada elimina por completo el paso de verificación de Meta. Si estás haciendo difusiones desde un solo número de empresa, la API de Cloud o un BSP son el punto de partida correcto en su lugar.
Construir con una cuenta vinculada Configurar
¿Qué necesitas antes de escribir cualquier código PHP?
Necesitas PHP 8.1 o más reciente, Compositor, la Guzzle Cliente HTTP y una clave de API de Unipile. Actualmente no existe un SDK de PHP mantenido activamente para Unipile, por lo que todos los ejemplos de esta guía se comunican con
/api/v1 API REST directamente.PHP 8.1+
Las propiedades tipadas y los argumentos con nombre permiten que el código del cliente sea conciso.
Compositor
Gestor de dependencias estándar, necesario para incluir Guzzle.
Guzzle 7
Cliente HTTP para peticiones JSON y multipart, con un sistema alternativo (fallback) de cURL.
Clave de API de Unipile
Enviado como
X-API-KEY en cada solicitud, generada desde tu panel.composer require guzzlehttp/guzzle:^7.0¿Hay un SDK de PHP? No uno que debas instalar.
Un paquete comunitario,
unipile/unipile-php-sdk, existe en Packagist, pero figura como abandonado: sin commits desde el 17 de octubre de 2023, 5 estrellas en GitHub, y apunta a una URL base de API antigua. No ejecutar composer require unipile/unipile-php-sdk. El SDK de Node.js (unipile-node-sdk) se mantiene oficialmente y está actualizado, pero para PHP, Guzzle directamente contra la REST API es el camino confiable, y es lo que usa cada ejemplo a continuación. Ver La documentación de Guzzle para las opciones de cliente utilizadas aquí.Implementación
¿Cómo se conecta una cuenta de WhatsApp desde PHP?
Conectar una cuenta de WhatsApp desde PHP toma uno
POST solicitud con el proveedor establecido en WHATSAPP, luego una acción del usuario: el usuario final escanea el código QR devuelto desde WhatsApp > Dispositivos vinculados, o escribe un código de emparejamiento en su teléfono si solicitó uno en su lugar. Sin aplicación de desarrollador de Meta, sin WABA, sin paso de verificación.1
Solicitar un punto de control, código QR por defecto
Llamar al endpoint de cuentas solo con
provider set. Unipile devuelve un account_id más una carga útil de punto de control creada para el flujo QR.$response = $client->Correo electrónico:('/api/v1/accounts', [
'json' => ['provider' => 'WHATSAPP'],
]);
$payload = json_decode($response->obtenerCuerpo()->obtenerContenido(), true);
$accountId = $payload['id_cuenta'];
// $payload también transporta el punto de control que se va a renderizar en el
el código QR para escanear desde WhatsApp > Vinculados
// dispositivos. var_dump($payload) vuelve a comprobarlo con tu propia clave
// para ver la forma exacta antes de conectar el frontend.2
O solicita un código de emparejamiento en su lugar
Pasar
vinculación_de_número_de_teléfono en dígitos E.164, sin signo más, cuando escanear un código QR no es práctico, por ejemplo, un agente de soporte incorporando a un cliente por teléfono.$response = $client->Correo electrónico:('/api/v1/accounts', [
'json' => [
'provider' => 'WHATSAPP',
'pairing_phone_number' => '33612345678',
],
]);
$payload = json_decode($response->obtenerCuerpo()->obtenerContenido(), true);
$accountId = $payload['id_cuenta'];
// El punto de control aquí es el código que tu interfaz de usuario muestra al usuario
// para escribir en WhatsApp > Dispositivos vinculados > Vincular con
// número de teléfono en su lugar.3
Consultar periódicamente hasta que la cuenta esté completamente vinculada
La cuenta permanece en
CONECTAR estado hasta que se escanee el código QR o se introduzca el código de emparejamiento. Comprobar sources[0].status a cuenta.$response = $client->consiga(""/api/v1/accounts/{$accountId}"");
$account = json_decode($response->obtenerCuerpo()->obtenerContenido(), true);
$status = $account['fuentes'][0]['estado'] ?? null;
si ($status === OK) {
// Se puede enviar, recibir y ver la lista de chats de forma segura en $accountId
}Las consultas en bucle funcionan pero desperdician solicitudes. Si tu aplicación PHP ya tiene un punto de conexión de webhook, registra uno con
source: "account_status" instead and react the moment the status flips to OK. The webhook setup itself is covered further down for incoming messages, the same endpoint accepts account status events.Core flow
How do you send a WhatsApp message with PHP and Guzzle?
Sending a message from PHP is one
POST to the chat's messages endpoint with a text field in a multipart body. The call returns a id_de_mensaje the instant WhatsApp accepts it.$response = $client->Correo electrónico:(""/api/v1/chats/{$chatId}/messages"", [
'multipart' => [
['nombre' => texto, 'contenido' => '¡Hola! Tu pedido ha sido enviado.'],
],
]);
$result = json_decode($response->obtenerCuerpo()->obtenerContenido(), true);
$messageId = $result['id_mensaje'];Lo mismo
multipart array also carries archivos adjuntos, a mensaje_voz file, or a quote_id to reply to a specific message, Guzzle attaches them as additional multipart fields next to text. For the full breakdown of every message type the WhatsApp API supports, including templates and the 24-hour service window, see our dedicated guide.Core flow
How do you start a new conversation from PHP?
Starting a conversation that does not exist yet means calling the chats endpoint with an
account_id y un attendees_ids array instead of an existing chat_id. Unipile creates the chat and, if you include a text field, sends the first message in the same call.$response = $client->Correo electrónico:('/api/v1/chats', [
'multipart' => [
['nombre' => 'id_cuenta', 'contenido' => [$accountId],
['nombre' => 'attendees_ids[]', 'contenido' => '33612345678@s.whatsapp.net'],
['nombre' => texto, 'contenido' => 'Hola, soy el servicio de soporte de Acme.'],
],
]);
$result = json_decode($response->obtenerCuerpo()->obtenerContenido(), true);
$chatId = $result['chat_id'];The recipient is identified by
{phone_number}@s.whatsapp.net, international digits, no leading plus sign, no spaces. Store the returned chat_id next to that user record: every message after the first one goes through the messages endpoint from the previous section, not through this one.Core flow
How do you retrieve chats and messages in PHP?
Most WhatsApp API tutorials stop at sending. A PHP backend usually also needs to list a user's existing conversations, page through their history, and see who is in a chat. Three endpoints cover it:
GET /api/v1/chats, GET /api/v1/chats/{chat_id}/messagesy GET /api/v1/chats/{chat_id}/attendees, all paginated with a cursor.List every WhatsApp chat on an account
$response = $client->consiga('/api/v1/chats', [
'query' => [
''account_id' => $accountId,
'tipo_de_cuenta' => 'WHATSAPP',
'limit' => 50,
],
]);
$data = json_decode($response->obtenerCuerpo()->obtenerContenido(), true);
$chats = $data['artículos'];
$nextCursor = $data['cursor'];Page through one chat's history
$cursor = null;
$all = [];
hacer {
$res = $client->consiga(""/api/v1/chats/{$chatId}/messages"", [
'query' => array_filter(['cursor' => $cursor, 'límite' => 100]),
]);
$page = json_decode($res->obtenerCuerpo()->obtenerContenido(), true);
$all = array_merge($all, $page['artículos']);
$cursor = $page['cursor'];
} mientras ($cursor !== null);Every list endpoint follows the same cursor contract: pass the previous response's
cursor back in, stop when it comes back null. To see who is actually in a conversation, call GET /api/v1/chats/{chat_id}/attendees: it returns each participant's id, proveedor_id and display name, which is what you need before you can call agregarParticipante o eliminarParticipante on a group later in this guide.Producción
How do you receive WhatsApp messages with a PHP webhook?
A WhatsApp webhook in PHP is a public HTTPS endpoint that Unipile calls with a JSON payload on events like
mensaje_recibido. Your script reads the raw body, confirms the call is genuine, replies fast, and hands the actual work to a queue. Polling GET /api/v1/chats for new messages works for a demo, not for a product with more than a handful of users.1. Register the webhook, with a shared secret in a custom header
$client->Correo electrónico:('/api/v1/webhooks', [
'json' => [
'origen' => 'mensajería',
'request_url' => 'https://example.com/webhooks/whatsapp',
'eventos' => ['mensaje_recibido'],
'headers' => [
['clave' => 'X-Webhook-Secret', 'valor' => [$webhookSecret],
],
],
]);2. Verify, respond, then process
$rawBody = file_get_contents('php://entrada');
$provided = $_SERVER['HTTP_X_WEBHOOK_SECRET'] ?? '';
si (!hash_equals($expectedSecret, $provided)) {
código_de_respuesta_http(401);
salir;
}
código_de_respuesta_http(200);
si (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
$payload = json_decode($rawBody, true);
si (($payload['evento'] ?? null) === 'mensaje_recibido') {
// insertar la carga útil $ en una tabla de cola o en una lista de Redis,
// nunca proceses un webhook en línea
poner_en_cola_mensaje_de_whatsapp($payload);
}Unipile does not sign the webhook body with an HMAC secret you recompute, the create-webhook endpoint exposes a
cabeceras array instead. Attach your own secret as a custom header when you register the webhook, then check it with hash_equals(), a timing-safe string comparison, before trusting the payload. Every field in $payload, account_id, tipo_de_cuenta, evento, chat_id, id_de_mensaje, message y sender.attendee_provider_id, matches the JSON Unipile documents for the messaging webhook.Producción
How do you integrate the WhatsApp API into a Laravel application?
Laravel replaces three things from the plain-PHP examples above: the
Http facade instead of a hand-rolled Guzzle client, a queued job instead of inline processing, and a route plus controller instead of parsing php://input by hand. The WhatsApp calls underneath are identical to every example already in this guide.1. ConfigurationOne entry in services.php, values from .env
return [
// ...servicios existentes
'unipile' => [
'base_uri' => env('UNIPILE_BASE_URI', 'https://api1.unipile.com:13111'),
'api_key' => env('UNIPILE_API_KEY'),
'secreto_de_webhook' => env('UNIPILE_WEBHOOK_SECRET'),
],
];2. A thin service class over the Http facade
espacio de nombres App\Services;
usar Illuminate\Support\Facades\Http;
clase ServicioDeWhatsapp
{
protegido función client()
{
return Http::baseUrl(configuración('services.unipile.base_uri'))
->conCabeceras(['X-API-KEY' => configuración('servicios.unipile.api_key')]);
}
público función sendMessage(cadena $chatId, cadena $text): matriz
{
return $esto->client()
->comoMultipartes()
->Correo electrónico:(""/api/v1/chats/{$chatId}/messages"", [
['nombre' => texto, 'contenido' => $text],
])
->json();
}
}3. Route and webhook controller
Route::Correo electrónico:('/webhooks/whatsapp', [WhatsappWebhookController::clase, 'mango']);public function handle(Request $request)
{
$provided = $request->header('X-Webhook-Secret', '');
if (!hash_equals(config('services.unipile.webhook_secret'), $provided)) {
abort(401);
}
if ($request->input('event') === 'message_received') {
ProcessWhatsappMessage::dispatch($request->all());
}
return response()->json(['status' => 'received']);
}4. A queued job, so the webhook route stays fast
clase ProcesarMensajeDeWhatsapp implementa ShouldQueue
{
usar Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
público función __construct(público matriz $payload) {}
público función mango(): vacío
{
// buscar el inquilino mediante $this->payload['account_id'],
// almacenar el mensaje, notificar al usuario correcto
}
}The route never touches Guzzle directly, it goes through
Build it in Laravel ServicioDeWhatsapp, which makes the WhatsApp calls mockable in tests the same way any other Laravel HTTP integration is. See Laravel's HTTP client documentation for faking these requests in your test suite.Producción
How do you manage WhatsApp groups from PHP?
Managing a WhatsApp group from PHP is a single
PATCH call to the chat endpoint with an acción field: agregarParticipante, eliminarParticipanteo obtenerEnlaceDeInvitacion. Meta's own Cloud API groups feature caps a group at 8 participantes and exposes no endpoint to add someone directly, only an invite link to share.Add a participant
$client->parche(""/api/v1/chats/{$groupChatId}"", [
'json' => [
'acción' => 'agregarParticipante',
'value' => '33612345678@s.whatsapp.net',
],
]);Obtener el enlace de invitación
$res = $client->parche(""/api/v1/chats/{$groupChatId}"", [
'json' => ['action' => 'obtenerEnlaceDeInvitación'],
]);
$link = json_decode($res->obtenerCuerpo()->obtenerContenido(), true)['enlace_de_invitación'];eliminarParticipante takes the same shape as agregarParticipante, with the participant's WhatsApp id in valor. This is the one action Meta's own Groups API does not expose at all, adding someone happens only through the invite link on their side. For every limit, the 10,000-groups-per-number ceiling and what still is not supported, see the API de grupos de WhatsApp reference.Escala
How do you handle many user accounts from one PHP application?
A PHP SaaS product does not have one WhatsApp account, it has one per customer. There is a single Unipile
X-API-KEY for your whole application, but a distinct account_id for every end user who links their WhatsApp. The pattern is to store that account_id next to your own tenant row, and pass it explicitly on every call that could otherwise cross accounts.// Dentro del controlador del webhook: encuentra a qué inquilino pertenece
// el mensaje pertenece a antes de hacer nada con él.
$stmt = $pdo->preparar('SELECT id FROM tenants WHERE unipile_account_id = ?');
$stmt->ejecutar([$payload['id_cuenta']]);
$tenantId = $stmt->recuperarColumna();
si ($tenantId === falso) {
// cuenta_id desconocida, rechazar en lugar de adivinar el inquilino
código_de_respuesta_http(404);
salir;
}
// Salida: pasar account_id para que un chat_id obsoleto o reutilizado pueda
// nunca envíes a través del número de WhatsApp del inquilino equivocado.
$client->Correo electrónico:(""/api/v1/chats/{$chatId}/messages"", [
'multipart' => [
['nombre' => texto, 'contenido' => $text],
['nombre' => 'id_cuenta', 'contenido' => [$expectedAccountId],
],
]);The optional
account_id field on the messages endpoint exists specifically to prevent sending in a chat outside that account, treat it as mandatory in a multi-tenant codebase even though the API does not require it. Encrypt the stored account_id values at rest like any other customer credential: they are not secret on their own, but they are the join key between your users table and someone's real WhatsApp conversations.Trampas
What are the most common PHP-specific mistakes?
Most WhatsApp API bugs in a PHP codebase come from PHP itself, not from the API. These five show up repeatedly in production integrations.
Truncating a message with substr() instead of mb_substr()
PHP's
substr() counts bytes, not characters. An emoji is a multi-byte UTF-8 sequence, cut it in half and you send a broken character or invalid JSON. Always use the multi-byte functions on user-generated WhatsApp text.$safe = mb_substr($text, 0, 4096, 'UTF-8');cURL error 60 on Windows and XAMPP
"SSL certificate problem: unable to get local issuer certificate." Windows PHP builds and XAMPP often ship without a current CA bundle wired into php.ini, so Guzzle's HTTPS calls fail even though the code is correct.
curl.cainfo = "C:\php\extras\cacert.pem"
openssl.cafile = "C:\php\extras\cacert.pem"max_execution_time killing the webhook before it answers
A default of 30 seconds is plenty until the handler makes a slow outbound call before responding. If Unipile does not get a fast response, it retries, and a slow handler starts processing the same message twice. Respond first, work second, as shown in the webhook section above.
No backoff on 429 or 5xx responses
A tight
mientras loop that retries immediately on failure turns one rate limit into a self-inflicted outage. Wait, and increase the wait on every consecutive failure.para ($attempt = 1; $attempt <= 5; $attempt++) {
intentar {
return $client->Correo electrónico:($uri, $options);
} captura (ConnectException | ServerException $e) {
suspender((2 ** $attempt) * 100_000); // 0,2s, 0,4s, 0,8s...
}
}Logging the raw response and calling it debugging
If you build a JSON body by hand instead of letting Guzzle's
json option handle it, add JSON_UNESCAPED_UNICODE a json_encode(). Without it, emoji and accented names show up as escaped \u sequences in your logs, which is technically valid JSON but unreadable when you are trying to debug a real conversation.Coste
How much does it cost to send WhatsApp messages from PHP?
Nothing in the PHP code changes the price. Meta bills por mensaje desde el 1 de julio de 2025, not per conversation. The one thing your code controls is timing: a reply sent inside the 24-hour service window after a user's message is still free, the identical call sent an hour later is billable.
Utility and authentication templates stay free inside an open service window, billed outside it.
Plantillas de marketing are billed regardless of the window.
Per-message rates vary by recipient country and message category, not by the client library you send them with.
Timestamp every inbound message your PHP webhook receives and keep a per-conversation "window open until" value, that is the only input your billing logic actually needs. For current per-message WhatsApp pricing by country and category, see the full pricing breakdown.
WhatsApp API with PHP - FAQ
Straight answers on connecting, sending, Laravel, and the abandoned PHP SDK.
Create a Guzzle client pointed at the Unipile REST API, call
POST /api/v1/accounts with provider WHATSAPP to get a QR code or pairing code, have the user complete it on their phone, then use the same client for GET, POST y PATCH calls to send messages, list chats and manage groups. There is no PHP-specific endpoint, every call is a normal HTTP request with an X-API-KEY header. The connect a WhatsApp account section above walks through the exact code.Yes. A single
POST a /api/v1/chats/{chat_id}/mensajes with a text field in a multipart body sends the message and returns a id_de_mensaje, vea el send section above for the exact Guzzle call.Solo si utilizas directamente la API en la nube de Meta o un Proveedor de Soluciones Empresariales construido sobre ella. Conectar una cuenta de WhatsApp a través de Unipile vincula el número de WhatsApp existente del usuario mediante un código QR o un código de emparejamiento y no requiere ninguna verificación de Meta Business.
No actively maintained one. A community package,
unipile/unipile-php-sdk, is on Packagist but marked abandonado, with no commit since October 17, 2023 and 5 stars on GitHub. Every example in this guide calls the REST API directly through Guzzle instead, which is also what the abandoned SDK did internally.You cannot receive webhooks without a publicly reachable HTTPS URL, that is how HTTP callbacks work. During local development, tunnel a local port with a service like ngrok and point the webhook's
URL_solicitud at the tunnel's HTTPS address. Polling the messages endpoint is the fallback when a public endpoint genuinely is not possible, at the cost of latency and extra requests.Yes, nothing about it is framework-specific. The Laravel section above wraps the same Guzzle-equivalent calls in the
Http facade, a queued job, and a webhook route and controller, which is the idiomatic way to structure it in a Laravel codebase.Yes, that is the normal SaaS shape. One Unipile API key covers the whole application, while every end user gets their own
account_id after connecting their WhatsApp. Store that id next to your tenant record and pass it on outbound calls to prevent cross-tenant sends, see the multi-tenant section arriba.PATCH /api/v1/chats/{chat_id} con {"action": "addParticipant", "value": "<phone>@s.whatsapp.net"}. Meta's own Cloud API groups feature has no equivalent endpoint, only an invite link, which is one of the clearer differences between building directly on Meta and going through Unipile. Details in the groups section arriba.Almost always because a string was truncated with
substr() en vez de mb_substr(). PHP's plain string functions count bytes, and an emoji is a multi-byte UTF-8 sequence, cut in the middle it turns into an invalid character or breaks the JSON payload entirely.Guzzle is enough, it is an HTTP client built on top of PHP's cURL extension, not a replacement for it. Every request in this guide uses Guzzle; a raw
curl_init() call works identically if a project cannot add the dependency, the JSON and multipart bodies are the same either way.The same as sending it any other way. Meta bills per message since July 1, 2025, based on category and recipient country, not on the client library used to call the API. See the cost section above and the full pricing guide for current rates.
Yes, it is not going away, it just determines what is free rather than what is technically allowed. A reply inside the 24-hour window after a user's last message ships without being billed as a template message, PHP code that ignores the window simply pays for messages it did not need to.
Still have questions about building this in PHP? Our team is here to help.
Guía de la API de WhatsApp en PHP
Ready to build the WhatsApp API into your PHP application?
Every endpoint in this guide, connect, send, receive, groups, runs on the same Unipile account you can create right now. No Meta Business verification, no abandoned SDK, just
Guzzle and a REST API your PHP application already knows how to call.Automating instead of integrating into an existing app? Read the same integration in Python.