Dokumentation / Integration

Anfragesignatur CP-HMAC

Jede Anfrage an /v1/* wird mit dem Secret des API-Schlüssels signiert. Das Secret selbst wird nie übertragen. Die fertigen Clients erledigen das für Sie – dieser Abschnitt ist für alle, die einen eigenen schreiben.

Header

HTTP
Authorization: CP-HMAC key=<key_id>, ts=<unix_seconds>, nonce=<nonce>, sig=<hex>
FeldFormat
keySchlüssel-ID: ak_ + 16–40 Zeichen [a-z0-9]
tsAktuelle Zeit in Unix-Sekunden. Zulässige Abweichung von der Serveruhr: ±5 Minuten
nonceEinmal-String [A-Za-z0-9_-]{16,64}; eine wiederverwendete Nonce wird abgelehnt
sig64 Hex-Zeichen in Kleinbuchstaben

Was signiert wird

text
string = METHOD + "\n" + PATH_WITH_QUERY + "\n" + ts + "\n" + nonce + "\n" + hex(sha256(body))
sig    = hex(HMAC-SHA256(secret, string))
  • METHOD – GET, POST usw.
  • PATH_WITH_QUERY – Pfad und Query-String exakt so, wie sie in der Anfrage gesendet werden: /v1/invoices?status=paid&limit=50. Schema und Host werden nicht signiert.
  • Der Body wird Byte für Byte signiert. Signieren Sie genau den JSON-String, den Sie senden, kein neu serialisiertes Objekt.
  • Für Anfragen ohne Body gilt hex(sha256("")) = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
sign.js
import { createHash, createHmac, randomBytes } from "node:crypto";

export function sign(keyId, secret, method, pathWithQuery, body = "") {
  const ts = Math.floor(Date.now() / 1000).toString();
  const nonce = randomBytes(16).toString("hex");
  const bodyHash = createHash("sha256").update(body).digest("hex");
  const sig = createHmac("sha256", secret)
    .update(`${method}\n${pathWithQuery}\n${ts}\n${nonce}\n${bodyHash}`)
    .digest("hex");
  return `CP-HMAC key=${keyId}, ts=${ts}, nonce=${nonce}, sig=${sig}`;
}
sign.php
function cp_sign(string $keyId, string $secret, string $method, string $pathWithQuery, string $body = ''): string {
    $ts = (string) time();
    $nonce = bin2hex(random_bytes(16));
    $str = $method . "\n" . $pathWithQuery . "\n" . $ts . "\n" . $nonce . "\n" . hash('sha256', $body);
    $sig = hash_hmac('sha256', $str, $secret);
    return "CP-HMAC key=$keyId, ts=$ts, nonce=$nonce, sig=$sig";
}
sign.py
import hashlib, hmac, secrets, time

def cp_sign(key_id: str, secret: str, method: str, path_with_query: str, body: bytes = b"") -> str:
    ts = str(int(time.time()))
    nonce = secrets.token_hex(16)
    msg = f"{method}\n{path_with_query}\n{ts}\n{nonce}\n{hashlib.sha256(body).hexdigest()}"
    sig = hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest()
    return f"CP-HMAC key={key_id}, ts={ts}, nonce={nonce}, sig={sig}"
sign.go
sig := cryptopayclient.SignRequest(secret, "POST", "/v1/invoices", ts, nonce, body)
req.Header.Set("Authorization", fmt.Sprintf("CP-HMAC key=%s, ts=%d, nonce=%s, sig=%s", keyID, ts, nonce, sig))

Referenzwerte

Prüfen Sie Ihre Implementierung – mit genau diesen Werten werden alle vier offiziellen Clients getestet.

Secrettest-secret-0123456789abcdef0123456789
Anfrage 1POST /v1/invoices, ts 1760000000, nonce nonce-0000000001, Body {"order_id":"WISH-1"}
sig 12c769f6a5be7b80871e03851ccfe84002130ec48a45348bd821752a6d45960ec
Anfrage 2GET /v1/invoices/inv_abcdefabcdefabcdef, ts 1760000000, nonce nonce-0000000002, ohne Body
sig 2a12924acf8a772c6f3fadb8874ff69e9934046ccd2fa5563b23fec3e98e1665b

Wenn Sie 401 erhalten

Jeder Signaturfehler ergibt 401 unauthorized mit identischem Text: Der Server verrät nicht, was genau falsch ist. Häufige Ursachen:

  • Die Serveruhr weicht um mehr als 5 Minuten ab – aktivieren Sie NTP;
  • ein anderer Pfad wurde signiert – ohne Query oder mit anderer Parameterreihenfolge;
  • der Body wurde nach dem Signieren neu serialisiert;
  • eine Nonce wurde wiederverwendet;
  • der Schlüssel wurde widerrufen.

403 merchant_suspended – die Signatur ist korrekt, aber der Händler ist gesperrt.

Schlüssel

  • Schlüssel erstellen Sie im Dashboard unter „Schlüssel und Webhooks“. Das Secret wird nur einmal angezeigt.
  • Bis zu 10 Schlüssel können gleichzeitig aktiv sein. Rotation: neuen Schlüssel erstellen, Server umstellen, alten widerrufen. Ein Widerruf wirkt sofort.
  • Bewahren Sie das Secret im Secret-Manager oder in einer Umgebungsvariablen des Servers auf, nicht im Code und nicht in Git.
  • Das Limit liegt bei etwa 20 Anfragen pro Sekunde je Händler, mit Puffer für Spitzen; bei Überschreitung folgt 429 rate_limited.