Webhook-Benachrichtigungen
Erhalten Sie Statusaktualisierungen für Zahlungen und Auszahlungen in Echtzeit über HMAC-signierte Webhooks.
Das System von 2328.io sendet einen Webhook an Ihre url_callback, sobald sich der Status einer Zahlung ändert. Dies ist die empfohlene Methode, um über erfolgreiche Zahlungen benachrichtigt zu werden.
Anfrageformat
- Methode:
POST - Content-Type:
application/json - Signatur: Feld
signim Anfrage-Body
Payload
Der Webhook-Body folgt dem Format der Antwort von /v1/payment/info und ergänzt tx_explorer_url sowie das sign-Feld zur Signaturprüfung.
Erfolgreiche Zahlung
{
"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"
}Stornierte / fehlgeschlagene Zahlung
Wenn sich die Zahlung nicht im finalen Zustand paid befindet, sind txid, payment_amount und merchant_amount 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"
}Feldreferenz
| Feld | Typ | Beschreibung |
|---|---|---|
uuid | string | Zahlungs-UUID |
order_id | string | Ihre Bestell-ID |
amount | decimal (8 dp) | Fiat-Betrag in currency |
currency | string | Vom Händler angeforderte Fiat-Währung |
url | string | URL des gehosteten Checkouts |
expires_at | string (ISO 8601) | Zeitpunkt, an dem die Zahlungssitzung abläuft |
created_at | string (ISO 8601) | Zeitpunkt, an dem die Zahlungssitzung erstellt wurde |
payer_currency | string | Krypto, in der der Zahler bezahlt |
payer_amount | decimal (8 dp) | Erwarteter Krypto-Betrag |
network | string | Blockchain-Netzwerk |
address | string | Einzahlungsadresse |
payment_status | string | Einer von: pending, check, paid, underpaid_check, underpaid, overpaid, cancel, aml_lock (siehe References) |
txid | string | null | Hash der Blockchain-Transaktion, nur nach bestätigter Zahlung vorhanden |
tx_explorer_url | string | null | URL der Transaktion im Blockchain-Explorer. null, wenn keine txid vorhanden ist oder es sich um einen internen P2P-Transfer handelt. |
payment_amount | decimal | null | Tatsächlich gezahlter Betrag, nur nach Zahlung vorhanden |
merchant_amount | decimal (18 dp) | null | Dem Händler nach Gebühren gutgeschriebener Betrag |
amount_usd | decimal (8 dp) | Betrag in USD zum Zeitpunkt der Erstellung |
exchange_rate | decimal | Verwendeter Krypto-/Fiat-Wechselkurs |
sign | string (hex) | HMAC-SHA256-Signatur des payload |
Signatur verifizieren
So verifizieren Sie eine Webhook-Signatur:
- Extrahieren Sie das Feld
signaus dem payload - Entfernen Sie das Feld
signaus dem Objekt - Codieren Sie die übrigen Felder als JSON
- Codieren Sie das JSON in base64
- Berechnen Sie HMAC-SHA256 vom Base64-String unter Verwendung Ihres API_KEY
- Vergleichen Sie die berechnete Signatur mit dem Wert
signmittels eines konstantzeitigen Vergleichs
<?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
endVerifizieren Sie immer die Signatur, bevor Sie einem Nutzer Mittel gutschreiben. Ein nicht oder falsch signierter Webhook könnte eine gefälschte Anfrage sein.
Auszahlungs-Webhooks
Wenn sich der status einer Auszahlung ändert, sendet das System einen POST-Webhook an die url_callback-URL, die bei der Erstellung der Auszahlung übergeben wurde. Wurde keine url_callback angegeben, werden für diese Auszahlung keine Webhooks gesendet.
Auszahlungs-Webhooks müssen mit Ihrem Payout-API-Schlüssel verifiziert werden — nicht mit dem regulären API-Schlüssel. Der Signieralgorithmus ist identisch zu dem für Zahlungs-Webhooks (Feld sign entfernen, JSON-codieren, base64, HMAC-SHA256), nur der Schlüssel ist ein anderer.
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"
}Feldreferenz
| Feld | Typ | Beschreibung |
|---|---|---|
uuid | string | Auszahlungs-UUID |
order_id | string | Ihre Idempotenz- / Referenz-ID, sofern angegeben |
status | string | pending, completed, failed, cancelled (siehe References) |
currency | string | Auszahlungswährung |
network | string | Blockchain-Netzwerk |
amount | decimal | Auszahlungsbetrag (in currency) |
merchant_amount | decimal | Vom Händlerguthaben belasteter Betrag |
network_amount | decimal | Tatsächlich on-chain gesendeter Betrag |
amount_usd | decimal | USD-Wert zum Zeitpunkt der Auszahlung |
to_address | string | Blockchain-Adresse des Empfängers |
memo | string | null | Memo / Destination-Tag, falls verwendet |
txid | string | null | Hash der Blockchain-Transaktion, gesetzt bei completed |
tx_explorer_url | string | null | URL der Transaktion im Blockchain-Explorer. null, wenn keine txid vorhanden ist oder es sich um einen internen P2P-Transfer handelt. |
block_number | integer | null | Blockhöhe der on-chain-Transaktion |
error_type | string | null | Fehlerursache, wenn status = failed (z. B. aml_risk, siehe References) |
created_at | string (ISO 8601) | Zeitpunkt, an dem die Auszahlung erstellt wurde |
updated_at | string (ISO 8601) | Zeitpunkt der letzten Statusänderung |
from_currency | string | Quell-Guthaben, von dem die Auszahlung bei automatischer Umrechnung abgebucht wurde (z. B. USDT für eine BTC-Auszahlung) |
debited_amount | decimal | Vom from_currency-Guthaben abgebuchter Betrag |
debited_currency | string | Währung der Abbuchung |
sign | string (hex) | HMAC-SHA256-Signatur des payload, signiert mit dem Payout-API-Schlüssel |
Best Practices
- Idempotenz — Prüfen Sie stets, ob die Zahlung bereits verarbeitet wurde (anhand von
order_idoderuuid). Webhooks können mehrfach eintreffen. - Schnelle Antwort — Antworten Sie so schnell wie möglich mit HTTP 200. Lagern Sie aufwändige Arbeit in eine Hintergrund-Queue aus.
- Wiederholungen — Erhält das System keine HTTP-200-Antwort, wird der Webhook nach 2 Minuten erneut gesendet. Maximal 5 Wiederholungsversuche.
- Asynchrone Verarbeitung — Verarbeiten Sie Webhook-Ereignisse asynchron, um die Antwort nicht zu blockieren.
- Sicherheit — Verifizieren Sie IMMER die
sign-Signatur, bevor Sie dem payload vertrauen.
Webhooks können in beliebiger Reihenfolge eintreffen. Gehen Sie nicht davon aus, dass der erste empfangene Webhook der finale Zustand ist — rufen Sie bei Unsicherheit immer erneut über /v1/payment/info (oder /v1/payout/status/{uuid}) ab.
Liefer- und Verarbeitungsvertrag
Verwenden Sie die folgende Reihenfolge in Ihrem webhook-Endpunkt:
- Lesen Sie den Anforderungstext, ohne Geheimnisse oder die vollständige Signatur zu protokollieren.
- Ermitteln Sie, ob es sich um ein Zahlungs-/statisches Wallet-Ereignis oder ein Auszahlungsevent handelt, um den richtigen API-Schlüssel auszuwählen.
- Entfernen Sie
sign, reproduzieren Sie die dokumentierten JSON-Bytes, berechnen Sie HMAC-SHA256 und vergleichen Sie im konstanten Zeitintervall. - Validieren Sie die erforderlichen Kennungen, Dezimalstrings und Statuswerte.
- Fügen Sie atomar einen inbox/idempotency-Datensatz ein. Wenn er bereits existiert, geben Sie HTTP 200 zurück, ohne Nebeneffekte zu wiederholen.
- Bestätigen Sie die Mutation der Bestellung/der Buchführung und stellen Sie nicht-kritische E-Mails, Analysen oder Benachrichtigungen in die Warteschlange.
- Geben Sie HTTP 200 schnell zurück.
Rufen Sie keine langsamen Dienste von Drittanbietern auf, während Sie die idempotency-Transaktion halten. Ein timeout nach der Bestätigung, aber vor der Rückgabe, kann ein retry verursachen; das Duplikat muss den bestätigten inbox-Schlüssel beachten und ein No-Op werden.
Empfohlene idempotency-Schlüssel
| Ereignis | Primäre Identität | Notizen |
|---|---|---|
| Zahlungssitzung | uuid + Status-/Versionsnachweis | Dasselbe invoice kann mehrere legitime Statusänderungen auslösen. |
| Teilzahlung-Aufladung | invoice uuid + txid | Mehr als eine Überweisung kann zum gleichen unterbezahlten invoice gehören. |
| Statisches Wallet-Einzahlung | Netzwerk + txid | order_id wird von jeder Einzahlung auf dieses Wallet wiederverwendet. |
| Auszahlung | Auszahlung uuid + Status | Erstelle niemals eine zweite Auszahlung aus webhook retry Logik. |
Wenn Ihr Schema keine Ereignis-ID hat, speichern Sie den verifizierten payload-Hash als zusätzliches Audit-Nachweis, ersetzen Sie jedoch nicht die oben genannten Geschäftsidentitäten durch einen Zeitstempel.
Ordnung und reconciliation
Die Zustellung erfolgt mindestens einmal und Statusnachrichten können rasant sein. Implementiere monotone Geschäftsregeln statt "letzte Anfrage gewinnt":
- Verschiebe niemals eine erfüllte Bestellung zurück auf
check, nur weil ein älteres Ereignis zu spät ankam; underpaid_checkzusätzliche TXIDs zu erhalten, ohne frühere Credits zu wiederholen;paidundoverpaidals erfolgreiche settlement-Staaten zu behandeln, während ihre unterschiedlichen Mengen erhalten bleiben;- Behalte
underpaidals ein endgültiges Teilzahlungsergebnis, es sei denn, die autoritative API meldet später einen anderen Zustand; - Routen Sie
aml_lockzur Überprüfung und lassen Sie nicht zu, dass ein generischer retry-Worker es ausfüllt; - Abfrage von Zahlungs- oder Auszahlungsinformationen immer dann, wenn der Übergang unmöglich ist, der Kontext fehlt oder finanziell unklar ist.
Führe geplante reconciliation aus, selbst wenn webhook Lieferung gesund erscheint. Vergleichen Sie Ihren lokalen Terminalzustand und den gutgeschriebenen Betrag mit /v1/payment/info, /v1/static-wallet/transactions oder /v1/payout/status/{uuid} und warnen Sie auf Unterschiede, anstatt die Ledger-Geschichte stillschweigend zu überschreiben.
Webhook Endpunktsicherheit
- HTTPS erfordern und den Rückruf öffentlich zugänglich halten; Private/Loopback-Rückrufziele werden während der Zahlungserstellung abgelehnt.
- Setzen Sie ein kleines Request-Body-Limit und einen JSON-Inhaltstyp ein.
- Rate-Limit vor teuren Arbeiten, aber genug Headroom für echte Bursts und retries lassen.
- Autorisieren Sie niemals einen webhook ausschließlich durch die Quell-IP. Netzwerk-Lowlists sind Verteidigung in Tiefe; Die HMAC-Verifizierung ist verpflichtend.
- Schwärbe
sign, API-Schlüssel, Adressen, wenn es die Richtlinie ergibt, und persönliche Metadaten aus Anwendungsprotokollen. - Halte sowohl aktuelle als auch explizit geplante Ersatztasten während eines kontrollierten Rotationsfensters verfügbar; Rate niemals, welcher Schlüssel ein Ereignis signiert hat.
- Geben Sie einen generischen Fehlerkörper bei ungültigen Signaturen zurück, damit der Endpunkt nicht zu einem Schlüssel- oder Kontoorakel wird.