# Payout API

> Gửi rút tiền từ số dư merchant đến bất kỳ địa chỉ blockchain nào.

Payout API cho phép bạn rút tiền theo chương trình từ số dư merchant đến bất kỳ địa chỉ blockchain nào.

> **WARNING:** Đối với tất cả endpoint Payout, bạn phải sử dụng một **Payout API key** riêng để tạo chữ ký `sign`. Key này khác với API key thông thường và phải được tạo trong phần cài đặt project.

## Tạo rút tiền

Tạo một yêu cầu rút tiền từ số dư merchant.

`POST /v1/payout`

### Tham số yêu cầu

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `currency` | string | yes | Đồng tiền rút (xem [References](/docs/references)) |
| `network` | string | yes | Mã mạng (xem [References](/docs/references)) |
| `amount` | string | yes | Số tiền rút |
| `to_address` | string | yes | Địa chỉ blockchain nhận tiền |
| `order_id` | string | no | **Idempotency key** — duy nhất trong phạm vi project. Một `POST` lặp lại với cùng `order_id` sẽ không tạo rút tiền mới — thay vào đó trả về cái đã tồn tại |
| `url_callback` | string | no | URL nhận webhook rút tiền. Bỏ trống để tắt webhook cho rút tiền này |
| `memo` | string \| null | no | Destination tag / memo. Hiện tại chỉ được dùng bởi mạng **TON** và **SOL**; tối đa 255 ký tự |
| `from_currency` | string | no | Số dư nguồn sẽ bị trừ và tự động quy đổi sang `currency` tại thời điểm rút tiền. Cho phép bạn rút bằng các tài sản biến động (`BTC`, `ETH`, …) trong khi vẫn giữ số dư bằng stablecoin như `USDT` — bạn không phải tự nắm giữ tiền điện tử biến động. Truyền `"USDT"` để trừ từ số dư USDT |
| `fee_option` | string | no | Cách thu phí. `deduct` (mặc định) — phí mạng + phí nền tảng được trừ vào `amount`, người nhận nhận được `amount - fees`. `add` — phí được cộng thêm, merchant bị trừ `amount + fees`, người nhận nhận đúng `amount` |

> **INFO:** **Idempotency.** Trong một project, một rút tiền là duy nhất theo `order_id`. Gửi lại cùng `POST` với cùng `order_id` là **an toàn** — API trả về rút tiền hiện có thay vì tạo bản trùng lặp. Hãy luôn truyền `order_id` cho các rút tiền production.

### Ví dụ yêu cầu

<CodeTabs langs="curl,php,js,python,go">

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

```php
<?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);
```

```javascript
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();
```

```python
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()
```

```go
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()
}
```

</CodeTabs>

### Ví dụ phản hồi

```json
{
  "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"
  }
}
```

> **INFO:** **Phí.** Mặc định `fee_option: deduct` — phí mạng + phí nền tảng được trừ vào `amount` (người nhận nhận `amount - fees`). Truyền `fee_option: add` để cộng phí lên trên — người nhận nhận đúng `amount` và merchant bị trừ `amount + fees`.

## Tính toán rút tiền

Ước tính số tiền và phí rút **mà không tạo rút tiền** và không trừ số dư của bạn. Dùng để hiển thị cho người dùng số tiền chính xác họ sẽ nhận (hoặc trả) trước khi xác nhận.

`POST /v1/payout/calc`

### Tham số yêu cầu

