Webhooks
Receive automatic HTTP notifications on your server when important events occur in WAzion.
1. Introduction
Webhooks allow your server to receive automatic notifications when specific events occur in WAzion. Instead of periodically checking for updates, WAzion sends an HTTP POST request to your server when something relevant happens.
How does it work?
- 1 You configure a URL of your server in the WAzion Dashboard
- 2 You select which events you want to receive
- 3 When an event occurs, WAzion sends a POST to your URL
- 4 Your server processes the event and responds with HTTP 200
Use cases
- • Synchronize new contacts with your CRM
- • Automatically generate leads
- • Trigger automations in Zapier/Make
- • Notify internal systems
2. Available Events
phone.detected
MainIt triggers when a new phone number is detected in a conversation. Useful for capturing leads automatically.
Sample payload
{
"event_type": "phone.detected",
"phone": "+34612345678",
"detected_at": "2025-01-15T14:30:00Z",
"shop_id": 123,
"conversation_hash": "a1b2c3d4e5f6..."
}
Payload fields
| event_type | Type of event |
| phone | Detected number (E.164) |
| detected_at | ISO 8601 Date/Time |
| shop_id | Your store ID in WAzion |
| conversation_hash | Unique conversation hash |
test
Test event that you can manually trigger from the Dashboard to verify that your endpoint works correctly.
{
"event_type": "test",
"message": "Este es un webhook de prueba desde WAzion",
"timestamp": "2025-01-15T14:30:00+01:00",
"shop_id": 123,
"test": true
}
3. HTTP Headers
WAzion sends the following headers in each webhook request:
| Header | Description | Example |
|---|---|---|
| Content-Type | Content type | application/json |
| User-Agent | WAzion Identifier | WAzion-Webhooks/1.0 |
| X-Webhook-ID | Unique webhook ID (format wh_XXXXXXXX) | wh_00012345 |
| X-Webhook-Event | Type of event | phone.detected |
| X-Webhook-Attempt | Attempt number (1-6) | 1 |
| X-Webhook-Timestamp | Unix timestamp of sending | 1705329000 |
| X-Webhook-Signature | HMAC-SHA256 hex signature (if there is a secret) | a1b2c3d4e5f6... |
4. HMAC-SHA256 Signature
If you set up a webhook secret, WAzion will sign each request using HMAC-SHA256. This allows you to verify that the request really comes from WAzion.
How the signature is generated
// 1. Concatenar timestamp + "." + payload JSON
signature_payload = timestamp + "." + payload_json
// 2. Calcular HMAC-SHA256 y convertir a hexadecimal
signature = HMAC-SHA256(signature_payload, webhook_secret).toHex()
// 3. El header X-Webhook-Signature contiene la firma directamente:
"a1b2c3d4e5f6789..." // Sin prefijo, solo el hash hex
Verification in PHP
<?php
function verificarFirmaWebhook($payload, $signatureRecibida, $secret, $timestamp) {
// Verificar que el timestamp no sea muy antiguo (5 min)
if (abs(time() - intval($timestamp)) > 300) {
return false;
}
// Construir payload firmado
$signedPayload = $timestamp . '.' . $payload;
// Calcular firma esperada
$expectedSignature = hash_hmac('sha256', $signedPayload, $secret);
// Comparar de forma segura (timing-safe)
return hash_equals($expectedSignature, $signatureRecibida);
}
// Uso:
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$secret = 'tu-webhook-secret';
if (!verificarFirmaWebhook($payload, $signature, $secret, $timestamp)) {
http_response_code(401);
exit('Firma inválida');
}
Verification in Node.js
const crypto = require('crypto');
function verificarFirmaWebhook(payload, signatureRecibida, secret, timestamp) {
// Verificar que el timestamp no sea muy antiguo (5 min)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
return false;
}
// Construir payload firmado
const signedPayload = `${timestamp}.${payload}`;
// Calcular firma esperada
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Comparar de forma segura (timing-safe)
return crypto.timingSafeEqual(
Buffer.from(signatureRecibida),
Buffer.from(expectedSignature)
);
}
Important: You should also verify that the timestamp is not too old (e.g., a maximum of 5 minutes) to prevent replay attacks.
5. Technical Specifications
Timeouts
| Timeout by request | 10 seconds |
| Maximum in queue | 100 webhooks |
Retries
| Maximum number of attempts | 6 |
| Strategy | Exponential backoff |
Retry schedule
After attempt 6, the webhook is marked as a permanent failure.
Accepted answers
Any 2xx code is considered a success.
Codes that trigger retry
No-retry codes (permanent failure)
Automatic deduplication
WAzion prevents sending the same phone number twice. A SHA256 hash of each sent phone number is stored. If the phone number has been previously notified, it is not sent again.
Rate Limiting
Maximum 100 webhooks in queue per store. If the limit is exceeded, new webhooks are discarded. The processor sends a maximum of 50 webhooks per execution with a 1-second delay between each.
Connection Settings
Webhooks do NOT follow redirects. Your endpoint must respond directly without redirecting.
6. Complete Sample Code
<?php
// webhook-handler.php
header('Content-Type: application/json');
// Configuración
$webhookSecret = 'tu-webhook-secret'; // Deja vacío si no usas firma
// Obtener datos
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$eventType = $_SERVER['HTTP_X_WEBHOOK_EVENT'] ?? '';
// Verificar firma (si hay secret configurado)
if ($webhookSecret) {
$expectedSignature = hash_hmac(
'sha256',
$timestamp . '.' . $payload,
$webhookSecret
);
if (!hash_equals($expectedSignature, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Verificar timestamp (máximo 5 minutos de antigüedad)
if (abs(time() - intval($timestamp)) > 300) {
http_response_code(401);
echo json_encode(['error' => 'Timestamp too old']);
exit;
}
}
// Decodificar payload
$data = json_decode($payload, true);
// Procesar según tipo de evento
switch ($eventType) {
case 'phone.detected':
$phone = $data['phone'];
$shopId = $data['shop_id'];
// Guardar en tu base de datos, CRM, etc.
guardarNuevoLead($phone, $shopId);
echo json_encode(['status' => 'ok', 'message' => 'Lead saved']);
break;
case 'test':
// Evento de prueba
echo json_encode(['status' => 'ok', 'message' => 'Test received']);
break;
default:
echo json_encode(['status' => 'ok', 'message' => 'Event not handled']);
}
function guardarNuevoLead($phone, $shopId) {
// Tu lógica aquí: guardar en BD, enviar a CRM, etc.
error_log("Nuevo lead: $phone de shop $shopId");
}
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = 'tu-webhook-secret'; // Deja vacío si no usas firma
// Middleware para raw body
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const payload = req.body.toString();
const signature = req.headers['x-webhook-signature'] || '';
const timestamp = req.headers['x-webhook-timestamp'] || '';
const eventType = req.headers['x-webhook-event'] || '';
// Verificar firma (si hay secret)
if (WEBHOOK_SECRET) {
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${timestamp}.${payload}`)
.digest('hex');
const sigBuffer = Buffer.from(signature);
const expectedBuffer = Buffer.from(expectedSignature);
if (sigBuffer.length !== expectedBuffer.length ||
!crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Verificar timestamp
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return res.status(401).json({ error: 'Timestamp too old' });
}
}
const data = JSON.parse(payload);
// Procesar evento
switch (eventType) {
case 'phone.detected':
console.log(`Nuevo lead: ${data.phone} de shop ${data.shop_id}`);
// Tu lógica aquí
break;
case 'test':
console.log('Test webhook received');
break;
}
res.json({ status: 'ok' });
});
app.listen(3000, () => console.log('Webhook server running on port 3000'));
Change log
No recent changes in this documentation.