API de saques
Envie saques do saldo do seu comerciante para qualquer endereço blockchain.
A API de saques permite sacar fundos programaticamente do saldo do seu comerciante para qualquer endereço blockchain.
Para todos os endpoints de saque, você deve usar uma Payout API key separada para gerar a assinatura sign. Essa chave é diferente da sua API key comum e deve ser gerada nas configurações do projeto.
Criar saque
Cria uma solicitação de saque a partir do saldo do seu comerciante.
/v1/payoutParâmetros da requisição
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
currency | string | sim | Moeda do saque (veja References) |
network | string | sim | Código da rede (veja References) |
amount | string | sim | Valor do saque |
to_address | string | sim | Endereço blockchain do destinatário |
order_id | string | não | Chave de idempotência — única dentro de um projeto. Um POST repetido com o mesmo order_id não cria um novo saque — o existente é retornado |
url_callback | string | não | URL para webhooks do saque. Omita para desabilitar webhooks neste saque |
memo | string | null | não | Tag/memo de destino. Atualmente usado apenas pelas redes TON e SOL; máx. 255 caracteres |
from_currency | string | não | Saldo de origem a ser debitado e convertido automaticamente em currency no momento do saque. Permite que você saque em ativos voláteis (BTC, ETH, …) mantendo o saldo em uma stablecoin como USDT — você não precisa manter a cripto volátil. Passe "USDT" para debitar do saldo USDT |
fee_option | string | não | Como as taxas são cobradas. deduct (padrão) — taxas de rede + plataforma subtraídas de amount, o destinatário recebe amount - fees. add — taxas adicionadas por cima, o comerciante é debitado em amount + fees e o destinatário recebe exatamente amount |
Idempotência. Dentro de um projeto, um saque é único por order_id. Reenviar o mesmo POST com o mesmo order_id é seguro — a API retorna o saque existente em vez de criar uma duplicata. Sempre informe um order_id em saques de produção.
Exemplos de requisição
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()
}Exemplo de resposta
{
"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"
}
}Taxas. Padrão fee_option: deduct — taxas de rede + plataforma são subtraídas de amount (destinatário recebe amount - fees). Passe fee_option: add para cobrar as taxas por cima — o destinatário recebe exatamente amount e o comerciante é debitado em amount + fees.
Calcular saque
Estima os valores e taxas do saque sem criar um saque nem debitar o seu saldo. Use para mostrar ao usuário o valor exato que ele receberá (ou pagará) antes de confirmar.
/v1/payout/calcParâmetros da requisição
Idênticos a Criar saque — mesmos campos, mesma assinatura. order_id, url_callback, to_address e memo são aceitos, mas ignorados: nenhum saque é persistido e nenhum callback é enviado.
Exemplo de requisição
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"}'Exemplo de resposta
{
"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"
}
}Apenas pré-visualização. Este endpoint é somente leitura — nenhum saldo é debitado e nenhum registro de saque é criado. Chame quantas vezes precisar para exibir o detalhamento de taxas na sua interface.
Status do saque
Obtenha o status de uma solicitação de saque.
/v1/payout/status/{uuid}Parâmetros do path
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
uuid | string | sim | UUID do saque (de result.uuid na criação) |
Exemplo de resposta
{
"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 requisição GET, a assinatura é calculada a partir de um corpo vazio:
hash_hmac('sha256', base64_encode(''), $apiKey)
Campos da resposta
Campos retornados em result por POST /v1/payout e GET /v1/payout/status/{uuid}:
| Campo | Tipo | Descrição |
|---|---|---|
uuid | string | UUID do saque atribuído pelo sistema |
order_id | string | Seu identificador interno do saque (único dentro do projeto) |
status | string | Status atual do saque (veja abaixo) |
currency | string | Moeda do saque |
network | string | Código da rede |
amount | string | Valor do saque conforme solicitado |
merchant_amount | string | Valor debitado do saldo do comerciante |
network_amount | string | Valor efetivamente enviado on-chain (após taxas de rede + plataforma) |
amount_usd | string | Equivalente em USD do valor do saque |
to_address | string | Endereço blockchain do destinatário |
memo | string | null | Tag/memo de destino (TON, SOL). null caso contrário |
txid | string | null | Hash da transação na blockchain. null até que a transação seja enviada |
block_number | int | null | Número do bloco em que a transação foi incluída. null até a inclusão |
error_type | string | null | Motivo da falha quando status = failed (veja Tipos de erro abaixo). null caso contrário |
created_at | string (ISO 8601) | Horário de criação do saque |
updated_at | string (ISO 8601) | Horário da última mudança de status |
from_currency | string | null | Saldo de origem do qual o saque foi debitado quando houve conversão automática (ex.: USDT para um saque em BTC). null se não houve conversão |
debited_amount | string | null | Valor efetivamente debitado do saldo de origem após a conversão. Presente apenas quando há conversão automática |
debited_currency | string | null | Moeda de debited_amount — o saldo do qual os fundos foram debitados |
Status de saque
O campo status pode assumir os seguintes valores:
| Status | Descrição |
|---|---|
pending | Criado, aguardando processamento |
completed | Concluído com sucesso — txid está definido |
failed | Erro de envio — veja error_type |
cancelled | Cancelado |
Tipos de erro
Quando status = failed, o campo error_type descreve o motivo:
| Código | Descrição |
|---|---|
aml_risk | Saque bloqueado pelas verificações de risco AML (endereço do destinatário sinalizado como de alto risco) |
Notificações de webhook
Quando o status de um saque muda, o sistema envia um webhook POST para a URL url_callback informada na criação do saque. Se url_callback não foi informado, nenhum webhook é enviado para esse saque.
- Método:
POST - Content-Type:
application/json - Assinatura: campo
signno corpo da requisição, calculado com a Payout API key (a mesma chave usada para assinar requisições de saque).
O payload espelha o objeto result de GET /v1/payout/status/{uuid} mais um campo sign para verificação.
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"
}Verificando a assinatura. Use o mesmo algoritmo dos webhooks de pagamento, mas assine com sua Payout API key em vez da API key comum. Remova o campo sign, codifique o restante do payload em JSON, em seguida em base64 e calcule hash_hmac('sha256', $base64, $payoutApiKey), comparando com o sign recebido.