Spesifikasi webhook
Format payload, verifikasi tanda tangan, semantik retry.
Spesifikasi webhook
Saat order dikredit, TriaPay menandatangani request POST ke URL webhook yang Anda konfigurasikan. Berkas callback plugin DHRU bawaan adalah receiver standar. Gunakan halaman ini bila Anda menulis sendiri.
Endpoint
Atur URL pada halaman Integration. Dengan plugin DHRU bawaan:
https://yourdhru.com/modules/gateways/callback/triapay.php
Harus HTTPS dan resolve ke IP publik. Alamat privat, loopback, dan link-local ditolak.
Header
| Header | Nilai |
|---|---|
Content-Type |
application/json |
X-Payment-Signature |
hex(HMAC_SHA256(webhook_secret, signed_input)) |
X-Payment-Sig-Version |
2 |
X-Payment-Timestamp |
Unix epoch dalam detik saat penandatanganan |
X-Payment-Idempotency |
Idempotency key order |
X-Payment-Mode |
live atau sandbox |
Signed input
Input HMAC adalah:
{timestamp}\n{idempotency}\n{raw_body}
\n adalah satu line feed ASCII (0x0A). Integrasi baru harus mengimplementasi 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 | Catatan |
|---|---|
event |
Selalu payment.credited. |
invoiceId |
ID invoice DHRU Anda (digemakan dari pembuatan order). |
tenantPublicId |
Pengenal tenant opaque (tnt_*). Stabil per akun. |
amount |
String desimal. Jumlah yang diterima secara persis. |
baseAmount |
String desimal. Jumlah invoice asli sebelum penyerapan over-fee. |
feeDeducted |
String desimal. Selisih antara jumlah yang diharapkan dan yang diterima saat pelanggan membayar surplus biaya jaringan. Biasanya "0". |
fees |
String desimal. Saat ini "0". |
txHash |
Referensi transaksi unik. Hash tx on-chain, transactionId Binance Pay, atau ID deposit off-chain Binance. Gunakan sebagai dedup key saat mengkredit. |
gateway |
Selalu triapay. |
asset |
USDT atau USDC. |
chain |
trc20, bep20, atau binance_pay. BEP20 dan TRC20 juga memunculkan transfer internal Binance (off-chain) di bawah nilai chain yang sama. |
matchCode |
Kode pencocokan numerik. |
mode |
live atau sandbox. Sama dengan nilai header X-Payment-Mode. |
ts |
Sama dengan nilai X-Payment-Timestamp. |
Perubahan breaking
- (2026-05-10) Payload webhook kini menggunakan camelCase untuk semua field. Receiver custom yang membaca
invoice_id/tx_hash/match_code(snake_case) WAJIB diperbarui keinvoiceId/txHash/matchCode. Plugin DHRU bawaan (v2.x atau lebih baru) mendukung format baru secara langsung. tenantId(integer mentah) telah dihapus dan digantitenantPublicId, string opaquetnt_*yang stabil per akun dan aman untuk dicatat.
Verifikasi (berurutan)
- Tolak bila
|now - X-Payment-Timestamp| > 300detik. - Hitung
hex(HMAC_SHA256(webhook_secret, "{ts}\n{idempotency}\n{raw_body}"))atas bytes mentah body. Jangan re-serialize JSON. - Bandingkan dengan waktu konstan terhadap
X-Payment-Signature(PHPhash_equals, Nodecrypto.timingSafeEqual, Pythonhmac.compare_digest). - Tolak bila Anda sudah memproses nilai
X-Payment-Idempotencyini.
Respons yang diharapkan
| Status | Arti |
|---|---|
2xx |
Sukses. Order difinalisasi. |
4xx / 5xx / timeout jaringan |
Diperlakukan sebagai kegagalan. Dicoba ulang. |
Hanya status HTTP yang penting. Isi body dicatat untuk debugging.
Kebijakan retry
Pengiriman yang gagal diulang dengan backoff. Bila retry tidak berhasil, order dapat direkonsiliasi manual dari Admin → Orders → Recredit.
Bila webhook URL tidak diatur, order menunggu hingga URL yang valid dikonfigurasi.
Contoh 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]);
Contoh 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 });
});
Catatan keamanan
- Perlakukan webhook secret Anda seperti kata sandi. Putar segera bila bocor.
- Selalu verifikasi terhadap body request mentah, jangan JSON yang sudah diparse.
- Selalu gunakan perbandingan waktu konstan.
- URL webhook harus HTTPS dan resolve ke IP publik.
Butuh lebih?
Apa pun di luar halaman ini tersedia di bawah perjanjian mitra. Hubungi kami via WhatsApp di halaman beranda.