Ir para o conteúdo principal
← Voltar para documentação

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 para invoiceId / 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 por tenantPublicId, uma string opaca tnt_* estável por conta e segura para logar.

Verificação (em ordem)

  1. Rejeite se |now - X-Payment-Timestamp| > 300 segundos.
  2. Calcule hex(HMAC_SHA256(webhook_secret, "{ts}\n{idempotency}\n{raw_body}")) sobre os bytes crus do body. Não reserialize o JSON.
  3. Compare em tempo constante contra X-Payment-Signature (PHP hash_equals, Node crypto.timingSafeEqual, Python hmac.compare_digest).
  4. 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.