Sign in
Introduzione/Notifiche Webhook

Notifiche Webhook

Ricevi aggiornamenti in tempo reale sullo stato di pagamenti e prelievi tramite webhook firmati con HMAC.

Il sistema 2328.io invia un webhook al tuo url_callback ogni volta che cambia lo stato di un pagamento. Questo è il modo consigliato per essere notificati dei pagamenti andati a buon fine.

Formato della richiesta

  • Method: POST
  • Content-Type: application/json
  • Signature: campo sign nel body della richiesta

Payload

Il body del webhook segue il formato della risposta di /v1/payment/info e aggiunge tx_explorer_url, oltre al campo sign usato per verificare la firma.

Pagamento andato a buon fine

JSON
{
  "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"
}

Pagamento annullato / fallito

Quando il pagamento non si trova in uno stato terminale paid, txid, payment_amount e merchant_amount sono null:

JSON
{
  "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"
}

Riferimento dei campi

CampoTipoDescrizione
uuidstringUUID del pagamento
order_idstringIl tuo ID ordine
amountdecimal (8 dp)Importo fiat in currency
currencystringValuta fiat richiesta dal merchant
urlstringURL del checkout ospitato
expires_atstring (ISO 8601)Quando scade la sessione di pagamento
created_atstring (ISO 8601)Quando è stata creata la sessione di pagamento
payer_currencystringCrypto con cui sta pagando il pagatore
payer_amountdecimal (8 dp)Importo crypto atteso
networkstringRete blockchain
addressstringIndirizzo di deposito
payment_statusstringUno tra: pending, check, paid, underpaid_check, underpaid, overpaid, cancel, aml_lock (vedi References)
txidstring | nullHash della transazione blockchain, presente solo dopo un pagamento confermato
tx_explorer_urlstring | nullURL della transazione nell’explorer blockchain. È null se txid è assente o se il trasferimento è P2P interno.
payment_amountdecimal | nullImporto effettivamente pagato, presente solo dopo il pagamento
merchant_amountdecimal (18 dp) | nullImporto accreditato al merchant dopo le commissioni
amount_usddecimal (8 dp)Importo in USD al momento della creazione
exchange_ratedecimalTasso di cambio crypto / fiat utilizzato
signstring (hex)Firma HMAC-SHA256 del payload

Verifica della firma

Per verificare la firma di un webhook:

  1. Estrai il campo sign dal payload
  2. Rimuovi il campo sign dall'oggetto
  3. Codifica i campi rimanenti in JSON
  4. Codifica il JSON in Base64
  5. Calcola HMAC-SHA256 dalla stringa Base64 utilizzando la tua API_KEY
  6. Confronta la firma calcolata con il valore di sign utilizzando un confronto a tempo costante
PHP
<?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);

Verifica sempre la firma prima di accreditare qualsiasi fondo a un utente. Un webhook non firmato o firmato in modo errato potrebbe essere una richiesta contraffatta.

Webhook dei prelievi

Quando lo status di un prelievo cambia, il sistema invia un webhook POST all'URL url_callback passato al momento della creazione del prelievo. Se url_callback non è stato fornito, non viene inviato alcun webhook per quel prelievo.

I webhook dei prelievi devono essere verificati con la tua Payout API key — non con l'API key normale. L'algoritmo di firma è identico a quello dei webhook di pagamento (rimuovi sign, codifica in JSON, base64, HMAC-SHA256), cambia solo la chiave.

Payload

JSON
{
  "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"
}

Riferimento dei campi

CampoTipoDescrizione
uuidstringUUID del prelievo
order_idstringIl tuo ID di idempotenza / riferimento, se ne hai fornito uno
statusstringpending, completed, failed, cancelled (vedi References)
currencystringValuta del prelievo
networkstringRete blockchain
amountdecimalImporto del prelievo (in currency)
merchant_amountdecimalImporto addebitato dal saldo merchant
network_amountdecimalImporto effettivamente inviato on-chain
amount_usddecimalValore in USD al momento del prelievo
to_addressstringIndirizzo blockchain del destinatario
memostring | nullMemo / destination tag, se utilizzato
txidstring | nullHash della transazione blockchain, valorizzato a completed
tx_explorer_urlstring | nullURL della transazione nell’explorer blockchain. È null se txid è assente o se il trasferimento è P2P interno.
block_numberinteger | nullAltezza del blocco della transazione on-chain
error_typestring | nullMotivo quando status = failed (es. aml_risk, vedi References)
created_atstring (ISO 8601)Quando è stato creato il prelievo
updated_atstring (ISO 8601)Quando è cambiato per l'ultima volta lo stato
from_currencystringSaldo di origine da cui è stato addebitato il prelievo quando è stata usata la conversione automatica (es. USDT per un prelievo in BTC)
debited_amountdecimalImporto addebitato dal saldo from_currency
debited_currencystringValuta dell'addebito
signstring (hex)Firma HMAC-SHA256 del payload, firmata con la Payout API key

