การยืนยันตัวตนและการลงลายเซ็นคำขอ
ลงลายเซ็นคำขอ 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 ตัวอย่างด้านล่างจะใช้คำว่า "API key" แบบรวม ๆ — ให้แทนที่ด้วยคีย์ที่ถูกต้องตาม endpoint ที่คุณกำลังเรียกใช้
ห้าม สลับใช้คีย์ทั้งสอง: การลงลายเซ็นคำขอถอนด้วย 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 คำขอ มันถูกสร้างขึ้นโดย:
- แปลง body เป็น JSON (กระชับ — ไม่มีช่องว่างเกิน)
- เข้ารหัส JSON นั้นด้วย Base64 ขั้นตอนนี้ทำให้อินพุตเป็นมาตรฐานเดียวกันในทุกภาษา — เมื่อกลายเป็น ASCII ล้วนแล้ว ทุกภาษาจะให้ไบต์เดียวกันสำหรับ HMAC
- คำนวณ HMAC-SHA256 ของสตริง Base64 โดยใช้ API key ของคุณ จากนั้นแปลงผลลัพธ์เป็น hex ตัวพิมพ์เล็ก
สำหรับคำขอแบบ GET และคำขอประเภทอื่น ๆ ที่ไม่มี body ให้ลงลายเซ็นสตริงว่างแทน JSON
ลายเซ็นของสตริงว่างเป็นค่าคงที่สำหรับ API key ใด ๆ คุณสามารถ cache ไว้ได้หากเรียก GET หลายครั้ง
การ implement
<?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);
}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");
}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");
}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()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}) ให้ลงลายเซ็นสตริงว่าง:
SIGN=$(printf '' | openssl dgst -sha256 -hmac "$API_KEY" -hex | awk '{print $NF}')$sign = hash_hmac('sha256', base64_encode(''), $apiKey);import { createHmac } from "crypto";
const sign = createHmac("sha256", apiKey)
.update(Buffer.from("").toString("base64"))
.digest("hex");import { createHmac } from "crypto";
const sign: string = createHmac("sha256", apiKey)
.update(Buffer.from("").toString("base64"))
.digest("hex");import hmac
import hashlib
import base64
sign = hmac.new(
api_key.encode(),
base64.b64encode(b"").decode().encode(),
hashlib.sha256,
).hexdigest()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 -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
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);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();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;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()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()
}ห้ามเปิดเผย API key ของคุณในโค้ดฝั่ง client เด็ดขาด ให้ลงลายเซ็นคำขอบน backend หาก API key รั่วไหล ใครก็ตามจะเข้าถึงบัญชีผู้ค้าของคุณได้อย่างเต็มรูปแบบ
การตรวจสอบลายเซ็น Webhook
เมื่อ 2328.io ส่ง Webhook มาให้คุณ จะใช้อัลกอริทึมเดียวกันแต่ทำกลับด้าน:
- ดึงฟิลด์
signออกจาก payload - เข้ารหัสฟิลด์ที่เหลือเป็น JSON (กระชับ ไม่มีช่องว่าง)
- เข้ารหัสสตริงนั้นด้วย Base64
- คำนวณ
HMAC-SHA256ด้วยคีย์ที่เหมาะสม - เปรียบเทียบกับ
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 |
ข้อผิดพลาดที่พบบ่อยในการตรวจสอบ ตัวเข้ารหัส 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 ที่ได้รับ
ลายเซ็นที่ถูกต้องไม่สามารถป้องกันการ replay ได้ มันเพียงพิสูจน์ว่า webhook มาจาก 2328.io เท่านั้น — มันไม่ได้หยุดผู้โจมตีจากการส่ง webhook ที่ถูกดักจับไว้ซ้ำในภายหลัง ตรวจสอบ idempotency โดย uuid เสมอ (หรือ txid สำหรับ static wallets) ก่อนบันทึกเงินเข้าบัญชี ปฏิเสธด้วย HTTP 401 หากลายเซ็นหายไปหรือไม่ถูกต้อง
ตัวอย่างโค้ดเต็มอยู่ที่ Webhook Notifications การจัดการการลองใหม่และกฎ idempotency อยู่ใน แนวปฏิบัติที่ดี