# Payout API

> ส่งการถอนจากยอดคงเหลือของผู้ค้าไปยังที่อยู่บล็อกเชนใด ๆ

Payout API ให้คุณถอนเงินจากยอดคงเหลือของผู้ค้าไปยังที่อยู่บล็อกเชนใด ๆ ได้ผ่านโปรแกรม

> **WARNING:** สำหรับ endpoint การถอนทั้งหมด คุณต้องใช้ **Payout API key** แยกต่างหากเพื่อสร้างลายเซ็น `sign` คีย์นี้แตกต่างจาก API key ปกติของคุณและต้องสร้างขึ้นในการตั้งค่าโปรเจกต์

## สร้างการถอน

สร้างคำขอถอนเงินจากยอดคงเหลือของผู้ค้า

`POST /v1/payout`

### พารามิเตอร์ของคำขอ

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `currency` | string | yes | สกุลเงินที่ถอน (ดู [References](/docs/references)) |
| `network` | string | yes | รหัสเครือข่าย (ดู [References](/docs/references)) |
| `amount` | string | yes | จำนวนเงินที่ถอน |
| `to_address` | string | yes | ที่อยู่บล็อกเชนของผู้รับ |
| `order_id` | string | no | **คีย์ idempotency** — ไม่ซ้ำกันภายในโปรเจกต์ การส่ง `POST` ซ้ำด้วย `order_id` เดิมจะไม่สร้างการถอนใหม่ — ระบบจะคืนรายการที่มีอยู่แทน |
| `url_callback` | string | no | URL สำหรับ Webhook ของการถอน เว้นว่างไว้เพื่อปิดใช้งาน Webhook สำหรับการถอนนี้ |
| `memo` | string \| null | no | Destination tag / memo ปัจจุบันใช้กับเครือข่าย **TON** และ **SOL** เท่านั้น สูงสุด 255 ตัวอักษร |
| `from_currency` | string | no | ยอดคงเหลือต้นทางที่จะถูกตัดและแปลงอัตโนมัติเป็น `currency` ในขณะที่ทำการถอน ช่วยให้คุณถอนเป็นสินทรัพย์ที่มีความผันผวน (`BTC`, `ETH`, …) ได้โดยที่ยังเก็บยอดคงเหลือเป็นสเตเบิลคอยน์อย่าง `USDT` — คุณไม่ต้องถือคริปโตที่มีความผันผวนเอง ส่ง `"USDT"` เพื่อตัดจากยอดคงเหลือ USDT |
| `fee_option` | string | no | วิธีคิดค่าธรรมเนียม `deduct` (ค่าเริ่มต้น) — ค่าธรรมเนียมเครือข่าย + แพลตฟอร์มหักจาก `amount` ผู้รับได้ `amount - fees` `add` — ค่าธรรมเนียมบวกเพิ่ม ผู้ค้าถูกตัด `amount + fees` ผู้รับได้ `amount` พอดี |

> **INFO:** **Idempotency** ภายในโปรเจกต์เดียวกัน การถอนจะไม่ซ้ำตาม `order_id` การส่ง `POST` ซ้ำด้วย `order_id` เดิมเป็นเรื่อง **ปลอดภัย** — API จะคืนการถอนที่มีอยู่แทนการสร้างซ้ำ ในระบบโปรดักชันให้ระบุ `order_id` เสมอสำหรับการถอน

### ตัวอย่างคำขอ

<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>

### ตัวอย่างการตอบกลับ

```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:** **ค่าธรรมเนียม** ค่าเริ่มต้น `fee_option: deduct` — ค่าธรรมเนียมเครือข่าย + แพลตฟอร์มจะถูกหักจาก `amount` (ผู้รับได้ `amount - fees`) ส่ง `fee_option: add` เพื่อบวกค่าธรรมเนียมเพิ่ม — ผู้รับจะได้ `amount` พอดี และผู้ค้าจะถูกตัด `amount + fees`

## คำนวณการถอน

ประเมินจำนวนเงินและค่าธรรมเนียมการถอน **โดยไม่สร้างการถอนจริง** และไม่หักจากยอดคงเหลือ ใช้เพื่อแสดงจำนวนเงินที่ผู้ใช้จะได้รับ (หรือจ่าย) อย่างแม่นยำก่อนยืนยัน

`POST /v1/payout/calc`

### พารามิเตอร์ของคำขอ

เหมือนกับ [สร้างการถอน](#create-payout) ทุกประการ — ฟิลด์เดียวกัน วิธีลงนามเดียวกัน ฟิลด์ `order_id`, `url_callback`, `to_address` และ `memo` รับได้แต่จะถูกละเว้น: ไม่มีการบันทึกการถอนและไม่มีการส่ง callback

### ตัวอย่างคำขอ

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

### ตัวอย่างการตอบกลับ

```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:** **เฉพาะตัวอย่าง.** Endpoint นี้เป็นแบบอ่านอย่างเดียว — ไม่มีการหักยอดคงเหลือและไม่มีการสร้างเรกคอร์ดการถอน เรียกใช้ได้ตามต้องการเพื่อแสดงรายละเอียดค่าธรรมเนียมใน UI

## สถานะการถอน

ดูสถานะของคำขอถอน

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

