Webhook Bildirimleri
HMAC ile imzalanmış webhook'lar üzerinden gerçek zamanlı ödeme ve çekim durumu güncellemeleri alın.
2328.io sistemi, bir ödeme durumu her değiştiğinde url_callback adresinize bir webhook gönderir. Başarılı ödemeler hakkında bildirim almanın önerilen yolu budur.
İstek formatı
- Method:
POST - Content-Type:
application/json - İmza: istek gövdesindeki
signalanı
Payload
Webhook gövdesi, /v1/payment/info yanıtıyla aynıdır, ek olarak imza doğrulaması için kullanılan bir sign alanı içerir.
Başarılı ödeme
{
"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"
}İptal edilmiş / başarısız ödeme
Ödeme terminal paid durumunda olmadığında txid, payment_amount ve merchant_amount null'dur:
{
"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"
}Alan referansı
| Alan | Tip | Açıklama |
|---|---|---|
uuid | string | Ödeme UUID'si |
order_id | string | Sipariş ID'niz |
amount | decimal (8 dp) | currency cinsinden fiat tutarı |
currency | string | Merchant'ın talep ettiği fiat para birimi |
url | string | Hosted checkout URL'si |
expires_at | string (ISO 8601) | Ödeme oturumunun ne zaman süresi dolar |
created_at | string (ISO 8601) | Ödeme oturumunun ne zaman oluşturulduğu |
payer_currency | string | Ödeyenin ödeme yaptığı kripto |
payer_amount | decimal (8 dp) | Beklenen kripto tutarı |
network | string | Blockchain ağı |
address | string | Yatırma adresi |
payment_status | string | Şunlardan biri: pending, check, paid, underpaid_check, underpaid, overpaid, cancel, aml_lock (bkz. References) |
txid | string | null | Blockchain tx hash'i, yalnızca onaylanmış bir ödemeden sonra mevcut |
payment_amount | decimal | null | Gerçek ödenen tutar, yalnızca ödemeden sonra mevcut |
merchant_amount | decimal (18 dp) | null | Ücretler sonrası merchant'a yansıtılan tutar |
amount_usd | decimal (8 dp) | Oluşturma anındaki USD tutarı |
exchange_rate | decimal | Kullanılan kripto / fiat döviz kuru |
sign | string (hex) | Payload'un HMAC-SHA256 imzası |
İmzayı doğrulama
Bir webhook imzasını doğrulamak için:
- Payload'dan
signalanını çıkarın - Nesneden
signalanını kaldırın - Kalan alanları JSON olarak encode edin
- JSON'u Base64 ile encode edin
- API_KEY'inizi kullanarak Base64 string'inden HMAC-SHA256 hesaplayın
- Hesaplanan imzayı
signdeğeri ile sabit zamanlı bir karşılaştırma kullanarak karşılaştırın
<?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
endBir kullanıcıya herhangi bir fon yansıtmadan önce her zaman imzayı doğrulayın. İmzasız veya yanlış imzalanmış bir webhook, sahte bir istek olabilir.
Çekim webhook'ları
Bir çekimin status'u değiştiğinde, sistem çekim oluşturulurken geçirilen url_callback URL'ine bir POST webhook gönderir. url_callback sağlanmadıysa, o çekim için webhook gönderilmez.
Çekim webhook'ları normal API key ile değil, Payout API key'iniz ile doğrulanmalıdır. İmzalama algoritması ödeme webhook'larıyla aynıdır (sign'i çıkar, JSON'a encode et, base64, HMAC-SHA256), yalnızca anahtar farklıdır.
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"
}Alan referansı
| Alan | Tip | Açıklama |
|---|---|---|
uuid | string | Çekim UUID'si |
order_id | string | Sağladıysanız idempotency / referans ID'niz |
status | string | pending, completed, failed, cancelled (bkz. References) |
currency | string | Çekim para birimi |
network | string | Blockchain ağı |
amount | decimal | Çekim tutarı (currency cinsinden) |
merchant_amount | decimal | Merchant bakiyesinden tahsil edilen tutar |
network_amount | decimal | Zincir üzerinde gerçekten gönderilen tutar |
amount_usd | decimal | Çekim anındaki USD değeri |
to_address | string | Alıcı blockchain adresi |
memo | string | null | Memo / hedef etiketi, kullanıldığında |
txid | string | null | Blockchain işlem hash'i, completed durumunda ayarlanır |
block_number | integer | null | Zincir üzerindeki işlemin blok yüksekliği |
error_type | string | null | status = failed olduğunda neden (örn. aml_risk, bkz. References) |
created_at | string (ISO 8601) | Çekimin oluşturulduğu zaman |
updated_at | string (ISO 8601) | Durumun en son ne zaman değiştiği |
from_currency | string | Otomatik dönüştürme kullanıldığında çekimin düşüldüğü kaynak bakiye (örn. BTC çekimi için USDT) |
debited_amount | decimal | from_currency bakiyesinden düşülen tutar |
debited_currency | string | Düşmenin para birimi |
sign | string (hex) | Payload'un Payout API key ile imzalanmış HMAC-SHA256 imzası |
En iyi uygulamalar
- Idempotency — Ödemenin zaten işlenip işlenmediğini her zaman kontrol edin (
order_idveyauuidile). Webhook'lar birden çok kez gelebilir. - Hızlı yanıt — Mümkün olduğunca hızlı HTTP 200 dönün. Ağır işleri arka plan kuyruğuna devredin.
- Yeniden denemeler — Sistem HTTP 200 alamazsa, webhook 2 dakika sonra yeniden gönderilir. En fazla 5 yeniden deneme.
- Asenkron işleme — Yanıtı bloke etmemek için webhook olaylarını asenkron işleyin.
- Güvenlik — Payload'a güvenmeden önce HER ZAMAN
signimzasını doğrulayın.
Webhook'lar sıra dışı gelebilir. Aldığınız ilk webhook'un nihai durum olduğunu varsaymayın — kesinlik istiyorsanız her zaman /v1/payment/info (veya /v1/payout/status/{uuid}) üzerinden yeniden alın.