Xác thực và ký yêu cầu
Ký các yêu cầu API bằng HMAC-SHA256 với project UUID và API key của bạn.
Mọi yêu cầu API (ngoại trừ webhook đến) đều phải kèm theo project UUID và chữ ký yêu cầu. Chữ ký chứng minh rằng yêu cầu xuất phát từ bạn và không bị ai thay đổi trên đường truyền.
API keys
2328.io sử dụng hai key dùng chung một thuật toán ký nhưng áp dụng cho các endpoint khác nhau:
| Key | Dùng cho |
|---|---|
| API key | Thanh toán, ví tĩnh, số dư, tỷ giá và xác minh webhook thanh toán / ví tĩnh |
| Payout API key | Tất cả endpoint /v1/payout/* và xác minh webhook rút tiền |
Cả hai key đều nằm trong phần cài đặt project tại 2328.io. Các ví dụ bên dưới ghi chung chung là "API key" — hãy thay bằng key phù hợp cho endpoint mà bạn đang gọi.
Tuyệt đối không lẫn lộn hai key: ký một yêu cầu rút tiền bằng API key thông thường (hoặc ký yêu cầu thanh toán bằng Payout key) sẽ trả về lỗi chữ ký.
Headers bắt buộc
| Header | Type | Required | Description |
|---|---|---|---|
Content-Type | string | yes | Luôn là application/json |
project | string | yes | Project UUID của bạn |
sign | string | yes | Chữ ký HMAC-SHA256 của yêu cầu, được tính bằng API key của bạn |
User-Agent | string | có | Định danh ứng dụng của bạn (ví dụ MyShop/1.4 (+https://myshop.example)). Yêu cầu không có User-Agent có thể bị chặn. |
Cách hoạt động của chữ ký
Hãy hình dung chữ ký như là dấu vân tay của thân yêu cầu. Nó được tạo ra bằng cách:
- Serialize thân yêu cầu sang JSON (compact — không có khoảng trắng dư).
- Mã hóa Base64 chuỗi JSON đó. Bước này chuẩn hóa đầu vào giữa các ngôn ngữ — khi đã là ASCII thuần, mọi ngôn ngữ đều cho ra cùng các byte để HMAC.
- Tính HMAC-SHA256 của chuỗi Base64 bằng API key của bạn, sau đó chuyển kết quả sang hex chữ thường.
Đối với GET và các loại yêu cầu khác không có thân, hãy ký chuỗi rỗng thay vì JSON.
Chữ ký của chuỗi rỗng là cố định với một API key cho trước. Bạn có thể cache lại nếu thực hiện nhiều cuộc gọi GET.
Triển khai
<?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
}Yêu cầu không có thân (GET)
Đối với các yêu cầu không có thân (ví dụ GET /v1/payout/status/{uuid}), hãy ký một chuỗi rỗng:
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))
}Ví dụ yêu cầu đầy đủ
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()
}Tuyệt đối không để lộ API key trong mã chạy phía client. Hãy ký yêu cầu trên backend của bạn. Một API key bị rò rỉ sẽ cho bất kỳ ai toàn quyền truy cập tài khoản merchant của bạn.
Xác minh chữ ký webhook
Khi 2328.io gửi webhook cho bạn, cùng thuật toán đó được chạy ngược lại:
- Lấy trường
signra khỏi payload. - Mã hóa JSON các trường còn lại (compact, không khoảng trắng).
- Mã hóa Base64 chuỗi đó.
- Tính
HMAC-SHA256bằng key phù hợp. - So sánh với
signnhận được bằng cách so sánh thời gian không đổi (hash_equals,crypto.timingSafeEqual,hmac.compare_digest,subtle.ConstantTimeCompare,OpenSSL.fixed_length_secure_compare).
Key dùng để ký phụ thuộc vào nguồn webhook:
| Webhook | Key để xác minh |
|---|---|
Webhook thanh toán / ví tĩnh (/v1/payment, /v1/static-wallet) | API key |
Webhook rút tiền (/v1/payout) | Payout API key |
Lỗi xác minh thường gặp. Bộ mã hóa JSON của bạn phải tạo ra đúng những byte giống hệt như bên gửi đã tạo — nếu không, Base64 sẽ khác đi và chữ ký sẽ không khớp.
- Go: dùng
json.NewEncodervớiSetEscapeHTML(false).json.Marshalmặc định sẽ escape<,>,&thành<và làm hỏng chữ ký. - Python: truyền
ensure_ascii=Falsevàojson.dumps. Nếu không, các ký tự không thuộc ASCII (Kirin, Trung Quốc, …) sẽ bị escape thành\uXXXX. - JSON rút gọn: không có khoảng trắng giữa các trường (
separators=(",", ":")trong Python). - Thứ tự trường (Go): một
map[string]anythường sẽ ngẫu nhiên hóa thứ tự khóa khi mã hóa lại. Hãy dùngjson.RawMessage, một struct có thứ tự, hoặc loại bỏsignkhỏi byte gốc.
Nếu việc xác minh vẫn thất bại, hãy tự chạy apiSign trên payload — nó phải tạo ra cùng chuỗi hex với sign đã nhận.
Một chữ ký hợp lệ không ngăn được replay. Nó chỉ chứng minh rằng webhook đến từ 2328.io — không ngăn được kẻ tấn công gửi lại một webhook đã bị bắt được sau đó. Luôn kiểm tra idempotency bằng uuid (hoặc txid cho ví tĩnh) trước khi ghi có tiền. Từ chối với HTTP 401 nếu chữ ký bị thiếu hoặc sai.
Các ví dụ mã đầy đủ có trên Webhook Notifications. Xử lý retry và quy tắc idempotency có trong Best practices.