API de retiros
Envía retiros desde tu saldo de comerciante a cualquier dirección blockchain.
La API de retiros te permite retirar fondos mediante programación desde tu saldo de comerciante hacia cualquier dirección blockchain.
Para todos los endpoints de retiro debes utilizar una Payout API key independiente para generar la firma sign. Esta clave es distinta de tu API key habitual y debe generarse en la configuración de tu proyecto.
Crear retiro
Crea una solicitud de retiro desde tu saldo de comerciante.
/v1/payoutParámetros de la solicitud
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
currency | string | sí | Divisa de retiro (consulta References) |
network | string | sí | Código de red (consulta References) |
amount | string | sí | Monto del retiro |
to_address | string | sí | Dirección blockchain del destinatario |
order_id | string | no | Clave de idempotencia — única dentro de un proyecto. Un POST repetido con el mismo order_id no crea un nuevo retiro — se devuelve el existente |
url_callback | string | no | URL para los webhooks de retiro. Omítela para deshabilitar webhooks de este retiro |
memo | string | null | no | Destination tag / memo. Actualmente lo usan únicamente las redes TON y SOL; máx. 255 caracteres |
from_currency | string | no | Saldo de origen del que debitar y convertir automáticamente a currency en el momento del retiro. Te permite pagar en activos volátiles (BTC, ETH, …) manteniendo tu saldo en una stablecoin como USDT — no tienes que mantener tú mismo la cripto volátil. Pasa "USDT" para debitar el saldo USDT |
fee_option | string | no | Cómo se cobran las comisiones. deduct (por defecto) — las comisiones de red + plataforma se restan de amount, el destinatario recibe amount - fees. add — las comisiones se añaden por encima, al comerciante se le debita amount + fees y el destinatario recibe exactamente amount |
Idempotencia. Dentro de un proyecto, un retiro es único por order_id. Reenviar el mismo POST con el mismo order_id es seguro — la API devuelve el retiro existente en lugar de crear un duplicado. Pasa siempre un order_id para los retiros en producción.
Ejemplos de solicitud
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()
}Ejemplo de respuesta
{
"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"
}
}Comisiones. Por defecto fee_option: deduct — las comisiones de red + plataforma se restan de amount (el destinatario recibe amount - fees). Pasa fee_option: add para cobrar las comisiones por encima — el destinatario recibe exactamente amount y al comerciante se le debita amount + fees.
Calcular retiro
Estima los importes y comisiones del retiro sin crear un retiro ni cargar tu saldo. Úsalo para mostrar al usuario el importe exacto que recibirá (o pagará) antes de confirmar.
/v1/payout/calcParámetros de la solicitud
Idénticos a Crear retiro — mismos campos, misma firma. order_id, url_callback, to_address y memo se aceptan pero se ignoran: no se persiste ningún retiro y no se envían callbacks.
Ejemplo de solicitud
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"}'Ejemplo de respuesta
{
"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 vista previa. Este endpoint es de solo lectura — no se carga ningún saldo ni se crea ningún registro de retiro. Llámalo todas las veces que necesites para mostrar el desglose de comisiones en tu interfaz.
Estado del retiro
Obtén el estado de una solicitud de retiro.
/v1/payout/status/{uuid}Parámetros de ruta
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
uuid | string | sí | UUID del retiro (de result.uuid al crearlo) |
Ejemplo de respuesta
{
"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"
}
}Para esta solicitud GET la firma se calcula sobre un cuerpo vacío:
hash_hmac('sha256', base64_encode(''), $apiKey)
Campos de la respuesta
Campos devueltos en result por POST /v1/payout y GET /v1/payout/status/{uuid}:
| Campo | Tipo | Descripción |
|---|---|---|
uuid | string | UUID del retiro asignado por el sistema |
order_id | string | Tu identificador interno de retiro (único dentro del proyecto) |
status | string | Estado actual del retiro (ver más abajo) |
currency | string | Divisa de retiro |
network | string | Código de red |
amount | string | Monto de retiro tal como se solicitó |
merchant_amount | string | Monto debitado del saldo del comerciante |
network_amount | string | Monto realmente enviado en la blockchain (después de las comisiones de red + plataforma) |
amount_usd | string | Equivalente en USD del monto de retiro |
to_address | string | Dirección blockchain del destinatario |
memo | string | null | Destination tag / memo (TON, SOL). null en caso contrario |
txid | string | null | Hash de la transacción en la blockchain. null hasta que se envíe la transacción |
block_number | int | null | Número de bloque donde se incluyó la transacción. null hasta que se incluya |
error_type | string | null | Motivo del fallo cuando status = failed (ver Tipos de error más abajo). null en caso contrario |
created_at | string (ISO 8601) | Hora de creación del retiro |
updated_at | string (ISO 8601) | Hora del último cambio de estado |
from_currency | string | null | Saldo de origen del que se debitó el retiro cuando se usó conversión automática (p. ej., USDT para un retiro en BTC). null si no hubo conversión |
debited_amount | string | null | Monto realmente debitado del saldo de origen tras la conversión. Solo presente cuando se usa conversión automática |
debited_currency | string | null | Divisa de debited_amount — el saldo del que se debitaron los fondos |
Estados de retiro
El campo status puede tomar los siguientes valores:
| Estado | Descripción |
|---|---|
pending | Creado, pendiente de procesamiento |
completed | Completado correctamente — txid está definido |
failed | Error de envío — consulta error_type |
cancelled | Cancelado |
Tipos de error
Cuando status = failed, el campo error_type describe la causa:
| Código | Descripción |
|---|---|
aml_risk | Retiro bloqueado por verificaciones de riesgo AML (la dirección destinataria fue marcada como de alto riesgo) |
Notificaciones por webhook
Cuando cambia el estado de un retiro, el sistema envía un webhook POST a la URL url_callback indicada al crear el retiro. Si no se proporcionó url_callback, no se envían webhooks para ese retiro.
- Método:
POST - Content-Type:
application/json - Firma: campo
signen el cuerpo de la solicitud, calculado con la Payout API key (la misma clave usada para firmar las solicitudes de retiro).
El payload refleja el objeto result de GET /v1/payout/status/{uuid} más un campo sign para la verificación.
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"
}Verificar la firma. Usa el mismo algoritmo que para los webhooks de pago, pero firma con tu Payout API key en lugar de la API key habitual. Quita el campo sign, codifica en JSON el resto del payload, codifícalo en Base64 y luego calcula hash_hmac('sha256', $base64, $payoutApiKey) y compáralo con el sign recibido.