API Prelievi
Invia prelievi dal saldo merchant a qualsiasi indirizzo blockchain.
L'API Prelievi consente di prelevare in modo programmatico fondi dal saldo merchant verso qualsiasi indirizzo blockchain.
Per tutti gli endpoint Prelievi, devi utilizzare una Payout API key separata per generare la firma sign. Questa chiave è diversa dalla tua API key normale e deve essere generata nelle impostazioni del progetto.
Creare un prelievo
Crea una richiesta di prelievo dal saldo merchant.
/v1/payoutParametri della richiesta
| Campo | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
currency | string | sì | Valuta del prelievo (vedi References) |
network | string | sì | Codice di rete (vedi References) |
amount | string | sì | Importo del prelievo |
to_address | string | sì | Indirizzo blockchain del destinatario |
order_id | string | no | Chiave di idempotenza — univoca all'interno di un progetto. Un POST ripetuto con lo stesso order_id non crea un nuovo prelievo — viene restituito quello esistente |
url_callback | string | no | URL per i webhook di prelievo. Ometti per disabilitare i webhook per questo prelievo |
memo | string | null | no | Destination tag / memo. Attualmente utilizzato solo dalle reti TON e SOL; max 255 caratteri |
from_currency | string | no | Saldo di origine da addebitare e convertire automaticamente in currency al momento del prelievo. Ti permette di prelevare in asset volatili (BTC, ETH, …) mantenendo il saldo in una stablecoin come USDT — non devi detenere tu stesso la cripto volatile. Passa "USDT" per addebitare il saldo USDT |
fee_option | string | no | Modalità di addebito delle commissioni. deduct (default) — commissioni di rete + piattaforma sottratte da amount, il destinatario riceve amount - fees. add — commissioni aggiunte sopra, il merchant viene addebitato di amount + fees, il destinatario riceve esattamente amount |
Idempotenza. All'interno di un progetto, un prelievo è univoco per order_id. Reinviare lo stesso POST con lo stesso order_id è sicuro — l'API restituisce il prelievo esistente invece di crearne un duplicato. Passa sempre un order_id per i prelievi in produzione.
Esempi di richiesta
curl -X POST https://api.2328.io/api/v1/payout \
-H "Content-Type: application/json" \
-H "User-Agent: MyShop/1.0 (+https://myshop.example)" \
-H "project: YOUR_PROJECT_UUID" \
-H "sign: YOUR_HMAC_SIGNATURE" \
-d '{"currency":"TRX","network":"TRX-TRC20","amount":"1.00","to_address":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t","order_id":"9ed25264-8be4-439f-acf5-2a8732538d27","url_callback":"https://your-site.com/webhook/payout","memo":null,"fee_option":"deduct"}'<?php
function apiSign(string $body, string $apiKey): string {
return hash_hmac('sha256', base64_encode($body), $apiKey);
}
$project = 'YOUR_PROJECT_UUID';
$apiKey = 'YOUR_PAYOUT_API_KEY';
$data = [
'currency' => 'TRX',
'network' => 'TRX-TRC20',
'amount' => '1.00',
'to_address' => 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
'order_id' => '9ed25264-8be4-439f-acf5-2a8732538d27',
'url_callback' => 'https://your-site.com/webhook/payout',
'memo' => null,
'fee_option' => 'deduct',
];
$body = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$sign = apiSign($body, $apiKey);
$ch = curl_init('https://api.2328.io/api/v1/payout');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'User-Agent: MyShop/1.0 (+https://myshop.example)',
"project: $project",
"sign: $sign",
],
]);
$response = json_decode(curl_exec($ch), true);import { createHmac } from "crypto";
function apiSign(body, apiKey) {
const base64 = Buffer.from(body, "utf8").toString("base64");
return createHmac("sha256", apiKey).update(base64).digest("hex");
}
const PROJECT_UUID = "YOUR_PROJECT_UUID";
const PAYOUT_API_KEY = process.env.PAYOUT_API_KEY;
const data = {
currency: "TRX",
network: "TRX-TRC20",
amount: "1.00",
to_address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
order_id: "9ed25264-8be4-439f-acf5-2a8732538d27",
url_callback: "https://your-site.com/webhook/payout",
memo: null,
fee_option: "deduct",
};
const body = JSON.stringify(data);
const sign = apiSign(body, PAYOUT_API_KEY);
const res = await fetch("https://api.2328.io/api/v1/payout", {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "MyShop/1.0 (+https://myshop.example)",
project: PROJECT_UUID,
sign,
},
body,
});
const json = await res.json();import json
import hmac
import hashlib
import base64
import httpx
def api_sign(body: str, api_key: str) -> str:
b64 = base64.b64encode(body.encode("utf-8")).decode()
return hmac.new(api_key.encode(), b64.encode(), hashlib.sha256).hexdigest()
PROJECT_UUID = "YOUR_PROJECT_UUID"
PAYOUT_API_KEY = "YOUR_PAYOUT_API_KEY"
data = {
"currency": "TRX",
"network": "TRX-TRC20",
"amount": "1.00",
"to_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"order_id": "9ed25264-8be4-439f-acf5-2a8732538d27",
"url_callback": "https://your-site.com/webhook/payout",
"memo": None,
"fee_option": "deduct",
}
body = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
sign = api_sign(body, PAYOUT_API_KEY)
r = httpx.post(
"https://api.2328.io/api/v1/payout",
headers={
"Content-Type": "application/json",
"User-Agent": "MyShop/1.0 (+https://myshop.example)",
"project": PROJECT_UUID,
"sign": sign,
},
content=body.encode("utf-8"),
)
response = r.json()package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"net/http"
)
func apiSign(body []byte, apiKey string) string {
b64 := base64.StdEncoding.EncodeToString(body)
h := hmac.New(sha256.New, []byte(apiKey))
h.Write([]byte(b64))
return hex.EncodeToString(h.Sum(nil))
}
func marshalCanonical(v any) ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
return nil, err
}
return bytes.TrimRight(buf.Bytes(), "\n"), nil
}
type CreatePayout struct {
Currency string `json:"currency"`
Network string `json:"network"`
Amount string `json:"amount"`
ToAddress string `json:"to_address"`
OrderID string `json:"order_id"`
URLCallback string `json:"url_callback"`
Memo *string `json:"memo"`
FeeOption string `json:"fee_option"`
}
func main() {
const projectUUID = "YOUR_PROJECT_UUID"
const payoutAPIKey = "YOUR_PAYOUT_API_KEY"
data := CreatePayout{
Currency: "TRX",
Network: "TRX-TRC20",
Amount: "1.00",
ToAddress: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
OrderID: "9ed25264-8be4-439f-acf5-2a8732538d27",
URLCallback: "https://your-site.com/webhook/payout",
Memo: nil,
FeeOption: "deduct",
}
body, err := marshalCanonical(data)
if err != nil {
panic(err)
}
sign := apiSign(body, payoutAPIKey)
req, _ := http.NewRequest("POST",
"https://api.2328.io/api/v1/payout",
bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "MyShop/1.0 (+https://myshop.example)")
req.Header.Set("project", projectUUID)
req.Header.Set("sign", sign)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
}Esempio di risposta
{
"state": 0,
"result": {
"uuid": "019dea62-1727-72aa-ac2c-eaf2ade193ef",
"order_id": "9ed25264-8be4-439f-acf5-2a8732538d27",
"status": "pending",
"currency": "TRX",
"network": "TRX-TRC20",
"amount": "1.00",
"merchant_amount": "1",
"network_amount": "0.89",
"amount_usd": "0.33",
"to_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"memo": null,
"txid": null,
"block_number": null,
"error_type": null,
"created_at": "2026-05-02T23:29:50+03:00",
"updated_at": "2026-05-02T23:29:50+03:00"
}
}Commissioni. Default fee_option: deduct — le commissioni di rete + piattaforma vengono sottratte da amount (il destinatario riceve amount - fees). Passa fee_option: add per addebitare le commissioni sopra — il destinatario riceve esattamente amount e il merchant viene addebitato di amount + fees.
Calcolare un prelievo
Stima importi e commissioni del prelievo senza creare un prelievo né addebitare il saldo. Usalo per mostrare all'utente l'importo esatto che riceverà (o pagherà) prima della conferma.
/v1/payout/calcParametri della richiesta
Identici a Creare un prelievo — stessi campi, stessa firma. order_id, url_callback, to_address e memo sono accettati ma ignorati: nessun prelievo viene salvato e nessun callback viene inviato.
Esempio di richiesta
curl -X POST https://api.2328.io/api/v1/payout/calc \
-H "Content-Type: application/json" \
-H "User-Agent: MyShop/1.0 (+https://myshop.example)" \
-H "project: YOUR_PROJECT_UUID" \
-H "sign: YOUR_HMAC_SIGNATURE" \
-d '{"currency":"USDT","network":"TRX-TRC20","amount":"100","fee_option":"add"}'Esempio di risposta
{
"state": 0,
"result": {
"currency": "USDT",
"network": "TRX-TRC20",
"amount": "100",
"fee_option": "add",
"merchant_amount": "103.00000000",
"network_amount": "100",
"total_fee": "3.00000000",
"total_fee_usd": "3.00000000"
}
}Solo anteprima. Questo endpoint è in sola lettura — nessun saldo viene addebitato e nessun record di prelievo viene creato. Chiamalo tutte le volte che ti servono per mostrare il dettaglio delle commissioni nella tua UI.
Stato del prelievo
Ottieni lo stato di una richiesta di prelievo.
/v1/payout/status/{uuid}Parametri di percorso
| Campo | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
uuid | string | sì | UUID del prelievo (da result.uuid alla creazione) |
Esempio di risposta
{
"state": 0,
"result": {
"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"
}
}Per questa richiesta GET la firma viene calcolata da un body vuoto:
hash_hmac('sha256', base64_encode(''), $apiKey)
Campi della risposta
Campi restituiti in result da POST /v1/payout e GET /v1/payout/status/{uuid}:
| Campo | Tipo | Descrizione |
|---|---|---|
uuid | string | UUID del prelievo assegnato dal sistema |
order_id | string | Il tuo identificatore interno del prelievo (univoco all'interno del progetto) |
status | string | Stato corrente del prelievo (vedi sotto) |
currency | string | Valuta del prelievo |
network | string | Codice di rete |
amount | string | Importo del prelievo come richiesto |
merchant_amount | string | Importo addebitato dal saldo merchant |
network_amount | string | Importo effettivamente inviato on-chain (dopo commissioni di rete + piattaforma) |
amount_usd | string | Equivalente in USD dell'importo del prelievo |
to_address | string | Indirizzo blockchain del destinatario |
memo | string | null | Destination tag / memo (TON, SOL). null altrimenti |
txid | string | null | Hash della transazione blockchain. null finché la transazione non viene inviata |
block_number | int | null | Numero del blocco in cui è inclusa la transazione. null finché non è inclusa |
error_type | string | null | Motivo del fallimento quando status = failed (vedi Tipi di errore sotto). null altrimenti |
created_at | string (ISO 8601) | Ora di creazione del prelievo |
updated_at | string (ISO 8601) | Ora dell'ultimo cambio di stato |
from_currency | string | null | Saldo di origine da cui è stato addebitato il prelievo quando è stata usata la conversione automatica (es. USDT per un prelievo in BTC). null se non c'è stata alcuna conversione |
debited_amount | string | null | Importo effettivamente addebitato dal saldo di origine dopo la conversione. Presente solo quando viene utilizzata la conversione automatica |
debited_currency | string | null | Valuta di debited_amount — il saldo da cui sono stati addebitati i fondi |
Stati del prelievo
Il campo status può assumere i seguenti valori:
| Stato | Descrizione |
|---|---|
pending | Creato, in attesa di elaborazione |
completed | Completato con successo — txid è impostato |
failed | Errore di invio — vedi error_type |
cancelled | Annullato |
Tipi di errore
Quando status = failed, il campo error_type ne descrive il motivo:
| Codice | Descrizione |
|---|---|
aml_risk | Prelievo bloccato dai controlli di rischio AML (l'indirizzo del destinatario è stato segnalato come ad alto rischio) |
Notifiche webhook
Quando lo stato 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.
- Method:
POST - Content-Type:
application/json - Signature: campo
signnel body della richiesta, calcolato con la Payout API key (la stessa chiave utilizzata per firmare le richieste di prelievo).
Il payload rispecchia l'oggetto result di GET /v1/payout/status/{uuid} più un campo sign per la verifica.
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"
}Verifica della firma. Utilizza lo stesso algoritmo dei webhook di pagamento, ma firma con la tua Payout API key invece dell'API key normale. Rimuovi il campo sign, codifica in JSON il payload rimanente, codificalo in Base64, quindi calcola hash_hmac('sha256', $base64, $payoutApiKey) e confronta con il sign ricevuto.