### พารามิเตอร์ใน Path

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `uuid` | string | yes | UUID ของการถอน (จาก `result.uuid` ตอนสร้าง) |

### ตัวอย่างการตอบกลับ

```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:** สำหรับคำขอ GET นี้ลายเซ็นคำนวณจาก body ที่ว่าง:
`hash_hmac('sha256', base64_encode(''), $apiKey)`

## ฟิลด์ในการตอบกลับ

ฟิลด์ที่คืนใน `result` จาก `POST /v1/payout` และ `GET /v1/payout/status/{uuid}`:

| Field | Type | Description |
|-------|------|-------------|
| `uuid` | string | UUID ของการถอนที่ระบบกำหนด |
| `order_id` | string | ตัวระบุการถอนภายในของคุณ (ไม่ซ้ำกันภายในโปรเจกต์) |
| `status` | string | สถานะปัจจุบันของการถอน (ดูด้านล่าง) |
| `currency` | string | สกุลเงินที่ถอน |
| `network` | string | รหัสเครือข่าย |
| `amount` | string | จำนวนเงินถอนที่ร้องขอ |
| `merchant_amount` | string | จำนวนเงินที่หักจากยอดคงเหลือผู้ค้า |
| `network_amount` | string | จำนวนเงินที่ส่งจริงบนเชน (หลังหักค่าธรรมเนียมเครือข่าย + แพลตฟอร์ม) |
| `amount_usd` | string | มูลค่าเทียบเท่า USD ของจำนวนเงินถอน |
| `to_address` | string | ที่อยู่บล็อกเชนของผู้รับ |
| `memo` | string \| null | Destination tag / memo (TON, SOL) `null` ในกรณีอื่น |
| `txid` | string \| null | hash ของธุรกรรมบล็อกเชน `null` จนกว่าจะส่งธุรกรรม |
| `block_number` | int \| null | หมายเลขบล็อกที่บรรจุธุรกรรม `null` จนกว่าจะถูกบรรจุ |
| `error_type` | string \| null | สาเหตุที่ล้มเหลวเมื่อ `status = failed` (ดู Error types ด้านล่าง) `null` ในกรณีอื่น |
| `created_at` | string (ISO 8601) | เวลาที่สร้างการถอน |
| `updated_at` | string (ISO 8601) | เวลาที่สถานะเปลี่ยนแปลงล่าสุด |
| `from_currency` | string \| null | ยอดคงเหลือต้นทางที่ถูกตัดเมื่อใช้การแปลงอัตโนมัติ (เช่น `USDT` สำหรับการถอนเป็น `BTC`) เป็น `null` หากไม่มีการแปลง |
| `debited_amount` | string \| null | จำนวนเงินที่ถูกตัดจริงจากยอดคงเหลือต้นทางหลังการแปลง ปรากฏเฉพาะเมื่อใช้การแปลงอัตโนมัติ |
| `debited_currency` | string \| null | สกุลเงินของ `debited_amount` — ยอดคงเหลือที่เงินถูกตัดออก |

## สถานะของการถอน

ฟิลด์ `status` รับค่าต่อไปนี้:

| Status | Description |
|--------|-------------|
| `pending` | สร้างแล้ว รอการดำเนินการ |
| `completed` | ดำเนินการสำเร็จ — `txid` ถูกตั้งค่า |
| `failed` | เกิดข้อผิดพลาดในการส่ง — ดู `error_type` |
| `cancelled` | ยกเลิกแล้ว |

## ประเภทข้อผิดพลาด

เมื่อ `status = failed` ฟิลด์ `error_type` อธิบายสาเหตุ:

| Code | Description |
|------|-------------|
| `aml_risk` | การถอนถูกบล็อกโดยการตรวจสอบความเสี่ยง AML (ที่อยู่ผู้รับถูกแฟล็กว่ามีความเสี่ยงสูง) |

## การแจ้งเตือน Webhook

เมื่อ `status` ของการถอนเปลี่ยน ระบบจะส่ง Webhook แบบ `POST` ไปยัง URL `url_callback` ที่ระบุตอนสร้างการถอน หากไม่ได้ระบุ `url_callback` จะไม่มีการส่ง Webhook สำหรับการถอนนั้น

- **Method:** `POST`
- **Content-Type:** `application/json`
- **Signature:** ฟิลด์ `sign` ใน body ของคำขอ คำนวณด้วย **Payout API key** (คีย์เดียวกับที่ใช้ลงลายเซ็นคำขอถอน)

payload สะท้อนออบเจกต์ `result` จาก `GET /v1/payout/status/{uuid}` พร้อมเพิ่มฟิลด์ `sign` สำหรับการตรวจสอบ

### 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:** **การตรวจสอบลายเซ็น** ใช้อัลกอริทึมเดียวกับ [payment webhooks](/docs/webhooks) แต่ลงลายเซ็นด้วย **Payout API key** แทน API key ปกติ ตัดฟิลด์ `sign` ออก แปลง payload ที่เหลือเป็น JSON เข้ารหัสด้วย Base64 จากนั้นคำนวณ `hash_hmac('sha256', $base64, $payoutApiKey)` แล้วเปรียบเทียบกับ `sign` ที่ได้รับ