Entwickler/Erste Schritte

Authentifizierung

Jede API-Anfrage enthält Ihre Key-ID und eine Signatur. Die Signatur beweist, dass die Anfrage von Ihnen kommt, und niemand kann sie verändern oder wiederholen. Ihr Signing Secret selbst wird nie gesendet.

Siehe auch: Produkte und Bestellungen · Sicherheit · Übersicht

#Zugangsdaten

ZugangsdatenBeispielGeheim?
Key-IDck_live_...Nein. Sie zeigt, welchen Key Sie verwenden.
Signing Secretcs_live_...Ja. Damit signieren Sie. Es verlässt nie Ihren Server.
Merchant IDM00000001Nein. Bei API-Anfragen optional.
  • Ein Owner erstellt Keys im Portal (siehe API-Keys).
  • Das Portal zeigt das Signing Secret einmal an, beim Erstellen des Keys. Niemand kann es erneut anzeigen, auch CardV nicht.
  • Live-Keys beginnen mit ck_live_ und cs_live_. Sandbox-Keys beginnen mit ck_test_ und cs_test_. Ein Key aus der einen Umgebung funktioniert nie in der anderen.
  • Die API funktioniert erst, wenn CardV Ihr Unternehmen freigegeben hat. Dann zeigt GET/account den Wert "api_access_enabled": true.

#Header

Senden Sie bei jeder Anfrage diese vier Header, auch bei GET-Anfragen:

HTTP
X-Key-Id: ck_live_xxxxxxxxxxxxxxxxxxxxxxxx
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89
  • X-Key-Id ist die öffentliche ID Ihres Keys.
  • X-Timestamp ist die aktuelle Unix-Zeit in Sekunden. Sie darf höchstens 5 Minuten (300 Sekunden) von der Uhr von CardV abweichen.
  • X-Nonce ist ein Zufallswert, den Sie nie zweimal verwenden. Nehmen Sie 32 zufällige Hex-Zeichen.
  • X-Signature wird unter Anfrage signieren erklärt. Sie muss in Hex-Kleinbuchstaben geschrieben sein.

Sie können zusätzlich X-Merchant-Id senden. Dann muss der Wert zum Key passen.

#Anfrage signieren

  1. Wandeln Sie Ihren Anfrage-Body einmal in Bytes um. Genau diese Bytes signieren und senden Sie. Eine GET-Anfrage hat einen leeren Body.

  2. Bilden Sie den Hash des Bodys: BODY_HASH = SHA-256 des Bodys, in Hex-Kleinbuchstaben. Für einen leeren Body ist das e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

  3. Verbinden Sie diese fünf Zeilen mit einem Zeilenumbruch (\n), ohne Zeilenumbruch am Ende:

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

    METHOD steht in Großbuchstaben, zum Beispiel GET oder POST. Der Pfad enthält den Query-String genau so, wie Sie ihn senden, zum Beispiel /api/v1/skus?limit=50.

  4. Signieren Sie den Text: X-Signature = HMAC-SHA256 dieses Textes, mit Ihrem Signing Secret als Schlüssel. Schreiben Sie das Ergebnis in Hex-Kleinbuchstaben.

Die Anfragen zur Handy-Aufladung werden genauso signiert, mit ihrem eigenen Pfad, zum Beispiel /api/v1/recharge/orders.

Der häufigste Fehler: Man signiert eine Fassung des JSON und sendet eine andere. {"a":1} und {"a": 1} sind zum Beispiel unterschiedliche Bytes. Achten Sie darauf, dass Ihre HTTP-Bibliothek den Body nach dem Signieren nicht verändert und den Query-String nicht umsortiert.

Testvektoren. Prüfen Sie Ihren Code mit diesen Werten. Das Secret ist nicht echt.

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

Eine GET-Anfrage:

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

Eine POST-Anfrage an /api/v1/orders mit diesem Body (106 Bytes, eine Zeile, ohne Zeilenumbruch am Ende):

JSON
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.25"}]}
Text
BODY_HASH    5814ffcdadfa9eb69348150f9fd6895c00b4c37cf6cd0202b83174354645023e
X-Signature  e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89

#Codebeispiele

Alle Beispiele liefern die Ergebnisse der Testvektoren. Laden Sie das Secret aus Ihrem Secret Store, nicht aus dem 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"

Verwenden Sie --data-raw, nicht -d @file. -d entfernt Zeilenumbrüche, dann passen die gesendeten Bytes nicht mehr zur Signatur. Für eine GET-Anfrage signieren Sie GET, den Pfad mit Query-String und den Hash des leeren Bodys, und senden keinen Body.

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+ (eingebautes fetch)

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 (nur Signieren)

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

#Fehler

Alle folgenden Fehler liefern HTTP 403 (nicht 401). Nur das Anfragelimit liefert 429. Der Body sieht so aus:

