Authenticatie en ondertekening van verzoeken
Onderteken API-verzoeken met HMAC-SHA256 met behulp van je project UUID en API key.
Elk API-verzoek (behalve inkomende webhooks) moet je project UUID en een handtekening van het verzoek bevatten. De handtekening bewijst dat het verzoek van jou afkomstig is en dat niemand het onderweg heeft gewijzigd.
API keys
2328.io gebruikt twee keys die hetzelfde ondertekeningsalgoritme delen, maar verschillende endpoints bestrijken:
| Key | Gebruikt voor |
|---|---|
| API key | Betalingen, statische wallets, saldo, wisselkoersen en verificatie van betaling- / statische-wallet-webhooks |
| Payout API key | Alle /v1/payout/*-endpoints en verificatie van uitbetalingswebhooks |
Beide keys staan in je projectinstellingen op 2328.io. De voorbeelden hieronder zeggen generiek "API key" — vervang door de juiste key voor het endpoint dat je aanroept.
Meng de twee keys nooit: een uitbetalingsverzoek ondertekenen met de gewone API key (of een betalingsverzoek met de payout key) levert een handtekeningfout op.
Vereiste headers
| Header | Type | Vereist | Beschrijving |
|---|---|---|---|
Content-Type | string | ja | Altijd application/json |
project | string | ja | Je project UUID |
sign | string | ja | HMAC-SHA256-handtekening van het verzoek, berekend met je API key |
User-Agent | string | ja | Identificeert je applicatie (bijv. MyShop/1.4 (+https://myshop.example)). Verzoeken zonder User-Agent kunnen worden geblokkeerd. |
Hoe de handtekening werkt
Zie de handtekening als een vingerafdruk van de body van het verzoek. Hij wordt opgebouwd door:
- De body te serialiseren naar JSON (compact — geen extra whitespace).
- Die JSON te encoderen in base64. Deze stap normaliseert de input over talen heen — zodra het pure ASCII is, produceert elke taal dezelfde bytes voor HMAC.
- HMAC-SHA256 te berekenen over de base64-string met je API key, en het resultaat te converteren naar lowercase hex.
Voor GET en andere verzoeken zonder body onderteken je in plaats van de JSON een lege string.
De handtekening van de lege string is constant voor een gegeven API key. Je kunt hem cachen als je veel GET-aanroepen doet.
Implementaties
<?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
}Verzoeken zonder body (GET)
Voor verzoeken zonder body (bijv. GET /v1/payout/status/{uuid}) onderteken je een lege 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))
}Volledig voorbeeld van een verzoek
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()
}Stel je API key nooit bloot in client-side code. Onderteken verzoeken op je backend. Een gelekte API key geeft iedereen volledige toegang tot je merchantaccount.
Webhook-handtekeningen verifiëren
Wanneer 2328.io je een webhook stuurt, draait hetzelfde algoritme in omgekeerde richting:
- Haal het
sign-veld uit de payload. - Encodeer de overige velden als JSON (compact, geen whitespace).
- Encodeer die string in base64.
- Bereken
HMAC-SHA256met de juiste key. - Vergelijk met de ontvangen
signmet behulp van een constant-time vergelijking (hash_equals,crypto.timingSafeEqual,hmac.compare_digest,subtle.ConstantTimeCompare,OpenSSL.fixed_length_secure_compare).
De ondertekeningskey hangt af van de bron van de webhook:
| Webhook | Key voor verificatie |
|---|---|
Betaling- / statische-wallet-webhooks (/v1/payment, /v1/static-wallet) | API key |
Uitbetalingswebhooks (/v1/payout) | Payout API key |
Veelvoorkomende verificatie-valkuilen. Je JSON-encoder moet exact dezelfde bytes produceren als de verzender — anders verschilt de Base64 en zal de handtekening niet kloppen.
- Go: gebruik
json.NewEncodermetSetEscapeHTML(false). De standaardjson.Marshalescapet<,>,&naar<en breekt de handtekening. - Python: geef
ensure_ascii=Falsemee aanjson.dumps. Zonder dit worden niet-ASCII-tekens (cyrillisch, Chinees, …) ge-escaped naar\uXXXX. - Compacte JSON: geen witruimte tussen velden (
separators=(",", ":")in Python). - Veldvolgorde (Go): een gewone
map[string]anyrandomiseert sleutels bij her-encoderen. Gebruikjson.RawMessage, een geordende struct of stripsignuit de ruwe bytes.
Als verificatie blijft falen, voer dan zelf apiSign uit op de payload — die moet dezelfde hex-string produceren als de ontvangen sign.
Een geldige handtekening voorkomt geen replays. Het bewijst alleen dat de webhook van 2328.io komt — het belet een aanvaller niet om een opgevangen webhook later opnieuw te posten. Controleer altijd idempotentie via uuid (of txid voor statische wallets) voordat je geld bijschrijft. Weiger met HTTP 401 als de handtekening ontbreekt of fout is.
Volledige codevoorbeelden staan op Webhook Notifications. Retry-afhandeling en idempotentieregels staan in Best practices.