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:

EventKapan dikirim
transaction.successKetika pembayaran terdeteksi on-chain dan order berubah menjadi Success

Header

HeaderKeterangan
Content-Typeapplication/json
X-Hashpay-EventNama event, mis. transaction.success
X-Hashpay-Signaturesha256=<HMAC-SHA256(raw body, api_key)>
X-Hashpay-Idempotency-KeyHash 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..."
  }
}
FieldKeterangan
paid_amountJumlah crypto yang diterima (termasuk kode unik)
final_amountJumlah bersih yang masuk ke saldo Anda (setelah fee)
tx_hashBukti transaksi on-chain
link_explorerURL 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 internalArti
pendingMenunggu percobaan pertama
sendingSedang dikirim
sentEndpoint Anda membalas 2xx
retryingGagal sementara; dijadwalkan ulang
failedSudah mencapai batas percobaan

Testing lokal

  1. Jalankan server lokal Anda (mis. http://localhost:3000/hashpay/webhook).
  2. Buka tunnel publik (mis. cloudflared tunnel --url http://localhost:3000).
  3. Setel notification_url ke URL tunnel tersebut.
  4. Buat pembayaran testnet via quickstart.