# การยืนยันตัวตนและการลงลายเซ็นคำขอ

> ลงลายเซ็นคำขอ API ด้วย HMAC-SHA256 โดยใช้ project UUID และ API key ของคุณ

คำขอ API ทุกรายการ (ยกเว้น Webhook ขาเข้า) ต้องมี project UUID และลายเซ็นคำขอแนบมาด้วย ลายเซ็นเป็นการพิสูจน์ว่าคำขอมาจากคุณจริงและไม่มีใครแก้ไขระหว่างทาง

## API key

2328.io ใช้ **คีย์สองชุด** ที่ใช้อัลกอริทึมการลงลายเซ็นเดียวกัน แต่ครอบคลุม endpoint ต่างกัน:

| Key | ใช้สำหรับ |
|-----|-----------|
| **API key** | การชำระเงิน, กระเป๋าเงินคงที่, ยอดคงเหลือ, อัตราแลกเปลี่ยน และการตรวจสอบ Webhook ของการชำระเงิน / กระเป๋าเงินคงที่ |
| **Payout API key** | endpoint `/v1/payout/*` ทั้งหมด และการตรวจสอบ Webhook ของการถอน |

คีย์ทั้งสองอยู่ในการตั้งค่าโปรเจกต์ของคุณที่ [2328.io](https://2328.io) ตัวอย่างด้านล่างจะใช้คำว่า "API key" แบบรวม ๆ — ให้แทนที่ด้วยคีย์ที่ถูกต้องตาม endpoint ที่คุณกำลังเรียกใช้

> **INFO:** **ห้าม** สลับใช้คีย์ทั้งสอง: การลงลายเซ็นคำขอถอนด้วย API key ปกติ (หรือคำขอชำระเงินด้วย Payout key) จะคืนข้อผิดพลาดเรื่องลายเซ็น

## Header ที่จำเป็น

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Content-Type` | string | yes | ต้องเป็น `application/json` เสมอ |
| `project` | string | yes | project UUID ของคุณ |
| `sign` | string | yes | ลายเซ็น HMAC-SHA256 ของคำขอ คำนวณด้วย API key ของคุณ |
| `User-Agent` | string | ใช่ | ระบุแอปพลิเคชันของคุณ (เช่น `MyShop/1.4 (+https://myshop.example)`) คำขอที่ไม่มี `User-Agent` อาจถูกบล็อก |

## ลายเซ็นทำงานอย่างไร

คิดว่าลายเซ็นเป็นลายนิ้วมือของ body คำขอ มันถูกสร้างขึ้นโดย:

1. แปลง body เป็น JSON (กระชับ — ไม่มีช่องว่างเกิน)
2. เข้ารหัส JSON นั้นด้วย Base64 ขั้นตอนนี้ทำให้อินพุตเป็นมาตรฐานเดียวกันในทุกภาษา — เมื่อกลายเป็น ASCII ล้วนแล้ว ทุกภาษาจะให้ไบต์เดียวกันสำหรับ HMAC
3. คำนวณ **HMAC-SHA256** ของสตริง Base64 โดยใช้ API key ของคุณ จากนั้นแปลงผลลัพธ์เป็น hex ตัวพิมพ์เล็ก

สำหรับคำขอแบบ **GET** และคำขอประเภทอื่น ๆ ที่ไม่มี body ให้ลงลายเซ็นสตริงว่างแทน JSON

> **INFO:** ลายเซ็นของสตริงว่างเป็นค่าคงที่สำหรับ API key ใด ๆ คุณสามารถ cache ไว้ได้หากเรียก GET หลายครั้ง

## การ implement

#### php

```php
<?php
function apiSign(array $data, string $apiKey): string {
    $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $base64 = base64_encode($json);
    return hash_hmac('sha256', $base64, $apiKey);
}
```

#### js

```js
import crypto from "crypto";

export function apiSign(data, apiKey) {
  const json = JSON.stringify(data);
  const base64 = Buffer.from(json).toString("base64");
  return crypto.createHmac("sha256", apiKey).update(base64).digest("hex");
}
```

#### ts

```ts
import { createHmac } from "crypto";

export function apiSign(data: object, apiKey: string): string {
  const json = JSON.stringify(data);
  const base64 = Buffer.from(json).toString("base64");
  return createHmac("sha256", apiKey).update(base64).digest("hex");
}
```

#### python

```python
import json
import hmac
import hashlib
import base64

def api_sign(data: dict, api_key: str) -> str:
    # ensure_ascii=False keeps non-ASCII characters (Cyrillic, Chinese, …)
    # as-is. Without it, Python escapes them to \uXXXX and the signature
    # diverges from PHP / Node / Go.
    body = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
    b64 = base64.b64encode(body.encode("utf-8")).decode()
    return hmac.new(api_key.encode(), b64.encode(), hashlib.sha256).hexdigest()
```

#### go

```go
package sign

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
)

func ApiSign(data any, apiKey string) (string, error) {
    // json.Encoder with SetEscapeHTML(false) — without it, Go escapes <, >, &
    // to \u003c etc., which breaks compatibility with PHP / Node / Python.
    var buf bytes.Buffer
    enc := json.NewEncoder(&buf)
    enc.SetEscapeHTML(false)
    if err := enc.Encode(data); err != nil {
        return "", err
    }
    // Encoder appends a trailing newline — drop it.
    body := bytes.TrimRight(buf.Bytes(), "\n")

    b64 := base64.StdEncoding.EncodeToString(body)
    h := hmac.New(sha256.New, []byte(apiKey))
    h.Write([]byte(b64))
    return hex.EncodeToString(h.Sum(nil)), nil
}
```

### คำขอที่ไม่มี body (GET)

สำหรับคำขอที่ไม่มี body (เช่น `GET /v1/payout/status/{uuid}`) ให้ลงลายเซ็นสตริงว่าง:

#### curl

```curl
SIGN=$(printf '' | openssl dgst -sha256 -hmac "$API_KEY" -hex | awk '{print $NF}')
```

#### php

```php
$sign = hash_hmac('sha256', base64_encode(''), $apiKey);
```

#### js

```js
import { createHmac } from "crypto";

const sign = createHmac("sha256", apiKey)
  .update(Buffer.from("").toString("base64"))
  .digest("hex");
```

#### ts

```ts
import { createHmac } from "crypto";

const sign: string = createHmac("sha256", apiKey)
  .update(Buffer.from("").toString("base64"))
  .digest("hex");
```

#### python

```python
import hmac
import hashlib
import base64

sign = hmac.new(
    api_key.encode(),
    base64.b64encode(b"").decode().encode(),
    hashlib.sha256,
).hexdigest()
```

#### go

```go
package sign

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
)

func EmptyBodySign(apiKey string) string {
    b64 := base64.StdEncoding.EncodeToString([]byte(""))
    h := hmac.New(sha256.New, []byte(apiKey))
    h.Write([]byte(b64))
    return hex.EncodeToString(h.Sum(nil))
}
```

## ตัวอย่างคำขอแบบเต็ม

#### curl

```curl
curl -X POST https://api.2328.io/api/v1/payment \
  -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 '{"amount":"100.00","currency":"USD","order_id":"ORDER-123"}'
```

#### php

```php
<?php
function apiSign(string $body, string $apiKey): string {
    return hash_hmac('sha256', base64_encode($body), $apiKey);
}

$project = 'YOUR_PROJECT_UUID';
$apiKey  = 'YOUR_API_KEY';

$data = [
    'amount'   => '100.00',
    'currency' => 'USD',
    'order_id' => 'ORDER-123',
];

$body = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$sign = apiSign($body, $apiKey);

$ch = curl_init('https://api.2328.io/api/v1/payment');
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);
```

#### js

```js
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 data = {
  amount: "100.00",
  currency: "USD",
  order_id: "ORDER-123",
};

const body = JSON.stringify(data);
const sign = apiSign(body, process.env.API_KEY);

const res = await fetch("https://api.2328.io/api/v1/payment", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "User-Agent": "MyShop/1.0 (+https://myshop.example)",
    project: process.env.PROJECT_UUID,
    sign,
  },
  body,
});

const json = await res.json();
```

#### ts

```ts
import { createHmac } from "crypto";

function apiSign(body: string, apiKey: string): string {
  const base64 = Buffer.from(body, "utf8").toString("base64");
  return createHmac("sha256", apiKey).update(base64).digest("hex");
}

type CreatePaymentBody = {
  amount: string;
  currency: string;
  order_id: string;
};

type CreatePaymentResponse = { state: number; result: unknown };

const data: CreatePaymentBody = {
  amount: "100.00",
  currency: "USD",
  order_id: "ORDER-123",
};

const body = JSON.stringify(data);
const sign = apiSign(body, process.env.API_KEY!);

const res = await fetch("https://api.2328.io/api/v1/payment", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "User-Agent": "MyShop/1.0 (+https://myshop.example)",
    project: process.env.PROJECT_UUID!,
    sign,
  },
  body,
});

const json = (await res.json()) as CreatePaymentResponse;
```

#### python

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

data = {
    "amount": "100.00",
    "currency": "USD",
    "order_id": "ORDER-123",
}

body = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
sign = api_sign(body, API_KEY)

r = httpx.post(
    "https://api.2328.io/api/v1/payment",
    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

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

func main() {
    data := struct {
        Amount   string `json:"amount"`
        Currency string `json:"currency"`
        OrderID  string `json:"order_id"`
    }{
        Amount:   "100.00",
        Currency: "USD",
        OrderID:  "ORDER-123",
    }

    body, err := marshalCanonical(data)
    if err != nil {
        panic(err)
    }
    sign := ApiSign(body, apiKey)

    req, _ := http.NewRequest("POST",
        "https://api.2328.io/api/v1/payment",
        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()
}
```

> **DANGER:** **ห้ามเปิดเผย API key ของคุณในโค้ดฝั่ง client เด็ดขาด** ให้ลงลายเซ็นคำขอบน backend หาก API key รั่วไหล ใครก็ตามจะเข้าถึงบัญชีผู้ค้าของคุณได้อย่างเต็มรูปแบบ

## การตรวจสอบลายเซ็น Webhook

เมื่อ 2328.io ส่ง Webhook มาให้คุณ จะใช้อัลกอริทึมเดียวกันแต่ทำกลับด้าน:

1. ดึงฟิลด์ `sign` ออกจาก payload
2. เข้ารหัสฟิลด์ที่เหลือเป็น JSON (กระชับ ไม่มีช่องว่าง)
3. เข้ารหัสสตริงนั้นด้วย Base64
4. คำนวณ `HMAC-SHA256` ด้วยคีย์ที่เหมาะสม
5. เปรียบเทียบกับ `sign` ที่ได้รับโดยใช้การเปรียบเทียบแบบ **constant-time** (`hash_equals`, `crypto.timingSafeEqual`, `hmac.compare_digest`, `subtle.ConstantTimeCompare`, `OpenSSL.fixed_length_secure_compare`)

คีย์ที่ใช้ลงลายเซ็นขึ้นกับแหล่งของ Webhook:

| Webhook | Key to verify with |
|---------|---------------------|
| Webhook การชำระเงิน / กระเป๋าเงินคงที่ (`/v1/payment`, `/v1/static-wallet`) | **API key** |
| Webhook การถอน (`/v1/payout`) | **Payout API key** |

> **WARNING:** **ข้อผิดพลาดที่พบบ่อยในการตรวจสอบ** ตัวเข้ารหัส JSON ของคุณต้องสร้าง **ไบต์ที่เหมือนกันทุกประการ** กับที่ผู้ส่งสร้างขึ้น — มิฉะนั้น Base64 จะต่างกันและลายเซ็นจะไม่ตรงกัน
> 
> - **Go**: ใช้ `json.NewEncoder` พร้อม `SetEscapeHTML(false)` ค่าเริ่มต้นของ `json.Marshal` จะ escape `<`, `>`, `&` เป็น `<` และทำให้ลายเซ็นเสียหาย
> - **Python**: ส่ง `ensure_ascii=False` ไปยัง `json.dumps` ถ้าไม่ส่ง ตัวอักษรที่ไม่ใช่ ASCII (ซีริลลิก จีน …) จะถูก escape เป็น `\uXXXX`
> - **JSON แบบกระชับ**: ห้ามมีช่องว่างระหว่างฟิลด์ (`separators=(",", ":")` ใน Python)
> - **ลำดับฟิลด์** (Go): `map[string]any` ธรรมดาจะสุ่มลำดับคีย์เมื่อเข้ารหัสใหม่ ให้ใช้ `json.RawMessage`, struct ที่มีลำดับแน่นอน หรือลบ `sign` ออกจากไบต์ดิบ
> 
> หากการตรวจสอบยังคงล้มเหลว ให้รัน `apiSign` กับ payload ด้วยตัวเอง — ต้องสร้างสตริงเลขฐานสิบหกเดียวกันกับ `sign` ที่ได้รับ

> **INFO:** **ลายเซ็นที่ถูกต้องไม่สามารถป้องกันการ replay ได้** มันเพียงพิสูจน์ว่า webhook มาจาก 2328.io เท่านั้น — มันไม่ได้หยุดผู้โจมตีจากการส่ง webhook ที่*ถูกดักจับไว้*ซ้ำในภายหลัง ตรวจสอบ idempotency โดย `uuid` เสมอ (หรือ `txid` สำหรับ static wallets) ก่อนบันทึกเงินเข้าบัญชี ปฏิเสธด้วย HTTP `401` หากลายเซ็นหายไปหรือไม่ถูกต้อง

ตัวอย่างโค้ดเต็มอยู่ที่ **[Webhook Notifications](/docs/webhooks#verifying-the-signature)** การจัดการการลองใหม่และกฎ idempotency อยู่ใน [แนวปฏิบัติที่ดี](/docs/webhooks#best-practices)