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ının biçimini izler ve tx_explorer_url ile imza doğrulamasında kullanılan sign alanını ekler.
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",
"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"
}İ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,
"tx_explorer_url": 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 |
tx_explorer_url | string | null | Blockchain gezginindeki işlem URL'si. txid yoksa veya transfer dahili P2P ise null olur. |
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",
"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"
}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 |
tx_explorer_url | string | null | Blockchain gezginindeki işlem URL'si. txid yoksa veya transfer dahili P2P ise null olur. |
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.
Teslimat ve işleme sözleşmesi
Aşağıdaki sırayı webhook uç noktanız içinde kullanın:
- İstek gövdesini, sırları veya tam imzayı kaydetmeden okuyun.
- Doğru API anahtarını seçmek için bunun bir ödeme/statik-cüzdan olayı mı yoksa bir ödeme olayı mı olduğunu belirleyin.
signkaldırın, belgelenmiş JSON baytlarını çoğaltın, HMAC-SHA256 hesaplayın ve sabit zamanda karşılaştırın.- Gerekli tanımlayıcıları, ondalık dizeleri ve durum değerlerini doğrulayın.
- inbox/idempotency kaydını atomik olarak ekleyin. Zaten varsa yan etkileri tekrarlamadan HTTP 200 döndürün.
- Sipariş/defter mutasyonunu işleyin ve kritik olmayan e-posta, analiz veya bildirimleri sıraya alın.
- Hızlı bir şekilde HTTP 200 döndürün.
idempotency işlemini tutarken yavaş üçüncü taraf servislerini çağırmayın. Bir timeout, işleme koyduktan sonra ama döndürmeden önce, retry oluşturabilir; kopya, işlenmiş inbox anahtarını gözlemlemeli ve etkisiz olmalıdır.
Önerilen idempotency anahtarları
| Olay | Birincil kimlik | Notlar |
|---|---|---|
| Ödeme oturumu | uuid + durum/sürüm kanıtı | Aynı invoice, birden fazla meşru durum değişikliği üretebilir. |
| Kısmi ödeme yüklemesi | invoice uuid + txid | Birden fazla transfer aynı eksik ödenmiş invoice'ya ait olabilir. |
| Statik-cüzdan yatırımı | ağ + txid | Her cüzdana yapılan her yatırma işlemi order_id'yı yeniden kullanır. |
| Ödeme | Ödeme uuid + durum | webhook retry mantığından asla ikinci bir ödeme yaratmayın. |
Şemanızda olay kimliği yoksa, doğrulanmış payload hash'i ek denetim kanıtı olarak saklayın, ancak yukarıdaki işletme kimliklerini zaman damgası ile değiştirmeyin.
Sipariş ve reconciliation
Teslimat en az bir kere yapılabilir ve durum mesajları hızlanabilir. "Son istek kazanır" yerine monoton iş kuralları uygulayın:
- Eski bir etkinlik geç geldiği için yerine getirilen siparişi asla
check'a geri taşımayın; underpaid_check]'in önceki kredileri tekrarlamadan ek txidler almasına izin ver;paidveoverpaid]'i başarılı settlement durumları olarak ele almak, farklı miktarlarını koruyarak;underpaidnihai kısmi ödeme sonucu olarak tutulur; yetkili API daha sonra başka bir durum rapor etmedikçe;aml_lock]'i gözden geçirmeye yönlendirmeli ve genel bir retry çalışanının bunu yerine getirmesine izin vermemeli;- Geçiş imkansız, bağlam eksik veya finansal olarak belirsiz olduğunda ödeme/ödeme bilgisini sorgulayın.
webhook teslimatı sağlıklı görünse bile planlı reconciliation çalıştırın. Yerel terminal durumu ve kredili tutarınızı /v1/payment/info, /v1/static-wallet/transactions veya /v1/payout/status/{uuid} ile karşılaştırın ve farklılıklar hakkında sessizce defter geçmişini üzerine yazmak yerine uyarı yapın.
Webhook uç nokta güvenliği
- HTTPS gerektirmeli ve geri çağrıyı halka açık erişilebilir tut; Özel/geri dönüş geri dönüş hedefleri ödeme oluşturulurken reddedilir.
- Küçük bir istek-gövde sınırı ve JSON içerik türü zorunlu kılın.
- Pahalı çalışmadan önce oran sınırı olsun, ama gerçek patlamalar ve retries için yeterli boşluk bırakın.
- Bir webhook'i sadece kaynak IP ile yetkilendirmeyin. Ağ izin listeleri derinlikte savunmadır; HMAC doğrulaması zorunludur.
sign, API anahtarlarını, politika gerektirdiğinde adresleri ve uygulama kayıtlarından kişisel meta verileri sansür.- Kontrollü bir dönme penceresi boyunca hem güncel hem de açıkça planlanmış yedek anahtarları kullanılabilir tutmak; Bir etkinliği hangi anahtarın işaret ettiğini asla tahmin etmeyin.
- Geçersiz imzalarda genel bir hata gövdesi döndürün ki uç nokta anahtar veya hesap oracle haline gelmesin.