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 ainvoiceId/txHash/matchCode. El plugin DHRU incluido (v2.x o posterior) soporta el nuevo formato de fábrica. tenantId(entero crudo) fue eliminado y reemplazado portenantPublicId, un string opacotnt_*estable por cuenta y seguro para registrar en logs.
Verificación (en orden)
- Rechaza si
|now - X-Payment-Timestamp| > 300segundos. - Calcula
hex(HMAC_SHA256(webhook_secret, "{ts}\n{idempotency}\n{raw_body}"))sobre los bytes crudos del body. No vuelvas a serializar el JSON. - Compara en tiempo constante contra
X-Payment-Signature(PHPhash_equals, Nodecrypto.timingSafeEqual, Pythonhmac.compare_digest). - 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.