Webhooks
Webhook memberi tahu sistem Anda bahwa pembayaran sudah diterima — tanpa polling. Callback dikirim ke notification_url merchant, ditandatangani HMAC-SHA256, dan diulang otomatis bila endpoint Anda gagal merespons.
Konfigurasi
Setel notification_url di dashboard merchant (Settings). Gunakan URL HTTPS publik, mis. https://api.tokoanda.com/hashpay/webhook.
Event
Saat ini Hashpay mengirim satu event utama:
| Event | Kapan dikirim |
|---|---|
transaction.success | Ketika pembayaran terdeteksi on-chain dan order berubah menjadi Success |
Header
| Header | Keterangan |
|---|---|
Content-Type | application/json |
X-Hashpay-Event | Nama event, mis. transaction.success |
X-Hashpay-Signature | sha256=<HMAC-SHA256(raw body, api_key)> |
X-Hashpay-Idempotency-Key | Hash transaksi on-chain — unik per pembayaran |
Payload
json
{
"event": "transaction.success",
"data": {
"order_no": "INV-2026-0001",
"merchant_id": "10001",
"status": "Success",
"token": "USDT",
"network": "ETHEREUM",
"paid_amount": "8.424753",
"final_amount": "8.340510",
"tx_hash": "0x9f2c...",
"link_explorer": "https://sepolia.etherscan.io/tx/0x9f2c..."
}
}| Field | Keterangan |
|---|---|
paid_amount | Jumlah crypto yang diterima (termasuk kode unik) |
final_amount | Jumlah bersih yang masuk ke saldo Anda (setelah fee) |
tx_hash | Bukti transaksi on-chain |
link_explorer | URL explorer untuk verifikasi manual |
Verifikasi tanda tangan
Hitung HMAC-SHA256 atas raw body memakai API key merchant Anda, lalu bandingkan dengan header secara constant-time.
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
// Raw body WAJIB untuk verifikasi tanda tangan.
app.post(
"/hashpay/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const expected =
"sha256=" +
crypto.createHmac("sha256", process.env.HASHPAY_API_KEY)
.update(req.body)
.digest("hex");
const provided = req.header("X-Hashpay-Signature") ?? "";
const ok =
expected.length === provided.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
if (!ok) return res.status(401).send("invalid signature");
const event = JSON.parse(req.body.toString());
const idempotencyKey = req.header("X-Hashpay-Idempotency-Key");
// Proses sekali saja per idempotencyKey (simpan di DB dengan unique index).
handlePayment(event, idempotencyKey);
res.status(200).json({ received: true });
}
);PHP (Laravel)
$raw = $request->getContent();
$expected = 'sha256=' . hash_hmac('sha256', $raw, config('services.hashpay.api_key'));
$provided = $request->header('X-Hashpay-Signature', '');
if (!hash_equals($expected, $provided)) {
abort(401, 'invalid signature');
}Retry & status pengiriman
Bila endpoint Anda membalas non-2xx (atau timeout), callback dicoba ulang dengan backoff eksponensial hingga 3 percobaan:
| Status internal | Arti |
|---|---|
pending | Menunggu percobaan pertama |
sending | Sedang dikirim |
sent | Endpoint Anda membalas 2xx |
retrying | Gagal sementara; dijadwalkan ulang |
failed | Sudah mencapai batas percobaan |
Testing lokal
- Jalankan server lokal Anda (mis.
http://localhost:3000/hashpay/webhook). - Buka tunnel publik (mis.
cloudflared tunnel --url http://localhost:3000). - Setel
notification_urlke URL tunnel tersebut. - Buat pembayaran testnet via quickstart.