المصادقة وتوقيع الطلبات
قم بتوقيع طلبات API باستخدام HMAC-SHA256 مع project UUID وAPI key الخاص بك.
يجب أن يحمل كل طلب API (باستثناء webhooks الواردة) project UUID الخاص بك وتوقيع الطلب. يثبت التوقيع أن الطلب صادر منك وأن لا أحد قام بتعديله أثناء النقل.
مفاتيح API
يستخدم 2328.io مفتاحين يشتركان في خوارزمية التوقيع نفسها لكنهما يغطيان نقاط نهاية مختلفة:
| المفتاح | يُستخدم لـ |
|---|---|
| API key | المدفوعات، المحافظ الثابتة، الرصيد، أسعار الصرف، والتحقق من webhooks المدفوعات/المحافظ الثابتة |
| Payout API key | جميع نقاط النهاية /v1/payout/* والتحقق من webhooks السحب |
كلا المفتاحين موجودان في إعدادات مشروعك على 2328.io. الأمثلة أدناه تذكر "API key" بشكل عام — استبدله بالمفتاح المناسب لنقطة النهاية التي تستدعيها.
لا تخلط أبدًا بين المفتاحين: توقيع طلب سحب باستخدام API key العادي (أو طلب دفع باستخدام Payout key) يُرجع خطأ توقيع.
الرؤوس المطلوبة
| الرأس | النوع | مطلوب | الوصف |
|---|---|---|---|
Content-Type | string | نعم | دائمًا application/json |
project | string | نعم | project UUID الخاص بك |
sign | string | نعم | توقيع HMAC-SHA256 للطلب، محسوب باستخدام API key الخاص بك |
User-Agent | string | نعم | يحدد تطبيقك (مثل MyShop/1.4 (+https://myshop.example)). قد يتم حظر الطلبات بدون User-Agent. |
كيف يعمل التوقيع
اعتبر التوقيع بمثابة بصمة لمحتوى الطلب. يتم بناؤه عن طريق:
- تسلسل المحتوى إلى JSON (مضغوط — بدون مسافات إضافية).
- ترميز Base64 لذلك JSON. هذه الخطوة تطبّع المدخلات عبر اللغات — بمجرد أن تصبح ASCII خالصة، تنتج كل اللغات نفس البايتات لـ HMAC.
- حساب HMAC-SHA256 لسلسلة Base64 باستخدام API key الخاص بك، ثم تحويل النتيجة إلى hex بأحرف صغيرة.
بالنسبة لطلبات GET وغيرها من الطلبات بدون محتوى، قم بتوقيع سلسلة فارغة بدلًا من JSON.
توقيع السلسلة الفارغة ثابت لمفتاح API معين. يمكنك تخزينه مؤقتًا إذا قمت بإجراء العديد من استدعاءات GET.
التطبيقات
<?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
}الطلبات بدون محتوى (GET)
بالنسبة للطلبات بدون محتوى (مثل 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 الخاص بك في كود من جانب العميل. قم بتوقيع الطلبات على الواجهة الخلفية لديك. تسرّب API key يمنح أي شخص وصولًا كاملًا إلى حساب التاجر.
التحقق من توقيعات webhook
عندما يرسل لك 2328.io webhook، تعمل نفس الخوارزمية بشكل عكسي:
- استخرج حقل
signمن المحتوى. - قم بترميز الحقول المتبقية كـ JSON (مضغوط، بدون مسافات).
- قم بترميز Base64 لتلك السلسلة.
- احسب
HMAC-SHA256بالمفتاح المناسب. - قارنه بـ
signالمستلم باستخدام مقارنة بزمن ثابت (hash_equals،crypto.timingSafeEqual،hmac.compare_digest،subtle.ConstantTimeCompare،OpenSSL.fixed_length_secure_compare).
يعتمد مفتاح التوقيع على مصدر webhook:
| Webhook | المفتاح المستخدم للتحقق |
|---|---|
webhooks المدفوعات / المحافظ الثابتة (/v1/payment، /v1/static-wallet) | API key |
webhooks السحب (/v1/payout) | Payout API key |
أخطاء شائعة في التحقق. يجب أن ينتج مُرمِّز JSON لديك نفس البايتات تمامًا التي أنتجها المُرسِل — وإلا فسيختلف Base64 ولن يتطابق التوقيع.
- Go: استخدم
json.NewEncoderمعSetEscapeHTML(false). الـjson.Marshalالافتراضي يهرب<و>و&إلى<ويُفسد التوقيع. - Python: مرِّر
ensure_ascii=Falseإلىjson.dumps. بدونها، تُهرَّب الأحرف غير ASCII (السيريلية، الصينية، …) إلى\uXXXX. - JSON مضغوط: بدون مسافات بيضاء بين الحقول (
separators=(",", ":")في Python). - ترتيب الحقول (Go): إن
map[string]anyالعادي يُرتِّب المفاتيح عشوائيًا عند إعادة الترميز. استخدمjson.RawMessageأو struct مُرتَّبة، أو احذفsignمن البايتات الأصلية.
إذا استمر فشل التحقق، فشغِّل apiSign على الحمولة بنفسك — يجب أن ينتج نفس السلسلة السداسية عشرية مثل sign المستلم.
التوقيع الصحيح لا يمنع إعادة الإرسال. هو فقط يُثبت أن الـ webhook جاء من 2328.io — ولا يمنع المهاجم من إعادة إرسال webhook تم اعتراضه لاحقًا. تحقق دائمًا من عدم التكرار باستخدام uuid (أو txid للمحافظ الثابتة) قبل قيد الأموال. ارفض الطلب بـ HTTP 401 إذا كان التوقيع مفقودًا أو خاطئًا.
أمثلة الكود الكاملة موجودة في Webhook Notifications. ومعالجة إعادة المحاولة وقواعد عدم التكرار في أفضل الممارسات.