Notifikasi Webhook
Terima pembaruan status pembayaran dan penarikan secara real-time melalui webhook yang ditandatangani HMAC.
Sistem 2328.io mengirim webhook ke url_callback Anda setiap kali status pembayaran berubah. Ini adalah cara yang direkomendasikan untuk mendapatkan notifikasi tentang pembayaran sukses.
Format permintaan
- Method:
POST - Content-Type:
application/json - Tanda tangan: field
signdi body permintaan
Payload
Body webhook mengikuti format respons /v1/payment/info dan menambahkan tx_explorer_url serta field sign untuk verifikasi tanda tangan.
Pembayaran sukses
{
"uuid": "db17d490-15b6-47b9-9015-91d1d8b119f2",
"order_id": "ORDER-12345",
"amount": "180.00000000",
"currency": "RUB",
"url": "https://go.2328.io/db17d490-15b6-47b9-9015-91d1d8b119f2",
"expires_at": "2026-05-09T16:56:58+03:00",
"created_at": "2026-05-09T15:56:58+03:00",
"payer_currency": "TON",
"payer_amount": "0.95256917",
"network": "TON",
"address": "UQA0RevhkCQx-EltyNgPPeG8dqtnCz7ZslOzMdNQlLxVaNBb",
"payment_status": "paid",
"txid": "41c2a327323480af8e705d05deb09c238a41779928832abef4bb77c862357b11",
"tx_explorer_url": "https://tonviewer.com/transaction/41c2a327323480af8e705d05deb09c238a41779928832abef4bb77c862357b11",
"payment_amount": "0.95256917",
"merchant_amount": "0.949711462490000000",
"amount_usd": "2.41324380",
"exchange_rate": "0.01340691",
"sign": "6f8c15b6e53b506d5bfa38ed3fb3b50697af73434262153c02e412541372f04d"
}Pembayaran dibatalkan / gagal
Saat pembayaran tidak dalam status terminal paid, txid, payment_amount, dan merchant_amount adalah null:
{
"uuid": "48edaf2d-2c49-4638-8f86-88636f661c1f",
"order_id": "ORDER-12345",
"amount": "2800.00000000",
"currency": "RUB",
"url": "https://go.2328.io/48edaf2d-2c49-4638-8f86-88636f661c1f",
"expires_at": "2026-05-09T06:19:04+03:00",
"created_at": "2026-05-09T05:19:04+03:00",
"payer_currency": "ETH",
"payer_amount": "0.01620968",
"network": "ETH-ERC20",
"address": "0x37c20d6d96d130Bc5B33D832e43b8e16aACe0c59",
"payment_status": "cancel",
"txid": null,
"tx_explorer_url": null,
"payment_amount": null,
"merchant_amount": null,
"amount_usd": "37.53934800",
"exchange_rate": "0.01340691",
"sign": "40ce68ad9691ad54e684329d75ab5adaf5b01409a2d18d3e0110b8c1be605342"
}Referensi field
| Field | Tipe | Deskripsi |
|---|---|---|
uuid | string | UUID pembayaran |
order_id | string | ID pesanan Anda |
amount | decimal (8 dp) | Jumlah fiat dalam currency |
currency | string | Mata uang fiat yang diminta merchant |
url | string | URL checkout terhosting |
expires_at | string (ISO 8601) | Saat sesi pembayaran kedaluwarsa |
created_at | string (ISO 8601) | Saat sesi pembayaran dibuat |
payer_currency | string | Kripto yang dibayarkan oleh pembayar |
payer_amount | decimal (8 dp) | Jumlah kripto yang diharapkan |
network | string | Jaringan blockchain |
address | string | Alamat deposit |
payment_status | string | Salah satu dari: pending, check, paid, underpaid_check, underpaid, overpaid, cancel, aml_lock (lihat References) |
txid | string | null | Hash tx blockchain, hadir hanya setelah pembayaran terkonfirmasi |
tx_explorer_url | string | null | URL transaksi di penjelajah blockchain. Bernilai null jika txid tidak ada atau transfer merupakan P2P internal. |
payment_amount | decimal | null | Jumlah aktual yang dibayar, hadir hanya setelah pembayaran |
merchant_amount | decimal (18 dp) | null | Jumlah yang dikreditkan ke merchant setelah biaya |
amount_usd | decimal (8 dp) | Jumlah dalam USD pada saat pembuatan |
exchange_rate | decimal | Nilai tukar Kripto / fiat yang digunakan |
sign | string (hex) | Tanda tangan HMAC-SHA256 dari payload |
Memverifikasi tanda tangan
Untuk memverifikasi tanda tangan webhook:
- Ekstrak field
signdari payload - Hapus field
signdari objek - Encode field yang tersisa sebagai JSON
- Encode JSON tersebut ke Base64
- Hitung HMAC-SHA256 dari string Base64 menggunakan API_KEY Anda
- Bandingkan tanda tangan yang dihitung dengan nilai
signmenggunakan perbandingan constant-time
<?php
function verifyWebhookSign(array $data, string $apiKey): bool {
$receivedSign = $data['sign'] ?? '';
unset($data['sign']);
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$base64 = base64_encode($json);
$calculated = hash_hmac('sha256', $base64, $apiKey);
return hash_equals($calculated, $receivedSign);
}
$apiKey = 'YOUR_API_KEY';
$payload = json_decode(file_get_contents('php://input'), true);
if (!verifyWebhookSign($payload, $apiKey)) {
http_response_code(401);
exit;
}
switch ($payload['payment_status']) {
case 'paid':
case 'overpaid':
// Credit the order — check idempotency by order_id first
break;
case 'underpaid_check':
case 'underpaid':
case 'cancel':
break;
}
http_response_code(200);import crypto from "crypto";
import express from "express";
const app = express();
app.use(express.json());
function verifyWebhookSign(payload, apiKey) {
const { sign, ...rest } = payload;
const json = JSON.stringify(rest);
const base64 = Buffer.from(json).toString("base64");
const calculated = crypto
.createHmac("sha256", apiKey)
.update(base64)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(calculated),
Buffer.from(sign || ""),
);
}
app.post("/webhook", (req, res) => {
if (!verifyWebhookSign(req.body, process.env.API_KEY)) {
return res.sendStatus(401);
}
const { order_id, payment_status, txid } = req.body;
if (payment_status === "paid" || payment_status === "overpaid") {
// Credit the order — check idempotency by order_id first
}
res.sendStatus(200);
});import json
import hmac
import hashlib
import base64
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
API_KEY = "YOUR_API_KEY"
def verify_webhook_sign(payload: dict, api_key: str) -> bool:
received = payload.pop("sign", "")
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
b64 = base64.b64encode(body.encode("utf-8")).decode()
calculated = hmac.new(api_key.encode(), b64.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(calculated, received)
@app.post("/webhook")
async def webhook(request: Request):
payload = await request.json()
if not verify_webhook_sign(payload, API_KEY):
raise HTTPException(401)
if payload["payment_status"] in ("paid", "overpaid"):
# Credit the order — check idempotency by order_id first
pass
return {"ok": True}package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"net/http"
)
func verifyWebhookSign(body []byte, apiKey string) (map[string]any, bool) {
var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil {
return nil, false
}
received, _ := payload["sign"].(string)
delete(payload, "sign")
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.Encode(payload)
reencoded := bytes.TrimRight(buf.Bytes(), "\n")
b64 := base64.StdEncoding.EncodeToString(reencoded)
h := hmac.New(sha256.New, []byte(apiKey))
h.Write([]byte(b64))
calculated := hex.EncodeToString(h.Sum(nil))
return payload, hmac.Equal([]byte(calculated), []byte(received))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
payload, ok := verifyWebhookSign(body, apiKey)
if !ok {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
status, _ := payload["payment_status"].(string)
if status == "paid" || status == "overpaid" {
// Credit the order — check idempotency first
}
w.WriteHeader(http.StatusOK)
}require "json"
require "openssl"
require "base64"
require "sinatra"
API_KEY = "YOUR_API_KEY"
def verify_webhook_sign(payload, api_key)
received = payload.delete("sign") || ""
body = payload.to_json
b64 = Base64.strict_encode64(body)
calculated = OpenSSL::HMAC.hexdigest("SHA256", api_key, b64)
OpenSSL.fixed_length_secure_compare(calculated, received)
end
post "/webhook" do
payload = JSON.parse(request.body.read)
halt 401 unless verify_webhook_sign(payload, API_KEY)
if %w[paid overpaid].include?(payload["payment_status"])
# Credit the order — check idempotency by order_id first
end
status 200
endSelalu verifikasi tanda tangan sebelum mengkreditkan dana apa pun ke pengguna. Webhook yang tidak ditandatangani atau ditandatangani secara tidak benar dapat menjadi permintaan palsu.
Webhook penarikan
Saat status sebuah penarikan berubah, sistem mengirim webhook POST ke URL url_callback yang diteruskan saat penarikan dibuat. Jika url_callback tidak disediakan, tidak ada webhook yang dikirim untuk penarikan tersebut.
Webhook penarikan harus diverifikasi dengan Payout API key Anda — bukan API key biasa. Algoritma penandatanganan identik dengan webhook pembayaran (hapus sign, encode JSON, base64, HMAC-SHA256), hanya key yang berbeda.
Payload
{
"uuid": "019dff1f-0dbd-7277-8d45-271e7775388f",
"order_id": "4dfdcc84402b1185b71cbe399321533e",
"status": "completed",
"currency": "TRX",
"network": "TRX-TRC20",
"amount": "3.00",
"merchant_amount": "3.00",
"network_amount": "3.00",
"amount_usd": "1.04",
"to_address": "THauRv5tcucQRohXg8NiyGTk16DX1XQG5x",
"memo": null,
"txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
"tx_explorer_url": "https://tronscan.org/#/transaction/9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
"block_number": null,
"error_type": null,
"created_at": "2026-05-07T00:08:38+03:00",
"updated_at": "2026-05-07T00:08:54+03:00",
"from_currency": "USDT",
"debited_amount": "1.050735",
"debited_currency": "USDT",
"sign": "925ad7bf3d6841864101f7cc2c7e30652e70a06cdb04dbe07a0129480000ce4a"
}Referensi field
| Field | Tipe | Deskripsi |
|---|---|---|
uuid | string | UUID penarikan |
order_id | string | ID idempotensi / referensi Anda, jika Anda menyediakannya |
status | string | pending, completed, failed, cancelled (lihat References) |
currency | string | Mata uang penarikan |
network | string | Jaringan blockchain |
amount | decimal | Jumlah penarikan (dalam currency) |
merchant_amount | decimal | Jumlah yang dibebankan dari saldo merchant |
network_amount | decimal | Jumlah yang sebenarnya dikirim on-chain |
amount_usd | decimal | Nilai USD pada saat penarikan |
to_address | string | Alamat blockchain penerima |
memo | string | null | Memo / destination tag, jika digunakan |
txid | string | null | Hash transaksi blockchain, ditetapkan saat completed |
tx_explorer_url | string | null | URL transaksi di penjelajah blockchain. Bernilai null jika txid tidak ada atau transfer merupakan P2P internal. |
block_number | integer | null | Tinggi blok dari transaksi on-chain |
error_type | string | null | Alasan saat status = failed (mis. aml_risk, lihat References) |
created_at | string (ISO 8601) | Saat penarikan dibuat |
updated_at | string (ISO 8601) | Saat status terakhir berubah |
from_currency | string | Saldo sumber yang didebet untuk penarikan ketika konversi otomatis digunakan (mis. USDT untuk penarikan BTC) |
debited_amount | decimal | Jumlah yang didebit dari saldo from_currency |
debited_currency | string | Mata uang dari debit |
sign | string (hex) | Tanda tangan HMAC-SHA256 dari payload, ditandatangani dengan Payout API key |
Praktik terbaik
- Idempotensi — Selalu periksa apakah pembayaran sudah diproses (berdasarkan
order_idatauuuid). Webhook bisa tiba beberapa kali. - Respon cepat — Kembalikan HTTP 200 secepat mungkin. Pindahkan pekerjaan berat ke antrean latar belakang.
- Retry — Jika sistem tidak menerima HTTP 200, webhook dikirim ulang setelah 2 menit. Maksimum 5 kali percobaan retry.
- Pemrosesan async — Tangani event webhook secara asinkron untuk menghindari pemblokiran respon.
- Keamanan — SELALU verifikasi tanda tangan
signsebelum mempercayai payload.
Webhook bisa tiba dengan urutan yang tidak sesuai. Jangan asumsikan webhook pertama yang Anda terima adalah status final — selalu ambil ulang melalui /v1/payment/info (atau /v1/payout/status/{uuid}) jika Anda perlu kepastian.
Kontrak pengiriman dan pemrosesan
Gunakan urutan berikut di dalam endpoint webhook Anda:
- Baca isi permintaan tanpa mencatat rahasia atau tanda tangan lengkap.
- Identifikasi apakah itu adalah event pembayaran/dompet statis atau event pembayaran keluar sehingga Anda memilih API key yang benar.
- Hapus
sign, reproduksi byte JSON yang didokumentasikan, hitung HMAC-SHA256, dan bandingkan dalam waktu konstan. - Validasi pengenal yang diperlukan, string desimal, dan nilai status.
- Masukkan catatan inbox/idempotensi secara atomik. Jika sudah ada, kembalikan HTTP 200 tanpa mengulangi efek samping.
- Komit mutasi pesanan/buku besar dan antrekan email, analitik, atau notifikasi yang tidak kritis.
- Kembalikan HTTP 200 dengan cepat.
Jangan memanggil layanan pihak ketiga yang lambat saat memegang transaksi idempoten. Timeout setelah Anda melakukan komit tetapi sebelum mengembalikan dapat menyebabkan percobaan ulang; duplikat harus memperhatikan kunci inbox yang telah dikomit dan menjadi tanpa operasi.
Kunci idempoten yang direkomendasikan
| Acara | Identitas utama | Catatan |
|---|---|---|
| Sesi pembayaran | uuid + bukti status/versi | Faktur yang sama dapat menghasilkan beberapa perubahan status yang sah. |
| Isi ulang pembayaran sebagian | faktur uuid + txid | Lebih dari satu transfer bisa dimiliki oleh faktur yang sama yang kurang dibayar. |
| Deposit dompet statis | jaringan + txid | order_id digunakan kembali oleh setiap deposit ke dompet itu. |
| Pembayaran | pembayaran uuid + status | Jangan pernah membuat pembayaran kedua dari logika percobaan ulang webhook. |
Jika skema Anda tidak memiliki id acara, simpan hash payload yang terverifikasi sebagai bukti audit tambahan, tetapi jangan mengganti identitas bisnis di atas dengan cap waktu.
Pemesan dan rekonsiliasi
Pengiriman setidaknya-satu-kali dan pesan status dapat bersaing. Terapkan aturan bisnis monotonic daripada “permintaan terakhir menang”:
- jangan pernah memindahkan pesanan yang telah dipenuhi kembali ke
checkkarena event yang lebih lama tiba terlambat; - izinkan
underpaid_checkmenerima txid tambahan tanpa mengulang kredit sebelumnya; - anggap
paiddanoverpaidsebagai status penyelesaian yang berhasil, sambil mempertahankan jumlah yang berbeda; - tetap pertahankan
underpaidsebagai hasil pembayaran parsial final kecuali API yang berwenang kemudian melaporkan status lain; - arahkan
aml_lockke peninjauan dan jangan biarkan pekerja ulang generik memenuhinya; - meminta info pembayaran/pencairan kapan pun transisi tidak mungkin, konteks hilang, atau secara finansial ambigu.
Jalankan rekonsiliasi terjadwal bahkan ketika pengiriman webhook tampak sehat. Bandingkan status terminal lokal dan jumlah yang dikreditkan dengan /v1/payment/info, /v1/static-wallet/transactions, atau /v1/payout/status/{uuid} dan beri peringatan pada perbedaan alih-alih menimpa riwayat pembukuan secara diam-diam.
Keamanan endpoint webhook
- Mewajibkan HTTPS dan menjaga callback tetap bisa diakses publik; target callback privat/loopback ditolak saat pembuatan pembayaran.
- Terapkan batas kecil pada body permintaan dan tipe konten JSON.
- Batasi kecepatan sebelum pekerjaan yang mahal, tetapi berikan ruang cukup untuk lonjakan sah dan percobaan ulang.
- Jangan pernah mengizinkan webhook hanya berdasarkan IP sumber. Daftar izin jaringan adalah pertahanan berlapis; verifikasi HMAC wajib.
- Sunting
sign, kunci API, alamat bila diperlukan oleh kebijakan, dan metadata pribadi dari log aplikasi. - Simpan baik kunci saat ini maupun kunci pengganti yang dijadwalkan secara eksplisit selama jendela rotasi terkontrol; jangan pernah menebak kunci mana yang menandatangani sebuah kejadian.
- Kembalikan isi kesalahan umum pada tanda tangan yang tidak valid agar endpoint tidak menjadi oracle kunci atau akun.