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

> ลงลายเซ็นคำขอ 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

<CodeSnippet name="apiSign" langs="php,js,ts,python,go" />

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

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

<CodeSnippet name="apiSignBodyless" langs="curl,php,js,ts,python,go" />

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

<CodeSnippet name="fullRequestExample" langs="curl,php,js,ts,python,go" />

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