Payout API
Send withdrawals from your merchant balance to any blockchain address.
The Payout API lets you programmatically withdraw funds from your merchant balance to any blockchain address.
For all Payout endpoints, you must use a separate Payout API key to generate the sign signature. This key is different from your regular API key and must be generated in your project settings.
Create payout
Creates a withdrawal request from your merchant balance.
/v1/payoutRequest parameters
| Field | Type | Required | Description |
|---|---|---|---|
currency | string | yes | Withdrawal currency (see References) |
network | string | yes | Network code (see References) |
amount | string | yes | Withdrawal amount |
to_address | string | yes | Recipient blockchain address |
order_id | string | no | Idempotency key — unique within a project. A repeated POST with the same order_id does not create a new payout — the existing one is returned instead |
url_callback | string | no | URL for payout webhooks. Omit to disable webhooks for this payout |
memo | string | null | no | Destination tag / memo. Currently used only by TON and SOL networks; max 255 chars |
from_currency | string | no | Source balance to debit and auto-convert into currency at the moment of payout. Lets you pay out in volatile assets (BTC, ETH, …) while keeping your balance in a stable coin like USDT — you don't have to hold the volatile crypto yourself. Pass "USDT" to debit the USDT balance |
fee_option | string | no | How fees are charged. deduct (default) — network + platform fees subtracted from amount, the recipient gets amount - fees. add — fees added on top, the merchant is debited amount + fees, the recipient receives exactly amount |
Idempotency. Within a project, a payout is unique by order_id. Re-sending the same POST with the same order_id is safe — the API returns the existing payout instead of creating a duplicate. Always pass an order_id for production payouts.
Request examples
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()
}Response example
{
"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"
}
}Fees. Default fee_option: deduct — network + platform fees are subtracted from amount (recipient gets amount - fees). Pass fee_option: add to charge fees on top — the recipient gets exactly amount and the merchant is debited amount + fees.
Calculate payout
Estimates withdrawal amounts and fees without creating a payout or debiting your balance. Use it to show users the exact amount they will receive (or pay) before they confirm.
/v1/payout/calcRequest parameters
Identical to Create payout — same fields, same signing. order_id, url_callback, to_address and memo are accepted but ignored: no payout is persisted and no callbacks are sent.
Request example
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"}'Response example
{
"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"
}
}Preview only. This endpoint is read-only — no balance is debited and no payout record is created. Call it as often as you need to render fee breakdowns in your UI.
Payout status
Get the status of a payout request.
/v1/payout/status/{uuid}Path parameters
| Field | Type | Required | Description |
|---|---|---|---|
uuid | string | yes | Payout UUID (from result.uuid on creation) |
Response example
{
"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"
}
}For this GET request the signature is computed from an empty body:
hash_hmac('sha256', base64_encode(''), $apiKey)
Response fields
Fields returned in result from POST /v1/payout and GET /v1/payout/status/{uuid}:
| Field | Type | Description |
|---|---|---|
uuid | string | Payout UUID assigned by the system |
order_id | string | Your internal payout identifier (unique within the project) |
status | string | Current payout status (see below) |
currency | string | Withdrawal currency |
network | string | Network code |
amount | string | Withdrawal amount as requested |
merchant_amount | string | Amount debited from the merchant balance |
network_amount | string | Amount actually sent on-chain (after network + platform fees) |
amount_usd | string | USD equivalent of the withdrawal amount |
to_address | string | Recipient blockchain address |
memo | string | null | Destination tag / memo (TON, SOL). null otherwise |
txid | string | null | Blockchain transaction hash. null until the transaction is sent |
block_number | int | null | Block number where the transaction was included. null until included |
error_type | string | null | Reason for failure when status = failed (see Error types below). null otherwise |
created_at | string (ISO 8601) | Payout creation time |
updated_at | string (ISO 8601) | Last status change time |
from_currency | string | null | Source balance the payout was debited from when auto-conversion was used (e.g. USDT for a BTC payout). null if no conversion happened |
debited_amount | string | null | Amount actually debited from the source balance after conversion. Present only when auto-conversion is used |
debited_currency | string | null | Currency of debited_amount — the balance from which funds were debited |
Payout statuses
The status field can take the following values:
| Status | Description |
|---|---|
pending | Created, awaiting processing |
completed | Completed successfully — txid is set |
failed | Sending error — see error_type |
cancelled | Cancelled |
Error types
When status = failed, the error_type field describes why:
| Code | Description |
|---|---|
aml_risk | Payout blocked by AML risk checks (recipient address flagged as high-risk) |
Webhook notifications
When a payout's status changes, the system sends a POST webhook to the url_callback URL passed when the payout was created. If url_callback was not provided, no webhooks are sent for that payout.
- Method:
POST - Content-Type:
application/json - Signature:
signfield in the request body, computed with the Payout API key (the same key used to sign payout requests).
The payload mirrors the result object from GET /v1/payout/status/{uuid} plus a sign field for verification.
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"
}Verifying the signature. Use the same algorithm as for payment webhooks, but sign with your Payout API key instead of the regular API key. Strip the sign field, JSON-encode the remaining payload, Base64-encode it, then compute hash_hmac('sha256', $base64, $payoutApiKey) and compare with the received sign.