Authentifizierung & Anfrage-Signierung
Signieren Sie API-Anfragen mit HMAC-SHA256 unter Verwendung Ihrer Projekt-UUID und Ihres API-Schlüssels.
Jede API-Anfrage (mit Ausnahme eingehender Webhooks) muss Ihre Projekt-UUID und eine Anfrage-Signatur enthalten. Die Signatur belegt, dass die Anfrage von Ihnen stammt und unterwegs nicht verändert wurde.
API-Schlüssel
2328.io verwendet zwei Schlüssel, die denselben Signieralgorithmus nutzen, aber unterschiedliche Endpoints abdecken:
| Schlüssel | Verwendet für |
|---|---|
| API key | Zahlungen, statische Wallets, Guthaben, Wechselkurse sowie Verifizierung von Zahlungs- und Static-Wallet-Webhooks |
| Payout API key | Alle /v1/payout/*-Endpoints und Verifizierung von Payout-Webhooks |
Beide Schlüssel finden Sie in den Projekteinstellungen unter 2328.io. Die Beispiele unten sprechen generisch von "API key" — verwenden Sie jeweils den richtigen Schlüssel für den aufgerufenen Endpoint.
Vermischen Sie die beiden Schlüssel niemals: Eine Auszahlungsanfrage mit dem regulären API-Schlüssel zu signieren (oder eine Zahlungsanfrage mit dem Payout-Schlüssel) führt zu einem Signaturfehler.
Erforderliche Header
| Header | Typ | Pflicht | Beschreibung |
|---|---|---|---|
Content-Type | string | ja | Immer application/json |
project | string | ja | Ihre Projekt-UUID |
sign | string | ja | HMAC-SHA256-Signatur der Anfrage, berechnet mit Ihrem API-Schlüssel |
User-Agent | string | ja | Identifiziert Ihre Anwendung (z. B. MyShop/1.4 (+https://myshop.example)). Anfragen ohne User-Agent können blockiert werden. |
So funktioniert die Signatur
Stellen Sie sich die Signatur als Fingerabdruck des Anfrage-Bodys vor. Sie wird wie folgt erstellt:
- Body in JSON serialisieren (kompakt — ohne zusätzliche Whitespaces).
- Dieses JSON in base64 codieren. Dieser Schritt normalisiert die Eingabe sprachübergreifend — sobald es sich um reines ASCII handelt, erzeugt jede Sprache dieselben Bytes für HMAC.
- HMAC-SHA256 des Base64-Strings mit Ihrem API-Schlüssel berechnen und das Ergebnis in hex (Kleinbuchstaben) umwandeln.
Für GET und andere Anfragetypen ohne Body signieren Sie stattdessen einen leeren String anstelle des JSON.
Die Signatur des leeren Strings ist für einen gegebenen API-Schlüssel konstant. Sie können sie zwischenspeichern, wenn Sie viele GET-Aufrufe ausführen.
Implementierungen
<?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
}Anfragen ohne Body (GET)
Für Anfragen mit leerem Body (z. B. GET /v1/payout/status/{uuid}) signieren Sie einen leeren String:
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))
}Vollständiges Anfragebeispiel
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()
}Geben Sie Ihren API-Schlüssel niemals in clientseitigem Code preis. Signieren Sie Anfragen auf Ihrem Backend. Ein durchgesickerter API-Schlüssel verschafft jedem vollen Zugriff auf Ihr Händlerkonto.
Webhook-Signaturen verifizieren
Wenn 2328.io einen Webhook an Sie sendet, läuft derselbe Algorithmus rückwärts ab:
- Ziehen Sie das Feld
signaus dem payload. - JSON-codieren Sie die übrigen Felder (kompakt, ohne Whitespace).
- Codieren Sie diesen String in base64.
- Berechnen Sie
HMAC-SHA256mit dem passenden Schlüssel. - Vergleichen Sie das Ergebnis mit der erhaltenen
signmittels eines konstantzeitigen Vergleichs (hash_equals,crypto.timingSafeEqual,hmac.compare_digest,subtle.ConstantTimeCompare,OpenSSL.fixed_length_secure_compare).
Der Signierschlüssel hängt von der Webhook-Quelle ab:
| Webhook | Schlüssel zur Verifizierung |
|---|---|
Zahlungs- / Static-Wallet-Webhooks (/v1/payment, /v1/static-wallet) | API key |
Payout-Webhooks (/v1/payout) | Payout API key |
Häufige Verifizierungsfehler. Ihr JSON-Encoder muss exakt dieselben Bytes erzeugen wie der Absender — andernfalls unterscheidet sich das Base64 und die Signatur passt nicht.
- Go: Verwenden Sie
json.NewEncodermitSetEscapeHTML(false). Das Standard-json.Marshalescaped<,>,&zu<und zerstört die Signatur. - Python: Übergeben Sie
ensure_ascii=Falseanjson.dumps. Ohne dies werden Nicht-ASCII-Zeichen (Kyrillisch, Chinesisch, …) zu\uXXXXescaped. - Kompaktes JSON: keine Leerzeichen zwischen den Feldern (
separators=(",", ":")in Python). - Feldreihenfolge (Go): Ein einfaches
map[string]anyrandomisiert die Schlüssel beim erneuten Kodieren. Verwenden Siejson.RawMessage, ein geordnetes struct oder entfernen Siesignaus den Rohbytes.
Wenn die Verifizierung weiterhin fehlschlägt, führen Sie apiSign selbst auf dem Payload aus — es muss dieselbe Hex-Zeichenkette erzeugen wie das empfangene sign.
Eine gültige Signatur verhindert keine Replays. Sie beweist nur, dass der Webhook von 2328.io stammt — sie verhindert nicht, dass ein Angreifer einen abgefangenen Webhook später erneut postet. Prüfen Sie immer die Idempotenz per uuid (oder txid bei statischen Wallets), bevor Sie Geldmittel gutschreiben. Lehnen Sie mit HTTP 401 ab, wenn die Signatur fehlt oder falsch ist.
Vollständige Codebeispiele finden Sie auf Webhook Notifications. Wiederholungsverhalten und Idempotenzregeln stehen in Best Practices.