Kimlik Doğrulama ve İstek İmzalama
API isteklerini project UUID ve API key kullanarak HMAC-SHA256 ile imzalayın.
Her API isteği (gelen webhook'lar hariç) project UUID'nizi ve bir istek imzasını taşımalıdır. İmza, isteğin sizden geldiğini ve yolda kimsenin değiştirmediğini kanıtlar.
API key'leri
2328.io, aynı imzalama algoritmasını paylaşan ancak farklı endpoint'leri kapsayan iki anahtar kullanır:
| Anahtar | Kullanım yeri |
|---|---|
| API key | Ödemeler, statik cüzdanlar, bakiye, döviz kurları ve ödeme / statik cüzdan webhook'larının doğrulanması |
| Payout API key | Tüm /v1/payout/* endpoint'leri ve çekim webhook'larının doğrulanması |
Her iki anahtar da 2328.io üzerindeki proje ayarlarınızda yer alır. Aşağıdaki örneklerde "API key" genel olarak belirtilmiştir — çağırdığınız endpoint'e uygun olanını yerine koyun.
İki anahtarı asla karıştırmayın: bir çekim isteğini normal API key ile (veya bir ödeme isteğini payout key ile) imzalamak imza hatası döner.
Gerekli header'lar
| Header | Tip | Gerekli | Açıklama |
|---|---|---|---|
Content-Type | string | evet | Her zaman application/json |
project | string | evet | Project UUID'niz |
sign | string | evet | API key'iniz ile hesaplanan isteğin HMAC-SHA256 imzası |
User-Agent | string | evet | Uygulamanızı tanımlar (örn. MyShop/1.4 (+https://myshop.example)). User-Agent olmayan istekler engellenebilir. |
İmza nasıl çalışır
İmzayı isteğin gövdesinin parmak izi gibi düşünün. Şu adımlarla oluşturulur:
- Gövdeyi JSON'a serileştirin (kompakt — fazladan boşluk yok).
- Bu JSON'u Base64 ile encode edin. Bu adım girdiyi diller arası normalleştirir — düz ASCII olduğunda her dil HMAC için aynı baytları üretir.
- API key'inizi kullanarak Base64 string'inin HMAC-SHA256'sını hesaplayın, sonra sonucu küçük harf hex'e dönüştürün.
Gövdesiz GET ve diğer istek türleri için JSON yerine boş bir string'i imzalayın.
Verilen bir API key için boş string imzası sabittir. Çok sayıda GET çağrısı yapıyorsanız bunu cache'leyebilirsiniz.
Uygulamalar
<?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
}Gövdesiz istekler (GET)
Boş gövdeli istekler için (örn. GET /v1/payout/status/{uuid}), boş bir string'i imzalayın:
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))
}Tam istek örneği
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'inizi asla istemci tarafı kodda açığa çıkarmayın. İstekleri backend'inizde imzalayın. Sızdırılan bir API key, herkese merchant hesabınıza tam erişim verir.
Webhook imzalarını doğrulama
2328.io size bir webhook gönderdiğinde, aynı algoritma ters yönde çalışır:
- Payload'dan
signalanını çıkarın. - Kalan alanları JSON'a encode edin (kompakt, boşluksuz).
- Bu string'i Base64 ile encode edin.
- Uygun anahtarla
HMAC-SHA256hesaplayın. - Bunu, alınan
signile sabit zamanlı karşılaştırma kullanarak (hash_equals,crypto.timingSafeEqual,hmac.compare_digest,subtle.ConstantTimeCompare,OpenSSL.fixed_length_secure_compare) karşılaştırın.
İmzalama anahtarı webhook kaynağına bağlıdır:
| Webhook | Doğrulama anahtarı |
|---|---|
Ödeme / statik cüzdan webhook'ları (/v1/payment, /v1/static-wallet) | API key |
Çekim webhook'ları (/v1/payout) | Payout API key |
Sık karşılaşılan doğrulama tuzakları. JSON kodlayıcınız gönderenin ürettiği birebir aynı baytları üretmelidir — aksi halde Base64 farklılaşır ve imza eşleşmez.
- Go:
json.NewEncoderileSetEscapeHTML(false)kullanın. Varsayılanjson.Marshal,<,>,&karakterlerini<olarak escape eder ve imzayı bozar. - Python:
json.dumpsçağrısınaensure_ascii=Falsegeçirin. Bu olmadan, ASCII olmayan karakterler (Kiril, Çince, …)\uXXXXolarak escape edilir. - Kompakt JSON: alanlar arasında boşluk olmamalı (Python'da
separators=(",", ":")). - Alan sırası (Go): düz bir
map[string]anyyeniden kodlamada anahtarları rastgeleleştirir.json.RawMessage, sıralı bir struct kullanın ya dasignalanını ham baytlardan çıkarın.
Doğrulama hâlâ başarısız oluyorsa, payload üzerinde apiSign fonksiyonunu kendiniz çalıştırın — alınan sign ile aynı hex dizesini üretmelidir.
Geçerli bir imza yeniden oynatmaları (replay) engellemez. Yalnızca webhook'un 2328.io'dan geldiğini kanıtlar — bir saldırganın yakalanmış bir webhook'u sonradan tekrar göndermesini engellemez. Fonları alacaklandırmadan önce her zaman uuid (veya statik cüzdanlar için txid) ile idempotency kontrolü yapın. İmza eksik veya yanlışsa HTTP 401 ile reddedin.
Tam kod örnekleri Webhook Notifications sayfasındadır. Yeniden deneme yönetimi ve idempotency kuralları En iyi uygulamalar bölümünde bulunur.