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

IdentifiantExempleSecret ?
Key IDck_live_...Non. Il indique quelle clé vous utilisez.
Secret de signaturecs_live_...Oui. Vous signez avec. Il ne quitte jamais votre serveur.
Merchant IDM00000001Non. 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_ et cs_live_. Les clés Sandbox commencent par ck_test_ et cs_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/account affiche alors "api_access_enabled": true.

#En-têtes

Envoyez ces quatre en-têtes à chaque appel, y compris les appels GET :

HTTP
X-Key-Id: ck_live_xxxxxxxxxxxxxxxxxxxxxxxx
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89
  • X-Key-Id est l'identifiant public de votre clé.
  • X-Timestamp est 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-Nonce est une valeur aléatoire que vous n'utilisez jamais deux fois. Prenez 32 caractères hexadécimaux aléatoires.
  • X-Signature est 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

  1. Convertissez le corps de votre requête en octets une seule fois. Ce sont exactement ces octets que vous signerez et enverrez. Un appel GET a un corps vide.

  2. Calculez l'empreinte du corps : BODY_HASH = SHA-256 du corps, en hexadécimal minuscule. Pour un corps vide, c'est e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

  3. Assemblez ces cinq lignes avec un saut de ligne (\n), sans saut de ligne à la fin :

    Text
    <METHOD>
    <PATH with query string>
    <X-Timestamp>
    <X-Nonce>
    <BODY_HASH>

    METHOD est en majuscules, par exemple GET ou POST. Le chemin inclut la chaîne de requête exactement comme vous l'envoyez, par exemple /api/v1/skus?limit=50.

  4. 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.

Text
Signing secret  cs_test_TEST_ONLY_not_a_real_secret_0123456789abcdef
X-Timestamp     1790000000
X-Nonce         0123456789abcdef0123456789abcdef

Un appel GET :

Text
Method       GET
Path         /api/v1/skus?limit=50
BODY_HASH    e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Signature  58f0362c355a6624fff9f0b84d47591c570263908832294375b7bf27731628e5

Un appel POST vers /api/v1/orders avec ce corps (106 octets, sur une ligne, sans saut de ligne à la fin) :

JSON
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.25"}]}
Text
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)

Shell
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)

Python
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é)

Node.js
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
<?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 :

JSON
{"detail": "Invalid HMAC signature."}
ProblèmeQue 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éeAjoutez-la à la liste d'IP autorisées dans le Portail.
Endpoint réservé au PortailPassez par le Portail. Seuls seize endpoints fonctionnent avec une clé.
Signature absente ou incorrecteCorrigez 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) ou 2001: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.

  1. 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.
  2. 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.
  3. 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 :

  1. Créez une nouvelle clé et enregistrez son secret.
  2. Déployez-la sur tous vos serveurs.
  3. Vérifiez dans le Portail que l'ancienne clé n'est plus utilisée.
  4. 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.