Authentification et signature des requêtes
Signez les requêtes API avec HMAC-SHA256 en utilisant votre project UUID et votre API key.
Chaque requête API (à l'exception des webhooks entrants) doit comporter votre project UUID et une signature de requête. La signature prouve que la requête provient bien de vous et que personne ne l'a modifiée en chemin.
Clés API
2328.io utilise deux clés qui partagent le même algorithme de signature mais couvrent des endpoints différents :
| Clé | Utilisée pour |
|---|---|
| API key | Paiements, portefeuilles statiques, solde, taux de change et vérification des webhooks de paiement / portefeuille statique |
| Payout API key | Tous les endpoints /v1/payout/* et la vérification des webhooks de retrait |
Les deux clés se trouvent dans les paramètres de votre projet sur 2328.io. Les exemples ci-dessous utilisent « API key » de manière générique — remplacez-la par la clé appropriée pour l'endpoint que vous appelez.
Ne mélangez jamais les deux clés : signer une requête de retrait avec l'API key classique (ou une requête de paiement avec la Payout API key) renvoie une erreur de signature.
Headers requis
| Header | Type | Requis | Description |
|---|---|---|---|
Content-Type | string | oui | Toujours application/json |
project | string | oui | UUID de votre projet |
sign | string | oui | Signature HMAC-SHA256 de la requête, calculée avec votre API key |
User-Agent | string | oui | Identifie votre application (p. ex. MyShop/1.4 (+https://myshop.example)). Les requêtes sans User-Agent peuvent être bloquées. |
Comment fonctionne la signature
Considérez la signature comme une empreinte du corps de la requête. Elle est construite ainsi :
- Sérialisation du corps en JSON (compact — sans espaces superflus).
- Encodage Base64 de ce JSON. Cette étape normalise l'entrée entre les langages — une fois en ASCII pur, chaque langage produit les mêmes octets pour HMAC.
- Calcul de HMAC-SHA256 de la chaîne Base64 avec votre API key, puis conversion du résultat en hex minuscule.
Pour les requêtes GET et autres types de requêtes sans corps, signez une chaîne vide à la place du JSON.
La signature de la chaîne vide est constante pour une API key donnée. Vous pouvez la mettre en cache si vous effectuez de nombreux appels GET.
Implémentations
<?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
}Requêtes sans corps (GET)
Pour les requêtes au corps vide (par exemple GET /v1/payout/status/{uuid}), signez une chaîne vide :
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))
}Exemple complet de requête
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()
}N'exposez jamais votre API key dans le code côté client. Signez les requêtes sur votre backend. Une API key divulguée donne à n'importe qui un accès complet à votre compte marchand.
Vérification des signatures de webhook
Lorsque 2328.io vous envoie un webhook, le même algorithme s'exécute en sens inverse :
- Extrayez le champ
signdu payload. - Encodez les champs restants en JSON (compact, sans espaces).
- Encodez cette chaîne en Base64.
- Calculez
HMAC-SHA256avec la clé appropriée. - Comparez-la au
signreçu en utilisant une comparaison à temps constant (hash_equals,crypto.timingSafeEqual,hmac.compare_digest,subtle.ConstantTimeCompare,OpenSSL.fixed_length_secure_compare).
La clé de signature dépend de la source du webhook :
| Webhook | Clé de vérification |
|---|---|
Webhooks de paiement / portefeuille statique (/v1/payment, /v1/static-wallet) | API key |
Webhooks de retrait (/v1/payout) | Payout API key |
Pièges courants de vérification. Votre encodeur JSON doit produire exactement les mêmes octets que l'expéditeur — sinon le Base64 diffère et la signature ne correspondra pas.
- Go : utilisez
json.NewEncoderavecSetEscapeHTML(false). Lejson.Marshalpar défaut échappe<,>,&en<et casse la signature. - Python : passez
ensure_ascii=Falseàjson.dumps. Sans cela, les caractères non ASCII (cyrillique, chinois, …) sont échappés en\uXXXX. - JSON compact : pas d'espaces entre les champs (
separators=(",", ":")en Python). - Ordre des champs (Go) : un simple
map[string]anyrandomise les clés lors de la ré-encodage. Utilisezjson.RawMessage, une struct ordonnée, ou retirezsigndes octets bruts.
Si la vérification échoue toujours, exécutez apiSign sur le payload vous-même — il doit produire la même chaîne hexadécimale que le sign reçu.
Une signature valide n'empêche pas les rejeux. Elle prouve seulement que le webhook provient de 2328.io — elle n'empêche pas un attaquant de re-publier un webhook capturé plus tard. Vérifiez toujours l'idempotence via uuid (ou txid pour les wallets statiques) avant de créditer des fonds. Rejetez avec HTTP 401 si la signature est absente ou incorrecte.
Les exemples de code complets sont sur Webhook Notifications. La gestion des retentatives et les règles d'idempotence sont dans Bonnes pratiques.