Powiadomienia webhook
Otrzymuj w czasie rzeczywistym aktualizacje statusu płatności i wypłat poprzez webhooki podpisane HMAC.
System 2328.io wysyła webhook pod Twój url_callback przy każdej zmianie statusu płatności. To zalecany sposób otrzymywania powiadomień o udanych płatnościach.
Format żądania
- Method:
POST - Content-Type:
application/json - Signature: pole
signw treści żądania
Payload
Treść webhooka zachowuje format odpowiedzi /v1/payment/info i dodaje tx_explorer_url oraz pole sign używane do weryfikacji podpisu.
Płatność udana
{
"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"
}Płatność anulowana / nieudana
Gdy płatność nie jest w terminalnym stanie paid, pola txid, payment_amount oraz merchant_amount mają wartość 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"
}Opis pól
| Pole | Typ | Opis |
|---|---|---|
uuid | string | UUID płatności |
order_id | string | Twój identyfikator zamówienia |
amount | decimal (8 dp) | Kwota fiat w currency |
currency | string | Waluta fiat zażądana przez sprzedawcę |
url | string | URL hostowanego checkoutu |
expires_at | string (ISO 8601) | Moment wygaśnięcia sesji płatności |
created_at | string (ISO 8601) | Moment utworzenia sesji płatności |
payer_currency | string | Kryptowaluta, w której płaci płacący |
payer_amount | decimal (8 dp) | Oczekiwana kwota w kryptowalucie |
network | string | Sieć blockchain |
address | string | Adres depozytowy |
payment_status | string | Jedna z wartości: pending, check, paid, underpaid_check, underpaid, overpaid, cancel, aml_lock (zobacz References) |
txid | string | null | Hash transakcji blockchain, obecny tylko po potwierdzonej płatności |
tx_explorer_url | string | null | Adres URL transakcji w eksploratorze blockchain. Wartość null, gdy brakuje txid lub przelew jest wewnętrznym P2P. |
payment_amount | decimal | null | Faktycznie zapłacona kwota, obecna tylko po płatności |
merchant_amount | decimal (18 dp) | null | Kwota zaksięgowana sprzedawcy po opłatach |
amount_usd | decimal (8 dp) | Kwota w USD w chwili utworzenia |
exchange_rate | decimal | Użyty kurs wymiany krypto / fiat |
sign | string (hex) | Podpis HMAC-SHA256 payloadu |
Weryfikacja podpisu
Aby zweryfikować podpis webhooka:
- Wyciągnij pole
signz payloadu - Usuń pole
signz obiektu - Zakoduj pozostałe pola jako JSON
- Zakoduj JSON w base64
- Oblicz HMAC-SHA256 z ciągu base64 przy użyciu swojego API_KEY
- Porównaj obliczony podpis z wartością
sign, używając porównania w czasie stałym
<?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
endZawsze weryfikuj podpis przed zaksięgowaniem jakichkolwiek środków użytkownikowi. Niepodpisany lub błędnie podpisany webhook może być spreparowanym żądaniem.
Webhooki wypłat
Gdy zmienia się status wypłaty, system wysyła webhook POST pod adres url_callback przekazany w momencie utworzenia wypłaty. Jeśli url_callback nie został podany, dla tej wypłaty nie są wysyłane żadne webhooki.
Webhooki wypłat muszą być weryfikowane Twoim Payout API key — nie zwykłym kluczem API. Algorytm podpisywania jest identyczny jak dla webhooków płatności (usuń sign, zakoduj jako JSON, base64, HMAC-SHA256), różni się wyłącznie kluczem.
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"
}Opis pól
| Pole | Typ | Opis |
|---|---|---|
uuid | string | UUID wypłaty |
order_id | string | Twój identyfikator idempotentności / referencyjny, jeśli go podano |
status | string | pending, completed, failed, cancelled (zobacz References) |
currency | string | Waluta wypłaty |
network | string | Sieć blockchain |
amount | decimal | Kwota wypłaty (w currency) |
merchant_amount | decimal | Kwota pobrana z salda sprzedawcy |
network_amount | decimal | Kwota faktycznie wysłana w sieci |
amount_usd | decimal | Wartość w USD w chwili wypłaty |
to_address | string | Adres blockchain odbiorcy |
memo | string | null | Memo / tag docelowy, jeśli użyty |
txid | string | null | Hash transakcji blockchain, ustawiany przy completed |
tx_explorer_url | string | null | Adres URL transakcji w eksploratorze blockchain. Wartość null, gdy brakuje txid lub przelew jest wewnętrznym P2P. |
block_number | integer | null | Wysokość bloku transakcji on-chain |
error_type | string | null | Powód, gdy status = failed (np. aml_risk, zobacz References) |
created_at | string (ISO 8601) | Moment utworzenia wypłaty |
updated_at | string (ISO 8601) | Moment ostatniej zmiany statusu |
from_currency | string | Saldo źródłowe, z którego pobrano wypłatę przy użyciu automatycznej konwersji (np. USDT dla wypłaty w BTC) |
debited_amount | decimal | Kwota pobrana z salda from_currency |
debited_currency | string | Waluta obciążenia |
sign | string (hex) | Podpis HMAC-SHA256 payloadu, podpisany Payout API key |
Dobre praktyki
- Idempotentność — zawsze sprawdzaj, czy płatność nie została już przetworzona (po
order_idlubuuid). Webhooki mogą docierać wielokrotnie. - Szybka odpowiedź — odpowiadaj HTTP 200 możliwie najszybciej. Cięższe operacje przekaż do kolejki w tle.
- Ponawianie prób — jeśli system nie otrzyma HTTP 200, webhook jest wysyłany ponownie po 2 minutach. Maksymalnie 5 prób ponawiania.
- Przetwarzanie asynchroniczne — obsługuj zdarzenia webhook asynchronicznie, aby nie blokować odpowiedzi.
- Bezpieczeństwo — ZAWSZE weryfikuj podpis
signprzed zaufaniem payloadowi.
Webhooki mogą docierać w innej kolejności niż zdarzenia. Nie zakładaj, że pierwszy otrzymany webhook to stan końcowy — jeśli potrzebujesz pewności, zawsze ponownie pobieraj dane przez /v1/payment/info (lub /v1/payout/status/{uuid}).
Umowa dostawy i przetwarzania
Użyj następującej kolejności w swoim punkcie końcowym webhook:
- Odczytaj ciało żądania bez rejestrowania sekretów ani pełnego podpisu.
- Określ, czy jest to zdarzenie płatności/statycznego portfela, czy zdarzenie wypłaty, aby wybrać właściwy klucz API.
- Usuń
sign, odtwórz udokumentowane bajty JSON, oblicz HMAC-SHA256 i porównaj w stałym czasie. - Zweryfikuj wymagane identyfikatory, ciągi dziesiętne i wartości statusu.
- Atomowo wstaw rekord skrzynki/idempotencji. Jeśli już istnieje, zwróć HTTP 200 bez powtarzania efektów ubocznych.
- Zatwierdź mutację zamówienia/księgi i umieść w kolejce mało istotne e-maile, analizy lub powiadomienia.
- Szybko zwróć HTTP 200.
Nie wywołuj wolnych usług zewnętrznych podczas trzymania transakcji idempotencji. Przekroczenie limitu czasu po zatwierdzeniu, ale przed zwróceniem odpowiedzi, może spowodować ponowne wywołanie; duplikat musi uwzględniać zatwierdzony klucz skrzynki odbiorczej i stać się operacją neutralną.
Zalecane klucze idempotencji
| Wydarzenie | Główna tożsamość | Notatki |
|---|---|---|
| Sesja płatności | uuid + dowody statusu/wersji | Ta sama faktura może generować wiele prawidłowych zmian statusu. |
| Częściowa dopłata | faktura uuid + txid | Więcej niż jeden przelew może należeć do tej samej niedopłaconej faktury. |
| Wpłata na portfel statyczny | sieć + txid | order_id jest używany ponownie przy każdej wpłacie na ten portfel. |
| Wypłata | wypłata uuid + status | Nigdy nie twórz drugiej wypłaty w ramach logiki ponawiania webhooka. |
Jeśli w Twoim schemacie nie ma identyfikatora zdarzenia, przechowuj zweryfikowany hash payload jako dodatkowy dowód audytu, ale nie zastępuj powyższych tożsamości biznesowych znacznikiem czasowym.
Zamawianie i uzgadnianie
Dostawa odbywa się co najmniej raz, a komunikaty o stanie mogą się ścigać. Wdróż monotoniczne zasady biznesowe zamiast „wygrywa ostatnie żądanie”:
- nigdy nie przenoś zrealizowanego zamówienia z powrotem do
checkz powodu opóźnionego przybycia starszego zdarzenia; - pozwól
underpaid_checkotrzymywać dodatkowe txidy bez powtarzania wcześniejszych kredytów; - traktuj
paidioverpaidjako stany pomyślnego rozliczenia, zachowując przy tym ich różne kwoty; - utrzymuj
underpaidjako ostateczny wynik częściowej płatności, chyba że autorytatywne API później zgłosi inny stan; - przekieruj
aml_lockdo przeglądu i nie pozwól, aby ogólny pracownik odpowiedzialny za ponawianie wykonał jego realizację; - zapytaj o informacje o płatności/wypłacie, gdy przejście jest niemożliwe, brak kontekstu lub jest niejednoznaczne finansowo.
Uruchamiaj zaplanowaną rekonsyliację nawet wtedy, gdy dostarczanie webhooka wydaje się prawidłowe. Porównaj stan lokalnego terminala i zaksięgowaną kwotę z /v1/payment/info, /v1/static-wallet/transactions lub /v1/payout/status/{uuid} i zgłaszaj różnice zamiast cicho nadpisywać historię księgi.
Bezpieczeństwo punktu końcowego webhooka
- Wymagaj HTTPS i utrzymuj callback publicznie dostępnym; prywatne/loopbackowe cele callback są odrzucane podczas tworzenia płatności.
- Egzekwuj mały limit rozmiaru ciała żądania i typ treści JSON.
- Ograniczaj częstość przed kosztownymi operacjami, ale zostaw wystarczający margines na legalne szczyty i ponowne próby.
- Nigdy nie autoryzuj webhooka wyłącznie na podstawie adresu IP źródła. Listy dostępu sieci to obrona w głąb; weryfikacja HMAC jest obowiązkowa.
- Redaguj
sign, klucze API, adresy, gdy wymaga tego polityka, oraz dane osobowe z logów aplikacji. - Utrzymuj zarówno bieżące, jak i wyraźnie zaplanowane klucze zamienne dostępne podczas kontrolowanego okna rotacji; nigdy nie zgaduj, który klucz podpisał zdarzenie.
- Zwracaj ogólny komunikat o błędzie w przypadku nieprawidłowych podpisów, aby punkt końcowy nie stał się wróżbitą kluczy lub kont.