API de retrait
Envoyez des retraits depuis votre solde marchand vers n'importe quelle adresse blockchain.
L'API de retrait vous permet de retirer par programmation des fonds depuis votre solde marchand vers n'importe quelle adresse blockchain.
Pour tous les endpoints de retrait, vous devez utiliser une Payout API key distincte pour générer la signature sign. Cette clé est différente de votre API key classique et doit être générée dans les paramètres de votre projet.
Créer un retrait
Crée une demande de retrait depuis votre solde marchand.
/v1/payoutParamètres de la requête
| Champ | Type | Requis | Description |
|---|---|---|---|
currency | string | oui | Devise du retrait (voir References) |
network | string | oui | Code de réseau (voir References) |
amount | string | oui | Montant du retrait |
to_address | string | oui | Adresse blockchain du destinataire |
order_id | string | non | Clé d'idempotence — unique au sein d'un projet. Un POST répété avec le même order_id ne crée pas de nouveau retrait — le retrait existant est renvoyé à la place |
url_callback | string | non | URL pour les webhooks de retrait. Omettez ce champ pour désactiver les webhooks pour ce retrait |
memo | string | null | non | Tag de destination / mémo. Actuellement utilisé uniquement par les réseaux TON et SOL ; max. 255 caractères |
from_currency | string | non | Solde source à débiter et à convertir automatiquement en currency au moment du retrait. Vous permet de payer en actifs volatils (BTC, ETH, …) tout en gardant votre solde dans un stablecoin comme USDT — vous n'avez pas à détenir vous-même la crypto volatile. Passez "USDT" pour débiter le solde USDT |
fee_option | string | non | Mode de facturation des frais. deduct (par défaut) — frais de réseau + frais de plateforme déduits de amount, le destinataire reçoit amount - fees. add — frais ajoutés en supplément, le marchand est débité de amount + fees, le destinataire reçoit exactement amount |
Idempotence. Au sein d'un projet, un retrait est unique par order_id. Renvoyer le même POST avec le même order_id est sûr — l'API renvoie le retrait existant au lieu d'en créer un doublon. Passez toujours un order_id pour les retraits en production.
Exemples de requête
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()
}Exemple de réponse
{
"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"
}
}Frais. Par défaut, fee_option: deduct — les frais de réseau + frais de plateforme sont déduits de amount (le destinataire reçoit amount - fees). Passez fee_option: add pour facturer les frais en supplément — le destinataire reçoit exactement amount et le marchand est débité de amount + fees.
Calculer un retrait
Estime les montants et frais d'un retrait sans créer de retrait ni débiter votre solde. Utilisez-le pour afficher à l'utilisateur le montant exact qu'il recevra (ou paiera) avant qu'il confirme.
/v1/payout/calcParamètres de la requête
Identiques à Créer un retrait — mêmes champs, même signature. order_id, url_callback, to_address et memo sont acceptés mais ignorés : aucun retrait n'est enregistré et aucun callback n'est envoyé.
Exemple de requête
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"}'Exemple de réponse
{
"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"
}
}Aperçu uniquement. Ce endpoint est en lecture seule — aucun solde n'est débité et aucun enregistrement de retrait n'est créé. Appelez-le autant que nécessaire pour afficher le détail des frais dans votre interface.
Statut du retrait
Récupérez le statut d'une demande de retrait.
/v1/payout/status/{uuid}Paramètres de chemin
| Champ | Type | Requis | Description |
|---|---|---|---|
uuid | string | oui | UUID du retrait (depuis result.uuid à la création) |
Exemple de réponse
{
"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"
}
}Pour cette requête GET, la signature est calculée à partir d'un corps vide :
hash_hmac('sha256', base64_encode(''), $apiKey)
Champs de réponse
Champs renvoyés dans result par POST /v1/payout et GET /v1/payout/status/{uuid} :
| Champ | Type | Description |
|---|---|---|
uuid | string | UUID du retrait attribué par le système |
order_id | string | Votre identifiant interne de retrait (unique au sein du projet) |
status | string | Statut actuel du retrait (voir ci-dessous) |
currency | string | Devise du retrait |
network | string | Code de réseau |
amount | string | Montant du retrait demandé |
merchant_amount | string | Montant débité du solde marchand |
network_amount | string | Montant réellement envoyé on-chain (après frais de réseau + plateforme) |
amount_usd | string | Équivalent en USD du montant du retrait |
to_address | string | Adresse blockchain du destinataire |
memo | string | null | Tag de destination / mémo (TON, SOL). null sinon |
txid | string | null | Hash de la transaction blockchain. null jusqu'à l'envoi de la transaction |
block_number | int | null | Numéro du bloc dans lequel la transaction a été incluse. null tant qu'elle n'est pas incluse |
error_type | string | null | Raison de l'échec lorsque status = failed (voir Types d'erreurs ci-dessous). null sinon |
created_at | string (ISO 8601) | Date de création du retrait |
updated_at | string (ISO 8601) | Date du dernier changement de statut |
from_currency | string | null | Solde source débité pour le retrait lorsqu'une conversion automatique a été utilisée (par ex. USDT pour un retrait en BTC). null s'il n'y a pas eu de conversion |
debited_amount | string | null | Montant réellement débité du solde source après conversion. Présent uniquement lorsque la conversion automatique est utilisée |
debited_currency | string | null | Devise de debited_amount — le solde depuis lequel les fonds ont été débités |
Statuts de retrait
Le champ status peut prendre les valeurs suivantes :
| Statut | Description |
|---|---|
pending | Créé, en attente de traitement |
completed | Terminé avec succès — txid est défini |
failed | Erreur d'envoi — voir error_type |
cancelled | Annulé |
Types d'erreurs
Lorsque status = failed, le champ error_type indique la raison :
| Code | Description |
|---|---|
aml_risk | Retrait bloqué par les contrôles de risque AML (adresse du destinataire signalée comme à haut risque) |
Notifications webhook
Lorsque le statut d'un retrait change, le système envoie un webhook POST à l'URL url_callback fournie lors de la création du retrait. Si url_callback n'a pas été fourni, aucun webhook n'est envoyé pour ce retrait.
- Méthode :
POST - Content-Type :
application/json - Signature : champ
signdans le corps de la requête, calculé avec la Payout API key (la même clé utilisée pour signer les requêtes de retrait).
Le payload reflète l'objet result de GET /v1/payout/status/{uuid} plus un champ sign pour la vérification.
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"
}Vérification de la signature. Utilisez le même algorithme que pour les webhooks de paiement, mais signez avec votre Payout API key au lieu de l'API key classique. Retirez le champ sign, encodez le payload restant en JSON, puis en Base64, puis calculez hash_hmac('sha256', $base64, $payoutApiKey) et comparez avec le sign reçu.