Autentisering och signering av förfrågningar
Signera API-förfrågningar med HMAC-SHA256 med ditt project UUID och din API key.
Varje API-förfrågan (utom inkommande webhooks) måste innehålla ditt project UUID och en signatur av förfrågan. Signaturen bevisar att förfrågan kommer från dig och att ingen har ändrat den på vägen.
API-nycklar
2328.io använder två nycklar som delar samma signeringsalgoritm men täcker olika endpoints:
| Nyckel | Används för |
|---|---|
| API key | Betalningar, statiska plånböcker, saldo, växelkurser och verifiering av betalnings- / statisk plånbok-webhooks |
| Payout API key | Alla /v1/payout/* endpoints och verifiering av uttags-webhooks |
Båda nycklarna finns i dina projektinställningar på 2328.io. Exemplen nedan säger generiskt "API key" — byt ut till rätt nyckel för den endpoint du anropar.
Blanda aldrig de två nycklarna: att signera en uttagsbegäran med den vanliga API-nyckeln (eller en betalningsbegäran med uttagsnyckeln) returnerar ett signaturfel.
Obligatoriska headers
| Header | Typ | Obligatorisk | Beskrivning |
|---|---|---|---|
Content-Type | string | ja | Alltid application/json |
project | string | ja | Ditt project UUID |
sign | string | ja | HMAC-SHA256-signatur av förfrågan, beräknad med din API key |
User-Agent | string | ja | Identifierar din applikation (t.ex. MyShop/1.4 (+https://myshop.example)). Förfrågningar utan User-Agent kan blockeras. |
Hur signaturen fungerar
Tänk på signaturen som ett fingeravtryck av förfrågans body. Den byggs så här:
- Serialisera body till JSON (kompakt — inga extra blanksteg).
- Base64-koda den JSON-strängen. Det här steget normaliserar indata mellan språk — när det väl är ren ASCII producerar varje språk samma byte för HMAC.
- Beräkna HMAC-SHA256 av Base64-strängen med din API key och konvertera sedan resultatet till hex med små bokstäver.
För GET och andra typer av förfrågningar utan body, signera en tom sträng istället för JSON.
Signaturen för en tom sträng är konstant för en given API key. Du kan cacha den om du gör många GET-anrop.
Implementationer
<?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
}Förfrågningar utan body (GET)
För förfrågningar utan body (t.ex. GET /v1/payout/status/{uuid}), signera en tom sträng:
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))
}Fullständigt exempel på förfrågan
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()
}Exponera aldrig din API key i klientkod. Signera förfrågningar i din backend. En läckt API key ger vem som helst full åtkomst till ditt handlarkonto.
Verifiera webhook-signaturer
När 2328.io skickar en webhook till dig körs samma algoritm omvänt:
- Plocka ut fältet
signur payloaden. - JSON-koda de återstående fälten (kompakt, utan blanksteg).
- Base64-koda den strängen.
- Beräkna
HMAC-SHA256med rätt nyckel. - Jämför den med den mottagna
signmed en tidskonstant jämförelse (hash_equals,crypto.timingSafeEqual,hmac.compare_digest,subtle.ConstantTimeCompare,OpenSSL.fixed_length_secure_compare).
Signeringsnyckeln beror på webhook-källan:
| Webhook | Nyckel att verifiera med |
|---|---|
Betalnings- / statisk plånbok-webhooks (/v1/payment, /v1/static-wallet) | API key |
Uttags-webhooks (/v1/payout) | Payout API key |
Vanliga verifieringsfallgropar. Din JSON-kodare måste producera exakt samma bytes som avsändaren producerade — annars skiljer sig Base64 och signaturen matchar inte.
- Go: använd
json.NewEncodermedSetEscapeHTML(false). Standardjson.Marshalescapar<,>,&till<och förstör signaturen. - Python: skicka
ensure_ascii=Falsetilljson.dumps. Utan det escapas icke-ASCII (kyrilliska, kinesiska, …) till\uXXXX. - Kompakt JSON: inga blanksteg mellan fält (
separators=(",", ":")i Python). - Fältordning (Go): en vanlig
map[string]anyrandomiserar nycklar vid omkodning. Användjson.RawMessage, en ordnad struct eller ta bortsignur de råa byten.
Om verifieringen fortsätter att misslyckas, kör apiSign på payloaden själv — den måste producera samma hex-sträng som det mottagna sign.
En giltig signatur förhindrar inte replays. Den bevisar endast att webhooken kom från 2328.io — den hindrar inte en angripare från att skicka en fångad webhook igen senare. Kontrollera alltid idempotens via uuid (eller txid för statiska plånböcker) innan du krediterar medel. Avvisa med HTTP 401 om signaturen saknas eller är fel.
Fullständiga kodexempel finns på Webhook Notifications. Återförsökshantering och idempotensregler finns i Bästa praxis.