JSON
{"detail": "Invalid HMAC signature."}
ProblemWas tun
Key-ID unbekannt oder deaktiviertPrüfen Sie X-Key-Id und die Umgebung (Live oder Sandbox).
API-Zugang nicht freigeschaltetWarten Sie, bis CardV Ihr Unternehmen freigegeben hat.
Die IP Ihres Servers ist nicht erlaubtTragen Sie sie im Portal in die IP-Allowlist ein.
Endpunkt nur im Portal verfügbarNutzen Sie das Portal. Nur sechzehn Endpunkte funktionieren mit einem Key.
Signatur fehlt oder ist falschKorrigieren Sie Ihren Signaturcode. Testen Sie ihn mit den Testvektoren.
Zeitstempel zu alt oder zu neuSynchronisieren Sie die Uhr Ihres Servers (NTP).
Nonce schon verwendetNehmen Sie für jede Anfrage eine neue zufällige Nonce.
Zu viele Anfragen (429)Warten Sie die Sekunden aus Retry-After ab und versuchen Sie es dann erneut.

Wichtig:

  • Wenn Sie eine Anfrage erneut senden, erzeugen Sie einen neuen Zeitstempel, eine neue Nonce und eine neue Signatur. Der Body bleibt gleich. Siehe sicher erneut senden.
  • Eine Nonce ist auch dann verbraucht, wenn die Signatur falsch war.
  • CardV protokolliert fehlgeschlagene Versuche und alarmiert bei auffällig vielen sein Team.

#IP-Allowlist

Mit der IP-Allowlist können nur Ihre eigenen Server Ihren Key nutzen. Sie verwalten sie im Portal (Owner).

  • Sie ist optional. Ohne Regeln werden Anfragen von jeder IP angenommen.
  • Sobald es mindestens eine Regel gibt, erhalten Anfragen von anderen IPs HTTP 403.
  • Regeln sehen so aus: 203.0.113.10/32 (IPv4) oder 2001:db8::/48 (IPv6).
  • Sie gilt nur für Anfragen mit API-Key, nicht für die Anmeldung im Portal.
  • Tragen Sie vor dem Einschalten alle Server-IPs ein, auch NAT-Gateways und Ausweich-Regionen.

#API-Keys

Ein Key kann genau die sechzehn Endpunkte aus Die API im Überblick nutzen. Dazu gehören Bestellen und Codes abrufen. Schützen Sie das Signing Secret daher wie ein Owner-Passwort.

Nur ein Owner kann Keys verwalten, im Portal unter Integrations → API keys. Das erledigen Sie für Live und Sandbox jeweils getrennt.

  1. Erstellen: Legen Sie einen Key an und geben Sie ihm einen Namen. Das Portal zeigt die Key-ID und das Signing Secret. Kopieren Sie das Secret sofort in Ihren Secret Store. Es wird nur dieses eine Mal angezeigt.
  2. Secret neu generieren: Wenn Sie das Signing Secret verlieren, generieren Sie es neu. Die Key-ID bleibt gleich. Das alte Secret funktioniert sofort nicht mehr. Verteilen Sie das neue Secret deshalb umgehend auf alle Ihre Server.
  3. Deaktivieren: Deaktivieren Sie einen Key, sobald Sie ihn nicht mehr brauchen. Das wirkt sofort und endgültig. Danach können Sie den deaktivierten Key aus der Liste entfernen. CardV bewahrt seinen Verlauf auf.

So wechseln Sie Keys ohne Ausfall:

  1. Erstellen Sie einen neuen Key und speichern Sie sein Secret.
  2. Verteilen Sie ihn auf alle Ihre Server.
  3. Prüfen Sie im Portal, dass der alte Key nicht mehr benutzt wird.
  4. Deaktivieren Sie den alten Key.

Wechseln Sie Keys mindestens einmal im Jahr und immer dann, wenn jemand mit Zugriff das Unternehmen verlässt. Wenn ein Key möglicherweise bekannt geworden ist, deaktivieren Sie ihn zuerst und untersuchen Sie den Fall danach.

#Alte Keys

Keys, die vor Oktober 2026 erstellt wurden, beginnen mit cvb2b_ und nutzen die älteren Header X-Merchant-Id und X-Api-Key, mit einer Signatur nur bei POST-Anfragen. Der alte Key wird bei jeder Anfrage mitgesendet und kann daher leichter bekannt werden.

Live und Sandbox akzeptieren diese Keys nicht mehr. Wechseln Sie jetzt zu einem neuen Key: Erstellen Sie einen im Portal, stellen Sie Ihren Code auf die oben genannten Header um und deaktivieren Sie dann den alten Key.

Fragen zur Integration? Schreiben Sie an [email protected] und nennen Sie Ihre Merchant ID sowie die Bestell- oder Request-ID.