Webhooks
Los webhooks de TAYPI te notifican en tiempo real cuando ocurre un evento importante, como la confirmacion de un pago. En lugar de consultar la API repetidamente (polling), TAYPI envia un POST HTTP a la URL que configures.
¿Como funcionan?
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Cliente │ │ TAYPI │ │ Tu servidor │
│ (Yape/Plin) │ │ │ │ (Webhook) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ Paga el QR │ │
│────────────────────>│ │
│ │ │
│ │ POST webhook_url │
│ │ + firma HMAC │
│ │────────────────────>│
│ │ │
│ │ 200 OK │
│ │<────────────────────│Configuracion
- Ingresa al panel de TAYPI.
- Ve a Configuracion > Webhooks.
- Ingresa la URL de tu endpoint (debe ser HTTPS en produccion).
- Guarda tu Webhook Secret — lo necesitaras para verificar las firmas.
TIP
En sandbox puedes usar herramientas como webhook.site o ngrok para recibir webhooks en tu maquina local.
Eventos
| Evento | Descripcion | Cuando se envia |
|---|---|---|
payment.completed | El pago fue completado exitosamente | Cuando el cliente paga el QR |
payment.expired | El pago ha expirado sin ser completado | 15 minutos despues de la creacion |
Payload
TAYPI envia el siguiente payload JSON en el body del POST:
{
"event": "payment.completed",
"payment_id": "a14dfb8e-d5c2-4a69-bae4-4688fef5eac2",
"merchant_id": "b25eac9f-e6d3-5b7a-cbf5-5799gfg6fbd3",
"amount": "50.00",
"currency": "PEN",
"status": "completed",
"reference": "ORD-12345",
"payer_wallet": "yape",
"paid_at": "2026-03-15T10:30:00-05:00",
"metadata": {
"customer_email": "cliente@example.com"
},
"timestamp": "2026-03-15T10:30:01-05:00"
}Campos del payload
| Campo | Tipo | Descripcion |
|---|---|---|
event | string | Tipo de evento (payment.completed, payment.expired) |
payment_id | string | UUID del pago |
merchant_id | string | UUID de tu comercio |
amount | string | Monto del pago con 2 decimales |
currency | string | Moneda (PEN) |
status | string | Estado del pago (completed, expired) |
reference | string | Tu referencia original del pago |
payer_wallet | string|null | Billetera del pagador (yape, plin, etc.) |
paid_at | string|null | Fecha y hora de pago (ISO 8601, timezone Lima) |
metadata | object | Metadata que enviaste al crear el pago |
timestamp | string | Momento en que se genero el webhook (ISO 8601) |
Headers del webhook
TAYPI incluye estos headers en cada entrega de webhook:
| Header | Descripcion | Ejemplo |
|---|---|---|
Taypi-Signature | Firma HMAC-SHA256 del body | sha256=a1b2c3d4e5f6... |
Taypi-Timestamp | Timestamp UNIX del envio | 1710504601 |
Taypi-Webhook-Id | ID unico de esta entrega | whd_a1b2c3d4... |
Content-Type | Tipo de contenido | application/json |
User-Agent | Agente de usuario | TAYPI-Webhook/1.0 |
Verificacion de firma
OBLIGATORIO
Siempre verifica la firma antes de procesar un webhook. Sin verificacion, un atacante podria enviar webhooks falsos a tu endpoint y simular pagos completados.
¿Como funciona la firma?
- TAYPI calcula
HMAC-SHA256(webhook_secret, raw_body). - El resultado (hex digest) se envia en el header
Taypi-Signaturecon el prefijosha256=. - Tu servidor debe recalcular la firma con el mismo
webhook_secrety comparar.
Paso a paso
- Obtén el header
Taypi-Signature(ejemplo:sha256=a1b2c3d4...). - Remueve el prefijo
sha256=para obtener la firma recibida. - Calcula
HMAC-SHA256(webhook_secret, raw_body)usando el body sin parsear. - Compara tu firma con la recibida usando una funcion de comparacion timing-safe.
- Si coinciden, la firma es valida. Si no, rechaza el webhook con
401.
Verificacion en PHP
$payload = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_TAYPI_SIGNATURE'] ?? '';
$webhookSecret = getenv('TAYPI_WEBHOOK_SECRET');
// 1. Extraer la firma del header
$receivedSignature = str_replace('sha256=', '', $signatureHeader);
// 2. Calcular la firma esperada
$expectedSignature = hash_hmac('sha256', $payload, $webhookSecret);
// 3. Comparar (timing-safe)
if (!hash_equals($expectedSignature, $receivedSignature)) {
http_response_code(401);
exit('Firma invalida');
}
// 4. Procesar el evento
$event = json_decode($payload, true);
if ($event['event'] === 'payment.completed') {
// Marcar orden como pagada
$paymentId = $event['payment_id'];
$amount = $event['amount'];
// ... tu logica de negocio
}
http_response_code(200);
echo 'OK';Verificacion en JavaScript (Node.js)
import crypto from 'crypto';
app.post('/webhooks/taypi',
express.raw({ type: 'application/json' }),
(req, res) => {
const payload = req.body.toString();
const signatureHeader = req.headers['taypi-signature'] || '';
const webhookSecret = process.env.TAYPI_WEBHOOK_SECRET;
// 1. Extraer la firma del header
const receivedSignature = signatureHeader.replace('sha256=', '');
// 2. Calcular la firma esperada
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
// 3. Comparar (timing-safe)
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature),
);
if (!isValid) {
return res.status(401).send('Firma invalida');
}
// 4. Procesar el evento
const event = JSON.parse(payload);
if (event.event === 'payment.completed') {
console.log('Pago completado:', event.payment_id);
}
res.status(200).send('OK');
}
);IMPORTANTE
Usa express.raw() en la ruta del webhook, no express.json(). Necesitas el body original sin parsear para verificar la firma.
Verificacion en C# (.NET)
using System.Security.Cryptography;
using System.Text;
[HttpPost("webhooks/taypi")]
public async Task<IActionResult> Webhook()
{
using var reader = new StreamReader(Request.Body);
var rawBody = await reader.ReadToEndAsync();
var signatureHeader = Request.Headers["Taypi-Signature"].FirstOrDefault() ?? "";
var webhookSecret = Environment.GetEnvironmentVariable("TAYPI_WEBHOOK_SECRET")!;
// 1. Extraer la firma del header
var receivedSignature = signatureHeader.Replace("sha256=", "");
// 2. Calcular la firma esperada
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(webhookSecret));
var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
var expectedSignature = Convert.ToHexString(hashBytes).ToLowerInvariant();
// 3. Comparar (timing-safe)
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expectedSignature),
Encoding.UTF8.GetBytes(receivedSignature)))
{
return Unauthorized("Firma invalida");
}
// 4. Procesar el evento
var webhookEvent = System.Text.Json.JsonSerializer.Deserialize<WebhookPayload>(rawBody);
if (webhookEvent?.Event == "payment.completed")
{
// Marcar orden como pagada
}
return Ok("OK");
}Verificacion con SDK (recomendado)
Si usas un SDK de TAYPI, la verificacion es aun mas simple:
$isValid = $taypi->verifyWebhook($payload, $signatureHeader);const isValid = taypi.verifyWebhook(rawBody, signatureHeader);bool isValid = client.VerifyWebhook(rawBody, signatureHeader);Politica de reintentos
Si tu endpoint no responde con un codigo HTTP 2xx, TAYPI reintentara la entrega:
| Intento | Espera | Tiempo total acumulado |
|---|---|---|
| 1 (original) | — | 0s |
| 2 (reintento 1) | 10 segundos | 10s |
| 3 (reintento 2) | 60 segundos | 70s |
| 4 (reintento 3) | 300 segundos | 370s (~6 min) |
Despues del cuarto intento fallido, TAYPI deja de intentar y marca la entrega como failed. Puedes ver el historial de entregas en el panel, en Configuracion > Webhooks > Entregas.
TIP
Si necesitas reprocesar un webhook fallido, puedes reenviar la entrega manualmente desde el panel.
Mejores practicas
1. Responde rapido con 200
Tu endpoint debe responder con 200 OK lo antes posible (idealmente en menos de 5 segundos). Si necesitas hacer procesamiento largo (enviar email, actualizar inventario), hazlo en segundo plano.
// Responde rapido
http_response_code(200);
echo 'OK';
// Luego procesa en segundo plano (ejemplo con Laravel)
ProcessPaymentJob::dispatch($event['payment_id']);2. Siempre verifica la firma
Nunca proceses un webhook sin verificar su firma HMAC-SHA256. Un atacante podria enviar un POST falso a tu endpoint simulando que un pago fue completado.
3. Maneja la idempotencia
Es posible que recibas el mismo webhook mas de una vez (por reintentos). Usa el payment_id para verificar si ya procesaste ese pago.
$order = Order::where('payment_id', $event['payment_id'])->first();
if ($order && $order->status === 'paid') {
// Ya fue procesado, ignorar
return response('OK', 200);
}
// Procesar el pago por primera vez
$order->update(['status' => 'paid']);4. Verifica el monto
Despues de verificar la firma, compara el monto del webhook con el monto original de la orden en tu base de datos.
$order = Order::where('payment_id', $event['payment_id'])->first();
if ($order->total != $event['amount']) {
// Monto no coincide — posible manipulacion
Log::critical('Monto webhook no coincide', [
'order_total' => $order->total,
'webhook_amount' => $event['amount'],
]);
return response('OK', 200); // Responde 200 pero no proceses
}5. Usa HTTPS
En produccion, tu webhook URL debe usar HTTPS. TAYPI rechazara URLs HTTP en el ambiente de produccion.
6. Registra los eventos
Guarda un log de todos los webhooks recibidos para debugging y auditoria.
WebhookLog::create([
'event' => $event['event'],
'payment_id' => $event['payment_id'],
'payload' => $payload,
'signature_valid' => true,
'received_at' => now(),
]);