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 identik dengan respon /v1/payment/info, ditambah field sign yang digunakan 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",
"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,
"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 |
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",
"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 |
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.