Skip to content

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

  1. Ingresa al panel de TAYPI.
  2. Ve a Configuracion > Webhooks.
  3. Ingresa la URL de tu endpoint (debe ser HTTPS en produccion).
  4. 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

EventoDescripcionCuando se envia
payment.completedEl pago fue completado exitosamenteCuando el cliente paga el QR
payment.expiredEl pago ha expirado sin ser completado15 minutos despues de la creacion

Payload

TAYPI envia el siguiente payload JSON en el body del POST:

json
{
    "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

CampoTipoDescripcion
eventstringTipo de evento (payment.completed, payment.expired)
payment_idstringUUID del pago
merchant_idstringUUID de tu comercio
amountstringMonto del pago con 2 decimales
currencystringMoneda (PEN)
statusstringEstado del pago (completed, expired)
referencestringTu referencia original del pago
payer_walletstring|nullBilletera del pagador (yape, plin, etc.)
paid_atstring|nullFecha y hora de pago (ISO 8601, timezone Lima)
metadataobjectMetadata que enviaste al crear el pago
timestampstringMomento en que se genero el webhook (ISO 8601)

Headers del webhook

TAYPI incluye estos headers en cada entrega de webhook:

HeaderDescripcionEjemplo
Taypi-SignatureFirma HMAC-SHA256 del bodysha256=a1b2c3d4e5f6...
Taypi-TimestampTimestamp UNIX del envio1710504601
Taypi-Webhook-IdID unico de esta entregawhd_a1b2c3d4...
Content-TypeTipo de contenidoapplication/json
User-AgentAgente de usuarioTAYPI-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?

  1. TAYPI calcula HMAC-SHA256(webhook_secret, raw_body).
  2. El resultado (hex digest) se envia en el header Taypi-Signature con el prefijo sha256=.
  3. Tu servidor debe recalcular la firma con el mismo webhook_secret y comparar.

Paso a paso

  1. Obtén el header Taypi-Signature (ejemplo: sha256=a1b2c3d4...).
  2. Remueve el prefijo sha256= para obtener la firma recibida.
  3. Calcula HMAC-SHA256(webhook_secret, raw_body) usando el body sin parsear.
  4. Compara tu firma con la recibida usando una funcion de comparacion timing-safe.
  5. Si coinciden, la firma es valida. Si no, rechaza el webhook con 401.

Verificacion en PHP

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)

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)

csharp
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:

php
$isValid = $taypi->verifyWebhook($payload, $signatureHeader);
js
const isValid = taypi.verifyWebhook(rawBody, signatureHeader);
csharp
bool isValid = client.VerifyWebhook(rawBody, signatureHeader);

Politica de reintentos

Si tu endpoint no responde con un codigo HTTP 2xx, TAYPI reintentara la entrega:

IntentoEsperaTiempo total acumulado
1 (original)0s
2 (reintento 1)10 segundos10s
3 (reintento 2)60 segundos70s
4 (reintento 3)300 segundos370s (~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.

php
// 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.

php
$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.

php
$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.

php
WebhookLog::create([
    'event' => $event['event'],
    'payment_id' => $event['payment_id'],
    'payload' => $payload,
    'signature_valid' => true,
    'received_at' => now(),
]);

Plataforma de pagos QR interoperables para Perú