Webhooks
Recibe notificaciones HTTP en tiempo real cuando el estado de una factura cambia. Configura un endpoint en tu servidor y nosotros lo llamamos automáticamente.
¿Cómo funciona?
Cuando configuras un Webhook URL en un servicio, el sistema envía una petición HTTP POST a esa URL cada vez que una factura de ese servicio cambia de estado. No necesitas consultar la API periódicamente — los eventos llegan automáticamente.
webhook_secret independiente. Puedes usar el mismo endpoint para múltiples servicios si verificas el campo serviceId del payload.El flujo completo:
- Configuras tu Webhook URL en Servicios → Editar servicio.
- Una factura cambia de estado (pagada, vencida, cancelada, etc.).
- Enviamos un
POSTa tu URL con el payload JSON y las cabeceras de seguridad. - Tu servidor verifica la firma HMAC y procesa el evento.
- Responde con cualquier código 2xx para confirmar la recepción.
Eventos disponibles
El campo event del payload (y el header X-Event) indica qué ocurrió:
Estructura del payload
El cuerpo de la petición es JSON con codificación UTF-8. Los campos con valor null se omiten.
{
"event": "invoice.paid",
"timestamp": "2026-07-15T18:32:00Z",
"invoice": {
"invoiceId": "BILLING-2026-000042",
"serviceName": "billing-core", // código del servicio
"serviceId": 3,
"amount": 150.00, // total a pagar
"subtotal": 130.00,
"taxAmount": 19.50,
"retentionAmount": 0.00,
"taxMode": "global", // "none" | "global" | "per_item"
"taxRate": 15,
"currency": "USD", // "USD" | "NIO"
"status": "PAID", // PENDING | PAID | OVERDUE | CANCELLED
"customerId": 12,
"customerName": "Empresa S.A.",
"issueDate": "2026-07-01T00:00:00Z",
"dueDate": "2026-07-31T23:59:59Z",
"notes": "Factura mensual julio",
"items": [
{
"description": "Plan Pro",
"quantity": 1,
"unitPrice": 130.00
}
],
"meta": { // atributos personalizados del cliente
"plan": "business",
"ref": "ERP-00821"
}
}
}
invoiceId como idempotency key. Es posible que recibas el mismo evento más de una vez si tu servidor tardó en responder. Almacena el invoiceId + event para evitar procesar duplicados.Headers de la petición
application/json; charset=utf-8invoice.paid, invoice.created, etc.sha256=<HMAC-SHA256 en base64> — firma del cuerpo completo.InvoiceSystem-Webhook/1.0Verificación de firma
Cada petición incluye el header X-Signature con una firma HMAC-SHA256 del cuerpo JSON usando el webhook secret de tu servicio (visible en Servicios → Editar → Webhook).
Ejemplo en PHP
$secret = 'tu_webhook_secret_aqui';
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
// El header tiene el formato "sha256=<base64>"
$expected = 'sha256=' . base64_encode(hash_hmac('sha256', $body, $secret, true));
if (!hash_equals($expected, $header)) {
http_response_code(401);
exit('Firma inválida');
}
$event = json_decode($body, true);
// Procesa el evento
match ($event['event']) {
'invoice.paid' => markAsPaid($event['invoice']),
'invoice.overdue' => sendOverdueAlert($event['invoice']),
default => null,
};
http_response_code(200);
echo 'ok';
Ejemplo en Node.js
import crypto from 'crypto';
import express from 'express';
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/invoices', (req, res) => {
const secret = process.env.WEBHOOK_SECRET;
const signature = req.headers['x-signature'] ?? '';
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('base64');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Firma inválida');
}
const { event, invoice } = JSON.parse(req.body);
if (event === 'invoice.paid') {
// Marca la orden como pagada en tu sistema
console.log('Factura pagada:', invoice.invoiceId);
}
res.sendStatus(200);
});
Ejemplo en Python
import hmac, hashlib, base64
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b'tu_webhook_secret_aqui'
@app.route('/webhooks/invoices', methods=['POST'])
def webhook():
body = request.get_data()
signature = request.headers.get('X-Signature', '')
digest = base64.b64encode(hmac.new(SECRET, body, hashlib.sha256).digest()).decode()
expected = f'sha256={digest}'
if not hmac.compare_digest(signature, expected):
abort(401, 'Firma inválida')
data = request.get_json(force=True)
event = data['event']
if event == 'invoice.paid':
invoice = data['invoice']
print(f"Pagada: {invoice['invoiceId']}")
return 'ok', 200
Reintentos y timeouts
Si tu endpoint no responde con un código 2xx dentro del tiempo límite, el sistema reintenta automáticamente con backoff exponencial.
200 ok inmediatamente para evitar timeouts en operaciones largas.Ejemplos de uso
Activar un servicio en tu plataforma al pagar
if ($event['event'] === 'invoice.paid') {
$invoice = $event['invoice'];
$userId = $invoice['meta']['userId'] ?? null; // campo meta del cliente
if ($userId) {
// Activa el plan en tu base de datos
User::find($userId)?->activate();
}
}
Enviar alerta a Slack cuando vence una factura
if ($event['event'] === 'invoice.overdue') {
$inv = $event['invoice'];
Http::post(env('SLACK_WEBHOOK'), [
'text' => "⚠️ Factura vencida: *{$inv['invoiceId']}* — {$inv['customerName']} — \${$inv['amount']} {$inv['currency']}"
]);
}