Giống hệt với [Tạo rút tiền](#create-payout) — cùng các trường, cùng chữ ký. `order_id`, `url_callback`, `to_address` và `memo` được chấp nhận nhưng bị bỏ qua: không có rút tiền nào được lưu và không có callback nào được gửi.

### Ví dụ yêu cầu

```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"}'
```

### Ví dụ phản hồi

```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"
  }
}
```

> **INFO:** **Chỉ xem trước.** Endpoint này chỉ đọc — không có số dư nào bị trừ và không có bản ghi rút tiền nào được tạo. Hãy gọi tuỳ ý để hiển thị chi tiết phí trong UI của bạn.

## Trạng thái rút tiền

Lấy trạng thái của một yêu cầu rút tiền.

`GET /v1/payout/status/{uuid}`

### Tham số đường dẫn

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `uuid` | string | yes | UUID rút tiền (lấy từ `result.uuid` khi tạo) |

### Ví dụ phản hồi

```json
{
  "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"
  }
}
```

> **INFO:** Đối với yêu cầu GET này, chữ ký được tính từ thân rỗng:
`hash_hmac('sha256', base64_encode(''), $apiKey)`

## Trường phản hồi

Các trường được trả về trong `result` từ `POST /v1/payout` và `GET /v1/payout/status/{uuid}`:

| Field | Type | Description |
|-------|------|-------------|
| `uuid` | string | UUID rút tiền do hệ thống cấp |
| `order_id` | string | Mã định danh rút tiền nội bộ của bạn (duy nhất trong project) |
| `status` | string | Trạng thái rút tiền hiện tại (xem bên dưới) |
| `currency` | string | Đồng tiền rút |
| `network` | string | Mã mạng |
| `amount` | string | Số tiền rút theo yêu cầu |
| `merchant_amount` | string | Số tiền bị trừ từ số dư merchant |
| `network_amount` | string | Số tiền thực sự được gửi trên chuỗi (sau phí mạng + phí nền tảng) |
| `amount_usd` | string | Giá trị quy đổi USD của số tiền rút |
| `to_address` | string | Địa chỉ blockchain nhận tiền |
| `memo` | string \| null | Destination tag / memo (TON, SOL). Ngược lại là `null` |
| `txid` | string \| null | Hash giao dịch blockchain. `null` cho đến khi giao dịch được gửi |
| `block_number` | int \| null | Số block chứa giao dịch. `null` cho đến khi được đưa vào |
| `error_type` | string \| null | Lý do thất bại khi `status = failed` (xem Error types bên dưới). Ngược lại là `null` |
| `created_at` | string (ISO 8601) | Thời điểm tạo rút tiền |
| `updated_at` | string (ISO 8601) | Thời điểm trạng thái thay đổi gần nhất |
| `from_currency` | string \| null | Số dư nguồn mà khoản rút tiền đã bị trừ khi sử dụng quy đổi tự động (ví dụ `USDT` cho khoản rút `BTC`). `null` nếu không có quy đổi nào xảy ra |
| `debited_amount` | string \| null | Số tiền thực sự bị trừ từ số dư nguồn sau quy đổi. Chỉ có khi sử dụng tự quy đổi |
| `debited_currency` | string \| null | Đồng tiền của `debited_amount` — số dư từ đó tiền bị trừ |

## Trạng thái rút tiền

Trường `status` có thể nhận các giá trị sau:

| Status | Description |
|--------|-------------|
| `pending` | Đã tạo, chờ xử lý |
| `completed` | Hoàn tất thành công — `txid` đã được đặt |
| `failed` | Lỗi gửi — xem `error_type` |
| `cancelled` | Đã hủy |

## Loại lỗi

Khi `status = failed`, trường `error_type` mô tả lý do:

| Code | Description |
|------|-------------|
| `aml_risk` | Rút tiền bị chặn bởi kiểm tra rủi ro AML (địa chỉ người nhận bị đánh dấu rủi ro cao) |

## Thông báo webhook

Khi trạng thái của một rút tiền thay đổi, hệ thống sẽ gửi webhook `POST` đến URL `url_callback` được truyền khi tạo rút tiền. Nếu không cung cấp `url_callback`, sẽ không có webhook nào được gửi cho rút tiền đó.

- **Method:** `POST`
- **Content-Type:** `application/json`
- **Chữ ký:** trường `sign` trong thân yêu cầu, được tính bằng **Payout API key** (cùng key dùng để ký yêu cầu rút tiền).

Payload phản chiếu object `result` từ `GET /v1/payout/status/{uuid}` cộng với một trường `sign` để xác minh.

### 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,
  "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"
}
```

> **WARNING:** **Xác minh chữ ký.** Sử dụng cùng thuật toán như với [webhook thanh toán](/docs/webhooks), nhưng ký bằng **Payout API key** thay vì API key thông thường. Loại bỏ trường `sign`, mã hóa JSON phần payload còn lại, mã hóa Base64, sau đó tính `hash_hmac('sha256', $base64, $payoutApiKey)` và so sánh với `sign` nhận được.