Développeurs/Démarrer
Authentification
Chaque appel à l'API contient votre key ID et une signature. La signature prouve que l'appel vient de vous, et personne ne peut le modifier ni le rejouer. Votre secret de signature lui-même n'est jamais envoyé.
Voir aussi : Catalogue et commandes · Sécurité · README
#Identifiants de connexion
| Identifiant | Exemple | Secret ? |
|---|---|---|
| Key ID | ck_live_... | Non. Il indique quelle clé vous utilisez. |
| Secret de signature | cs_live_... | Oui. Vous signez avec. Il ne quitte jamais votre serveur. |
| Merchant ID | M00000001 | Non. Facultatif dans les appels à l'API. |
- Un Owner crée les clés dans le Portail (voir Clés API).
- Le Portail affiche le secret de signature une seule fois, à la création de la clé. Personne ne peut l'afficher à nouveau, pas même CardV.
- Les clés Live commencent par
ck_live_etcs_live_. Les clés Sandbox commencent parck_test_etcs_test_. Une clé d'un environnement ne fonctionne jamais dans l'autre. - L'API ne fonctionne qu'après la validation de votre entreprise par CardV.
GET/accountaffiche alors"api_access_enabled": true.
#En-têtes
Envoyez ces quatre en-têtes à chaque appel, y compris les appels GET :
X-Key-Id: ck_live_xxxxxxxxxxxxxxxxxxxxxxxx
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89X-Key-Idest l'identifiant public de votre clé.X-Timestampest l'heure Unix actuelle, en secondes. Elle ne doit pas s'écarter de plus de 5 minutes (300 secondes) de l'horloge de CardV.X-Nonceest une valeur aléatoire que vous n'utilisez jamais deux fois. Prenez 32 caractères hexadécimaux aléatoires.X-Signatureest expliqué dans Signer une requête. Il doit être en hexadécimal minuscule.
Vous pouvez aussi envoyer X-Merchant-Id. Dans ce cas, il doit correspondre à la clé.
#Signer une requête
Convertissez le corps de votre requête en octets une seule fois. Ce sont exactement ces octets que vous signerez et enverrez. Un appel
GETa un corps vide.Calculez l'empreinte du corps :
BODY_HASH= SHA-256 du corps, en hexadécimal minuscule. Pour un corps vide, c'este3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.Assemblez ces cinq lignes avec un saut de ligne (
\n), sans saut de ligne à la fin :<METHOD> <PATH with query string> <X-Timestamp> <X-Nonce> <BODY_HASH>METHODest en majuscules, par exempleGETouPOST. Le chemin inclut la chaîne de requête exactement comme vous l'envoyez, par exemple/api/v1/skus?limit=50.Signez ce texte :
X-Signature= HMAC-SHA256 de ce texte, avec votre secret de signature comme clé. Écrivez le résultat en hexadécimal minuscule.
Les appels de recharge mobile se signent de la même façon, avec leur propre chemin, par exemple /api/v1/recharge/orders.
L'erreur la plus fréquente consiste à signer une version du JSON et à en envoyer une autre.
Par exemple, {"a":1} et {"a": 1} ne sont pas les mêmes octets.
Vérifiez que votre bibliothèque HTTP ne modifie pas le corps et ne réordonne pas la chaîne de requête après la signature.
Vecteurs de test. Vérifiez votre code avec ces valeurs. Le secret est fictif.
Signing secret cs_test_TEST_ONLY_not_a_real_secret_0123456789abcdef
X-Timestamp 1790000000
X-Nonce 0123456789abcdef0123456789abcdefUn appel GET :
Method GET
Path /api/v1/skus?limit=50
BODY_HASH e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Signature 58f0362c355a6624fff9f0b84d47591c570263908832294375b7bf27731628e5Un appel POST vers /api/v1/orders avec ce corps (106 octets, sur une ligne, sans saut de ligne à la fin) :
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.25"}]}BODY_HASH 5814ffcdadfa9eb69348150f9fd6895c00b4c37cf6cd0202b83174354645023e
X-Signature e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89#Exemples de code
Tous les exemples donnent les résultats des vecteurs de test. Chargez le secret depuis votre coffre à secrets, jamais depuis le code.
cURL (bash + OpenSSL)
BASE_URL="https://sandbox.cardv.net"
REQ_PATH="/api/v1/orders" # do not call this variable PATH
BODY='{"external_order_id":"SHOP-10001","items":'
BODY+='[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.25"}]}'
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.*= //')
SIG=$(printf 'POST\n%s\n%s\n%s\n%s' "$REQ_PATH" "$TS" "$NONCE" "$BODY_HASH" \
| openssl dgst -sha256 -hmac "$CARDV_SIGNING_SECRET" -hex | sed 's/^.*= //')
curl -sS -X POST "$BASE_URL$REQ_PATH" \
-H "Content-Type: application/json" \
-H "X-Key-Id: $CARDV_KEY_ID" \
-H "X-Timestamp: $TS" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIG" \
--data-raw "$BODY"Utilisez --data-raw et non -d @file. -d supprime les sauts de ligne : les octets envoyés ne correspondraient plus.
Pour un appel GET, signez GET, le chemin avec sa chaîne de requête et l'empreinte du corps vide, et n'envoyez pas de corps.
Python (requests)
import hashlib, hmac, json, os, secrets, time
import requests
BASE_URL = "https://sandbox.cardv.net"
KEY_ID = os.environ["CARDV_KEY_ID"]
SIGNING_SECRET = os.environ["CARDV_SIGNING_SECRET"]
def signed_headers(method: str, path: str, body: bytes) -> dict:
ts, nonce = str(int(time.time())), secrets.token_hex(16)
body_hash = hashlib.sha256(body).hexdigest()
text = "\n".join([method, path, ts, nonce, body_hash])
signature = hmac.new(SIGNING_SECRET.encode(), text.encode(), hashlib.sha256).hexdigest()
return {"X-Key-Id": KEY_ID, "X-Timestamp": ts, "X-Nonce": nonce, "X-Signature": signature}
def cardv_get(path: str) -> requests.Response:
# Put the query string in `path` so the signed path and the sent path are identical.
return requests.get(BASE_URL + path, headers=signed_headers("GET", path, b""), timeout=30)
def cardv_post(path: str, payload: dict) -> requests.Response:
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode()
headers = {**signed_headers("POST", path, body), "Content-Type": "application/json"}
return requests.post(BASE_URL + path, data=body, headers=headers, timeout=30)
resp = cardv_post("/api/v1/orders", {
"external_order_id": "SHOP-10001",
"items": [{"sku_id": "S000001", "quantity": 1, "expected_unit_price": "9.25"}],
})
print(resp.status_code, resp.json())
order_id = resp.json()["order"]["order_id"]
print(cardv_get(f"/api/v1/orders/{order_id}").json()["status"])Node.js 18+ (fetch intégré)
import crypto from "node:crypto";
const BASE_URL = "https://sandbox.cardv.net";
const { CARDV_KEY_ID, CARDV_SIGNING_SECRET } = process.env;
function signedHeaders(method, path, body) {
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const text = [method, path, timestamp, nonce, bodyHash].join("\n");
const signature = crypto.createHmac("sha256", CARDV_SIGNING_SECRET).update(text, "utf8").digest("hex");
return { "X-Key-Id": CARDV_KEY_ID, "X-Timestamp": timestamp, "X-Nonce": nonce, "X-Signature": signature };
}
async function cardvGet(path) {
const res = await fetch(BASE_URL + path, { headers: signedHeaders("GET", path, Buffer.alloc(0)) });
return { status: res.status, body: await res.json() };
}
async function cardvPost(path, payload) {
const body = Buffer.from(JSON.stringify(payload), "utf8"); // serialize once
const headers = { ...signedHeaders("POST", path, body), "Content-Type": "application/json" };
const res = await fetch(BASE_URL + path, { method: "POST", headers, body });
return { status: res.status, body: await res.json() };
}
console.log(await cardvPost("/api/v1/orders", {
external_order_id: "SHOP-10001",
items: [{ sku_id: "S000001", quantity: 1, expected_unit_price: "9.25" }],
}));
console.log(await cardvGet("/api/v1/balance"));PHP (signature uniquement)
<?php
function cardv_sign(
string $secret, string $method, string $path, string $body, string $ts, string $nonce
): string {
$text = implode("\n", [$method, $path, $ts, $nonce, hash('sha256', $body)]);
return hash_hmac('sha256', $text, $secret); // lowercase hex
}
// Send exactly $body. For a GET call use $body = ''.
$body = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ts = (string) time();
$nonce = bin2hex(random_bytes(16));
$signature = cardv_sign($signingSecret, 'POST', '/api/v1/orders', $body, $ts, $nonce);
// Headers: X-Key-Id, X-Timestamp, X-Nonce, X-Signature#Erreurs
Toutes ces erreurs renvoient HTTP 403 (et non 401), sauf la limite de requêtes, qui renvoie 429. Le corps de la réponse ressemble à ceci :
{"detail": "Invalid HMAC signature."}| Problème | Que faire |
|---|---|
| Key ID inconnu ou désactivé | Vérifiez X-Key-Id et l'environnement (Live ou Sandbox). |
| Accès API non activé | Attendez que CardV valide votre entreprise. |
| L'IP de votre serveur n'est pas autorisée | Ajoutez-la à la liste d'IP autorisées dans le Portail. |
| Endpoint réservé au Portail | Passez par le Portail. Seuls seize endpoints fonctionnent avec une clé. |
| Signature absente ou incorrecte | Corrigez votre code de signature. Testez-le avec les vecteurs de test. |
| Horodatage trop ancien ou trop avancé | Synchronisez l'horloge de votre serveur (NTP). |
| Nonce déjà utilisé | Utilisez un nouveau nonce aléatoire à chaque requête. |
| Trop de requêtes (429) | Attendez le nombre de secondes indiqué dans Retry-After, puis réessayez. |
Important :
- Quand vous renvoyez une requête, générez un nouvel horodatage, un nouveau nonce et une nouvelle signature. Gardez le même corps. Voir les renvois sans risque.
- Un nonce est consommé même si la signature était fausse.
- CardV enregistre les tentatives échouées et alerte son équipe si elles sont nombreuses.
#Liste d'IP autorisées
La liste d'IP autorisées réserve l'usage de votre clé à vos seuls serveurs. Vous la gérez dans le Portail (Owner).
- Elle est facultative. Sans règle, les appels sont acceptés depuis n'importe quelle IP.
- Dès qu'il existe une règle, les appels venant d'autres IP reçoivent HTTP 403.
- Les règles ont la forme
203.0.113.10/32(IPv4) ou2001:db8::/48(IPv6). - Elle ne concerne que les appels faits avec une clé API, pas la connexion au Portail.
- Ajoutez toutes les IP de vos serveurs avant de l'activer, y compris les passerelles NAT et les régions de secours.
#Clés API
Une clé donne accès exactement aux seize endpoints listés dans L'API en bref. Elle permet notamment de passer des commandes et de lire les codes : protégez le secret de signature comme le mot de passe d'un Owner.
Seul un Owner peut gérer les clés, dans le Portail, sous Integrations → API keys. Faites-le séparément en Live et en Sandbox.
- Créez une clé et donnez-lui un nom. Le Portail affiche le key ID et le secret de signature. Copiez tout de suite le secret dans votre coffre à secrets. Il n'est affiché que cette fois-ci.
- Régénérez le secret si vous le perdez. Le key ID reste le même. L'ancien secret cesse de fonctionner immédiatement : déployez donc tout de suite le nouveau sur tous vos serveurs.
- Désactivez une clé dont vous n'avez plus besoin. C'est immédiat et définitif. Vous pouvez ensuite retirer la clé désactivée de la liste. CardV conserve son historique.
Pour changer de clé sans interruption de service :
- Créez une nouvelle clé et enregistrez son secret.
- Déployez-la sur tous vos serveurs.
- Vérifiez dans le Portail que l'ancienne clé n'est plus utilisée.
- Désactivez l'ancienne clé.
Changez de clé au moins une fois par an, et chaque fois qu'une personne qui y avait accès s'en va. Si une clé a pu fuiter, désactivez-la d'abord, puis cherchez la cause.
#Anciennes clés
Les clés créées avant octobre 2026 commencent par cvb2b_ et utilisent les anciens en-têtes
X-Merchant-Id et X-Api-Key, avec une signature uniquement sur les appels POST.
L'ancienne clé circule avec chaque requête : elle risque donc davantage de fuiter.
Live et Sandbox n'acceptent plus ces clés. Passez dès maintenant à une nouvelle clé : créez-en une dans le Portail, adaptez votre code aux en-têtes ci-dessus, puis désactivez l'ancienne clé.
Une question sur votre intégration ? Écrivez à [email protected] en indiquant votre Merchant ID et l’identifiant de commande ou de requête.