Especificação do webhook
Formato de payload, verificação de assinatura, semântica de retry.
Especificação do webhook
Quando uma ordem é creditada, o TriaPay assina um POST para a URL de webhook que você configurou. O arquivo de callback do plugin DHRU incluído é o receiver padrão. Use esta página se for escrever o seu próprio.
Endpoint
Defina a URL na página Integration. Com o plugin DHRU incluído:
https://yourdhru.com/modules/gateways/callback/triapay.php
Ou, para uma instalação de DHRU em subcaminho:
https://yourdomain.com/dhru/modules/gateways/callback/triapay.php
Deve ser HTTPS e resolver para um IP público. Endereços privados, loopback e link-local são rejeitados.
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 em segundos no momento da assinatura |
X-Payment-Idempotency |
Idempotency key da ordem |
X-Payment-Mode |
live ou sandbox |
Signed input
A entrada do HMAC é:
{timestamp}\n{idempotency}\n{raw_body}
\n é um único line feed ASCII (0x0A). Novas integrações devem implementar a 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 | Observações |
|---|---|
event |
Sempre payment.credited. |
invoiceId |
Seu invoice id do DHRU (ecoado da criação da ordem). |
tenantPublicId |
Identificador opaco do tenant (tnt_*). Estável por conta. |
amount |
String decimal. Valor exato recebido. |
baseAmount |
String decimal. Valor original da fatura antes de qualquer absorção de over-fee. |
feeDeducted |
String decimal. Diferença entre o valor esperado e o recebido quando o cliente paga um excedente de taxa de rede. Normalmente "0". |
fees |
String decimal. Atualmente "0". |
txHash |
Referência única da transação. Hash de tx on-chain, transactionId do Binance Pay, ou id de depósito off-chain da Binance. Use como dedup key ao creditar. |
gateway |
Sempre triapay. |
asset |
USDT ou USDC. |
chain |
trc20, bep20, ou binance_pay. BEP20 e TRC20 também expõem transferências internas (off-chain) da Binance sob o mesmo valor de chain. |
matchCode |
Código de correspondência numérico. |
mode |
live ou sandbox. Mesmo valor do header X-Payment-Mode. |
ts |
Mesmo valor de X-Payment-Timestamp. |
Mudanças incompatíveis
- (2026-05-10) O payload do webhook agora usa camelCase em todos os campos. Receivers customizados que leem
invoice_id/tx_hash/match_code(snake_case) DEVEM atualizar parainvoiceId/txHash/matchCode. O plugin DHRU incluído (v2.x ou posterior) suporta o novo formato de fábrica. tenantId(inteiro cru) foi removido e substituído portenantPublicId, uma string opacatnt_*estável por conta e segura para logar.
Verificação (em ordem)
- Rejeite se
|now - X-Payment-Timestamp| > 300segundos. - Calcule
hex(HMAC_SHA256(webhook_secret, "{ts}\n{idempotency}\n{raw_body}"))sobre os bytes crus do body. Não reserialize o JSON. - Compare em tempo constante contra
X-Payment-Signature(PHPhash_equals, Nodecrypto.timingSafeEqual, Pythonhmac.compare_digest). - Rejeite se já processou este valor de
X-Payment-Idempotency.
Resposta esperada
| Status | Significado |
|---|---|
2xx |
Sucesso. Ordem finalizada. |
4xx / 5xx / network timeout |
Tratado como falha. Retentado. |
Só o status HTTP importa. O conteúdo do body fica logado para depuração.
Política de retry
Entregas que falham são retentadas com backoff. Se os retries não tiverem sucesso, a ordem pode ser reconciliada manualmente em Admin → Orders → Recredit.
Se nenhuma webhook URL estiver configurada, a ordem aguarda até que uma URL válida seja configurada.
Exemplo 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]);
Exemplo 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 segurança
- Trate seu webhook secret como uma senha. Rotacione imediatamente se vazar.
- Verifique sempre contra o body cru da requisição, nunca o JSON parseado.
- Use sempre comparação em tempo constante.
- A URL do webhook precisa ser HTTPS e resolver para um IP público.
Precisa de mais?
Qualquer coisa além desta página está disponível sob acordo de parceria. Fale com a gente pelo WhatsApp na home.