Uwierzytelnianie i podpisywanie żądań
Podpisuj żądania API za pomocą HMAC-SHA256, używając project UUID i klucza API.
Każde żądanie API (z wyjątkiem przychodzących webhooków) musi zawierać Twój project UUID oraz podpis żądania. Podpis potwierdza, że żądanie pochodzi od Ciebie i nie zostało zmodyfikowane podczas przesyłania.
Klucze API
2328.io korzysta z dwóch kluczy, które używają tego samego algorytmu podpisywania, ale obsługują różne endpointy:
| Klucz | Stosowany do |
|---|---|
| API key | Płatności, statyczne portfele, saldo, kursy walutowe oraz weryfikacja webhooków płatności / statycznych portfeli |
| Payout API key | Wszystkie endpointy /v1/payout/* oraz weryfikacja webhooków wypłat |
Oba klucze znajdują się w ustawieniach Twojego projektu na 2328.io. Poniższe przykłady używają nazwy „API key" w sposób ogólny — podstaw odpowiedni klucz dla wywoływanego endpointu.
Nigdy nie mieszaj obu kluczy: podpisanie żądania wypłaty zwykłym kluczem API (lub żądania płatności kluczem wypłaty) zwróci błąd podpisu.
Wymagane nagłówki
| Nagłówek | Typ | Wymagany | Opis |
|---|---|---|---|
Content-Type | string | tak | Zawsze application/json |
project | string | tak | Twój project UUID |
sign | string | tak | Podpis HMAC-SHA256 żądania, obliczony za pomocą Twojego klucza API |
User-Agent | string | tak | Identyfikuje Twoją aplikację (np. MyShop/1.4 (+https://myshop.example)). Żądania bez User-Agent mogą zostać zablokowane. |
Jak działa podpis
Podpis można rozumieć jako odcisk palca treści żądania. Tworzony jest poprzez:
- Serializację treści do formatu JSON (kompaktowo — bez dodatkowych białych znaków).
- Zakodowanie tego JSON-a w base64. Ten krok normalizuje wejście pomiędzy językami — gdy mamy zwykły ASCII, każdy język produkuje te same bajty dla HMAC.
- Obliczenie HMAC-SHA256 ciągu base64 przy użyciu Twojego klucza API, a następnie konwersję wyniku na hex zapisany małymi literami.
Dla żądań GET oraz innych typów żądań bez treści podpisuj pusty ciąg zamiast JSON-a.
Podpis pustego ciągu jest stały dla danego klucza API. Możesz go zbuforować, jeśli wykonujesz wiele wywołań GET.
Implementacje
<?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
}Żądania bez treści (GET)
Dla żądań z pustą treścią (np. GET /v1/payout/status/{uuid}) podpisuj pusty ciąg:
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))
}Pełny przykład żądania
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()
}Nigdy nie ujawniaj swojego klucza API w kodzie po stronie klienta. Podpisuj żądania na swoim backendzie. Wyciek klucza API daje każdemu pełny dostęp do Twojego konta sprzedawcy.
Weryfikacja podpisów webhooków
Gdy 2328.io wysyła webhook, ten sam algorytm jest wykonywany w odwrotnej kolejności:
- Wyciągnij pole
signz payloadu. - Zakoduj pozostałe pola jako JSON (kompaktowo, bez białych znaków).
- Zakoduj ten ciąg w base64.
- Oblicz
HMAC-SHA256z odpowiednim kluczem. - Porównaj wynik z otrzymanym
signprzy użyciu porównania w czasie stałym (hash_equals,crypto.timingSafeEqual,hmac.compare_digest,subtle.ConstantTimeCompare,OpenSSL.fixed_length_secure_compare).
Klucz podpisujący zależy od źródła webhooka:
| Webhook | Klucz do weryfikacji |
|---|---|
Webhooki płatności / statycznych portfeli (/v1/payment, /v1/static-wallet) | API key |
Webhooki wypłat (/v1/payout) | Payout API key |
Typowe pułapki weryfikacji. Twój enkoder JSON musi produkować dokładnie te same bajty, które wyprodukował nadawca — inaczej Base64 będzie różny i podpis się nie zgodzi.
- Go: użyj
json.NewEncoderzSetEscapeHTML(false). Domyślnyjson.Marshalescapeuje<,>,&na<i psuje podpis. - Python: przekaż
ensure_ascii=Falsedojson.dumps. Bez tego znaki spoza ASCII (cyrylica, chiński, …) są escapeowane do\uXXXX. - Kompaktowy JSON: bez białych znaków między polami (
separators=(",", ":")w Pythonie). - Kolejność pól (Go): zwykła
map[string]anyrandomizuje klucze przy ponownym kodowaniu. Użyjjson.RawMessage, uporządkowanej struktury lub usuńsignz surowych bajtów.
Jeśli weryfikacja nadal zawodzi, uruchom apiSign na payloadzie samodzielnie — musi wyprodukować ten sam ciąg szesnastkowy co otrzymane sign.
Poprawny podpis nie chroni przed replayami. Dowodzi tylko, że webhook pochodzi z 2328.io — nie zapobiega ponownemu wysłaniu przechwyconego webhooka przez atakującego później. Zawsze sprawdzaj idempotentność po uuid (lub txid dla statycznych portfeli) zanim zaksięgujesz środki. Odrzuć z HTTP 401, jeśli podpis jest brakujący lub błędny.
Pełne przykłady kodu znajdziesz na Webhook Notifications. Obsługa ponawiania prób i reguły idempotentności są w Najlepszych praktykach.