Saltar al contenido principal
← Volver a la documentación

Especificación del webhook

Formato de payload, verificación de firma, semántica de reintentos.

Especificación del webhook

Cuando una orden es acreditada, TriaPay firma un POST a la URL de webhook que configuraste. El archivo de callback del plugin DHRU incluido es el receptor estándar. Usa esta página si escribes el tuyo propio.

Endpoint

Configura la URL en la página Integration. Con el plugin DHRU incluido:

https://yourdhru.com/modules/gateways/callback/triapay.php

O para una instalación de DHRU en subruta:

https://yourdomain.com/dhru/modules/gateways/callback/triapay.php

Debe ser HTTPS y resolver a una IP pública. Las direcciones privadas, loopback y link-local son rechazadas.

Headers

Header Valor
Content-Type application/json
X-Payment-Signature hex(HMAC_SHA256(webhook_secret, signed_input))
X-Payment-Sig-Version 2
X-Payment-Timestamp Unix epoch en segundos al firmar
X-Payment-Idempotency Idempotency key de la orden
X-Payment-Mode live o sandbox

Signed input

La entrada del HMAC es:

{timestamp}\n{idempotency}\n{raw_body}

\n es un único line feed ASCII (0x0A). Las nuevas integraciones deben implementar v2.

Payload

{
  "event": "payment.credited",
  "invoiceId": 12345,
  "tenantPublicId": "tnt_a3b1c8d4e2f59071",
  "amount": "10.051231",
  "baseAmount": "10.000000",
  "feeDeducted": "0.000000",
  "fees": "0",
  "txHash": "0xabc...",
  "gateway": "triapay",
  "asset": "USDT",
  "chain": "bep20",
  "matchCode": 51231,
  "mode": "live",
  "ts": 1714579200
}
Field Notas
event Siempre payment.credited.
invoiceId Tu invoice id de DHRU (eco desde la creación de la orden).
tenantPublicId Identificador opaco de tenant (tnt_*). Estable por cuenta.
amount String decimal. Monto exacto recibido.
baseAmount String decimal. Monto original de la factura antes de cualquier absorción de over-fee.
feeDeducted String decimal. Diferencia entre el monto esperado y el recibido cuando el cliente paga un excedente de comisión de red. Usualmente "0".
fees String decimal. Actualmente "0".
txHash Referencia única de la transacción. Hash de tx on-chain, transactionId de Binance Pay, o id de depósito off-chain de Binance. Úsalo como dedup key al acreditar.
gateway Siempre triapay.
asset USDT o USDC.
chain trc20, bep20, o binance_pay. BEP20 y TRC20 también exponen transferencias internas (off-chain) de Binance bajo el mismo valor de chain.
matchCode Código de coincidencia numérico.
mode live o sandbox. Mismo valor que el header X-Payment-Mode.
ts Mismo valor que X-Payment-Timestamp.

Cambios incompatibles

  • (2026-05-10) El payload del webhook ahora usa camelCase para todos los campos. Los receivers personalizados que leen invoice_id / tx_hash / match_code (snake_case) DEBEN actualizarse a invoiceId / txHash / matchCode. El plugin DHRU incluido (v2.x o posterior) soporta el nuevo formato de fábrica.
  • tenantId (entero crudo) fue eliminado y reemplazado por tenantPublicId, un string opaco tnt_* estable por cuenta y seguro para registrar en logs.

Verificación (en orden)

  1. Rechaza si |now - X-Payment-Timestamp| > 300 segundos.
  2. Calcula hex(HMAC_SHA256(webhook_secret, "{ts}\n{idempotency}\n{raw_body}")) sobre los bytes crudos del body. No vuelvas a serializar el JSON.
  3. Compara en tiempo constante contra X-Payment-Signature (PHP hash_equals, Node crypto.timingSafeEqual, Python hmac.compare_digest).
  4. Rechaza si ya procesaste este valor de X-Payment-Idempotency.

Respuesta esperada

Status Significado
2xx Éxito. Orden finalizada.
4xx / 5xx / network timeout Tratado como falla. Se reintenta.

Solo importa el status HTTP. El contenido del body se registra para depuración.

Política de reintentos

Las entregas fallidas se reintentan con backoff. Si los reintentos no logran éxito, la orden puede reconciliarse manualmente desde Admin → Orders → Recredit.

Si no hay webhook URL configurada, la orden espera hasta que se configure una URL válida.

Ejemplo PHP

<?php
$secret = 'your_webhook_secret';
$raw    = file_get_contents('php://input');
$sig    = $_SERVER['HTTP_X_PAYMENT_SIGNATURE']  ?? '';
$ts     = $_SERVER['HTTP_X_PAYMENT_TIMESTAMP'] ?? '0';
$idem   = $_SERVER['HTTP_X_PAYMENT_IDEMPOTENCY'] ?? '';

if (abs(time() - intval($ts)) > 300) {
    http_response_code(401);
    exit('stale timestamp');
}

$signed   = $ts . "\n" . $idem . "\n" . $raw;
$expected = hash_hmac('sha256', $signed, $secret);
if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit('bad signature');
}

$payload = json_decode($raw, true);
if (!is_array($payload) || ($payload['event'] ?? '') !== 'payment.credited') {
    http_response_code(400);
    exit('bad event');
}

if (already_credited($payload['txHash'])) {
    echo json_encode(['ok' => true, 'duplicate' => true]);
    exit;
}

credit_invoice(
    $payload['invoiceId'],
    $payload['amount'],
    $payload['txHash'],
    $payload['gateway']
);

echo json_encode(['ok' => true]);

Ejemplo Node.js

import { createHmac, timingSafeEqual } from 'crypto';
import express from 'express';

const SECRET = process.env.TRIAPAY_WEBHOOK_SECRET!;

app.post('/webhooks/triapay', express.raw({ type: 'application/json' }), (req, res) => {
  const sig  = req.header('X-Payment-Signature')   ?? '';
  const ts   = parseInt(req.header('X-Payment-Timestamp') ?? '0', 10);
  const idem = req.header('X-Payment-Idempotency') ?? '';

  if (Math.abs(Date.now() / 1000 - ts) > 300) return res.status(401).end('stale');

  const signed   = Buffer.concat([Buffer.from(`${ts}\n${idem}\n`), req.body]);
  const expected = createHmac('sha256', SECRET).update(signed).digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(sig,      'hex');
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return res.status(401).end('bad signature');
  }

  const payload = JSON.parse(req.body.toString());
  // ... idempotency check + credit ...
  res.json({ ok: true });
});

Notas de seguridad

  • Trata tu webhook secret como una contraseña. Rótalo de inmediato si se filtra.
  • Verifica siempre contra el body crudo de la solicitud, nunca contra el JSON parseado.
  • Usa siempre comparación en tiempo constante.
  • La URL del webhook debe ser HTTPS y resolver a una IP pública.

¿Necesitas más?

Cualquier cosa más allá de esta página está disponible bajo acuerdo de socio. Contáctanos por WhatsApp en la página de inicio.