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.

POST/v1/payout

Request parameters

FieldTypeRequiredDescription
currencystringyesWithdrawal currency (see References)
networkstringyesNetwork code (see References)
amountstringyesWithdrawal amount
to_addressstringyesRecipient blockchain address
order_idstringnoIdempotency 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
descriptionstring | nullnoOptional description, up to 500 characters; e.g. #client1. Transaction description.
url_callbackstringnoURL for payout webhooks. Omit to disable webhooks for this payout
memostring | nullnoDestination tag / memo. Currently used only by TON and SOL networks; max 255 chars
from_currencystringnoSource 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_optionstringnoHow 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

bash
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","description":"#client1","url_callback":"https://your-site.com/webhook/payout","memo":null,"fee_option":"deduct"}'

Response example

json
{
  "state": 0,
  "result": {
    "uuid": "019dea62-1727-72aa-ac2c-eaf2ade193ef",
    "order_id": "9ed25264-8be4-439f-acf5-2a8732538d27",
    "description": "#client1",
    "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,
    "transfer_type": null,
    "txid": null,
    "tx_explorer_url": 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"
  }
}

description — Stored description or null, returned by the authenticated endpoints below. Transaction description.

POST /v1/payout; POST /v1/payout/info; GET /v1/payout/status/{uuid}.

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.

POST/v1/payout/calc

Request parameters

Identical to Create payout — same fields, same signing. description, order_id, url_callback, to_address and memo are accepted but ignored: no payout is persisted and no callbacks are sent.

description is ignored by /v1/payout/calc: it does not affect fees and is neither stored nor returned.

Request example

bash
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

json
{
  "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 information

Get current payout information by the system uuid or your order_id.

POST/v1/payout/info
  • uuid — payout UUID returned in result.uuid when the payout was created.
  • order_id — your payout identifier supplied during creation.

At least one of uuid or order_id is required. If both are supplied, both must identify the same payout in the authenticated project.

bash
curl -X POST https://api.2328.io/api/v1/payout/info \
  -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 '{"order_id":"4dfdcc84402b1185b71cbe399321533e"}'

Calculate sign from the exact JSON request body using the Payout API key. The response uses the same result schema as the status endpoint below.

Payout status

Get the status of a payout request.

GET/v1/payout/status/{uuid}

Path parameters

FieldTypeRequiredDescription
uuidstringyesPayout UUID (from result.uuid on creation)

Response example

json
{
  "state": 0,
  "result": {
    "uuid": "019dff1f-0dbd-7277-8d45-271e7775388f",
    "order_id": "4dfdcc84402b1185b71cbe399321533e",
    "description": "#client1",
    "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,
    "transfer_type": "blockchain",
    "txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
    "tx_explorer_url": "https://tronscan.org/#/transaction/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",
    "confirmations": {
      "current": 20,
      "required": 20
    },
    "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, POST /v1/payout/info and GET /v1/payout/status/{uuid}:

FieldTypeDescription
uuidstringPayout UUID assigned by the system
order_idstringYour internal payout identifier (unique within the project)
descriptionstring | nullOptional description, up to 500 characters; e.g. #client1. Transaction description.
statusstringCurrent payout status (see below)
currencystringWithdrawal currency
networkstringNetwork code
amountstringWithdrawal amount as requested
merchant_amountstringAmount debited from the merchant balance
confirmationsobject (optional)Confirmation progress: current is the observed count and required is the target. Omitted when unavailable.
network_amountstringAmount delivered to the recipient after fees, including internal P2P transfers.
amount_usdstringUSD equivalent of the withdrawal amount
to_addressstringRecipient blockchain address
memostring | nullDestination tag / memo (TON, SOL). null otherwise
transfer_typestring | nullinternal — settled inside 2328 without a blockchain transaction; blockchain — a blockchain transaction exists; null — no transaction has been recorded yet, so the transfer type is unknown.
txidstring | nullBlockchain transaction hash or the UUID of an internal P2P transfer. null until a transaction is recorded.
tx_explorer_urlstring | nullBlockchain explorer transaction URL. null when txid is absent or the transfer is internal P2P.
block_numberint | nullBlock number where the transaction was included. null until included
error_typestring | nullReason for failure when status = failed (see Error types below). null otherwise
created_atstring (ISO 8601)Payout creation time
updated_atstring (ISO 8601)Last status change time
from_currencystring | nullSource balance the payout was debited from when auto-conversion was used (e.g. USDT for a BTC payout). null if no conversion happened
debited_amountstring | nullAmount actually debited from the source balance after conversion. Present only when auto-conversion is used
debited_currencystring | nullCurrency of debited_amount — the balance from which funds were debited

Internal transfers (P2P)

Selected fields of a payout completed inside 2328 without a blockchain transaction:

json
{
  "type": "payout",
  "transfer_type": "internal",
  "status": "completed",
  "txid": "12450ae7-6023-4248-980f-3f54ca7efc55",
  "tx_explorer_url": null,
  "block_number": null
}

Identify internal transfers by transfer_type, not by the shape of txid. For blockchain, check status to determine whether the payout is completed. transfer_type is included in the webhook signature: remove only sign when verifying it.

php
$isInternal = ($data['transfer_type'] ?? null) === 'internal';

Payout statuses

The status field can take the following values:

StatusDescription
pendingCreated, awaiting processing
awaiting_confirmationBroadcast on-chain, waiting for confirmations — txid is set
completedCompleted successfully — txid is set
failedSending error — see error_type
cancelledCancelled

Error types

When status = failed, the error_type field describes why:

CodeDescription
aml_riskPayout 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: sign field in the request body, computed with the Payout API key (the same key used to sign payout requests).

A payout emits a webhook when the transaction is broadcast (status: awaiting_confirmation, txid filled) and again when it becomes completed or failed.

The payload follows the result object from GET /v1/payout/status/{uuid} with description omitted and adds sign for verification.

Payload

json
{
  "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,
  "transfer_type": "blockchain",
  "txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
  "tx_explorer_url": "https://tronscan.org/#/transaction/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",
  "confirmations": {
    "current": 20,
    "required": 20
  },
  "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.