Best practice

  • Idempotenza — Verifica sempre se il pagamento è già stato elaborato (tramite order_id o uuid). I webhook possono arrivare più volte.
  • Risposta rapida — Restituisci HTTP 200 il più rapidamente possibile. Delega il lavoro pesante a una coda in background.
  • Retry — Se il sistema non riceve un HTTP 200, il webhook viene rinviato dopo 2 minuti. Massimo 5 tentativi di retry.
  • Elaborazione asincrona — Gestisci gli eventi webhook in modo asincrono per evitare di bloccare la risposta.
  • Sicurezza — Verifica SEMPRE la firma sign prima di fidarti del payload.

I webhook possono arrivare in ordine non sequenziale. Non dare per scontato che il primo webhook ricevuto sia lo stato finale — recupera sempre nuovamente tramite /v1/payment/info (o /v1/payout/status/{uuid}) se hai bisogno di certezza.

Contratto di consegna e elaborazione

Usa il seguente ordine all'interno del tuo endpoint webhook:

  1. Leggi il corpo della richiesta senza registrare segreti o la firma completa.
  2. Identifica se si tratta di un evento di pagamento/wallet statico o di un evento di pagamento in uscita, in modo da selezionare la chiave API corretta.
  3. Rimuovi sign, riproduci i byte JSON documentati, calcola HMAC-SHA256 e confrontalo in tempo costante.
  4. Valida gli identificatori richiesti, le stringhe decimali e i valori di stato.
  5. Inserisci in modo atomico un record inbox/idempotency. Se esiste già, restituisci HTTP 200 senza ripetere effetti collaterali.
  6. Confermare la mutazione dell'ordine/libro mastro e mettere in coda le email, le analisi o le notifiche non critiche.
  7. Restituire rapidamente HTTP 200.

Non chiamare servizi di terze parti lenti mentre si tiene la transazione di idempotenza. Un timeout dopo aver confermato ma prima di restituire può causare un nuovo tentativo; il duplicato deve osservare la chiave inbox confermata e diventare un'operazione nulla.

EventoIdentità primariaNote
Sessione di pagamentouuid + evidenza di stato/versioneLa stessa fattura può emettere più cambiamenti di stato legittimi.
Ricarica a pagamento parzialefattura uuid + txidPiù di un bonifico può appartenere alla stessa fattura sotto-pagata.
Deposito in portafoglio staticorete + txidorder_id viene riutilizzato per ogni deposito in quel portafoglio.
Pagamentopagamento uuid + statoNon creare mai un secondo pagamento dalla logica di ripetizione del webhook.

Se il tuo schema non ha un id evento, memorizza l'hash del payload verificato come ulteriore prova di audit, ma non sostituire le identità aziendali sopra con un timestamp.

Ordinamento e riconciliazione

La consegna è almeno una volta e i messaggi di stato possono arrivare in ordine casuale. Implementare regole aziendali monotone piuttosto che “vince l’ultima richiesta”:

  • non spostare mai un ordine evaso indietro a check perché un evento precedente è arrivato in ritardo;
  • consentire a underpaid_check di ricevere txid aggiuntivi senza ripetere accrediti precedenti;
  • trattare paid e overpaid come stati di regolamento riusciti, pur mantenendo i loro importi differenti;
  • mantenere underpaid come risultato finale di pagamento parziale a meno che l’API autorevole non riporti successivamente un altro stato;
  • indirizzare aml_lock a revisione e non consentire a un generico lavoratore di ripetizione di evaderlo;
  • interroga le informazioni di pagamento/pagamento ogni volta che la transizione è impossibile, manca il contesto o è finanziariamente ambigua.

Esegui la riconciliazione programmata anche quando la consegna del webhook sembra funzionare correttamente. Confronta lo stato del tuo terminale locale e l'importo accreditato con /v1/payment/info, /v1/static-wallet/transactions o /v1/payout/status/{uuid} e segnala le differenze invece di sovrascrivere silenziosamente la cronologia del libro contabile.

Sicurezza del endpoint webhook

  • Richiedi HTTPS e mantieni il callback accessibile pubblicamente; i target di callback privati/loopback vengono rifiutati durante la creazione del pagamento.
  • Applica un piccolo limite alla dimensione del corpo della richiesta e tipo di contenuto JSON.
  • Limita la frequenza prima di lavori costosi, ma lascia spazio sufficiente per picchi legittimi e tentativi di ripetizione.
  • Non autorizzare mai un webhook solo in base all'indirizzo IP di origine. Le liste di permessi di rete sono una difesa in profondità; la verifica HMAC è obbligatoria.
  • Censura sign, chiavi API, indirizzi quando richiesto dalla policy e metadati personali dai log dell'applicazione.
  • Tieni disponibili sia le chiavi correnti che quelle di sostituzione programmate esplicitamente durante una finestra di rotazione controllata; non indovinare mai quale chiave ha firmato un evento.
  • Restituisci un corpo di errore generico in caso di firme non valide in modo che l'endpoint non diventi un oracolo di chiavi o account.