Signierte Anfragen
Anfragen mit einer HMAC-Signatur schützen.
Ein API-Schlüssel allein ist ein Passwort, das mit jeder Anfrage mitreist. Leakt er, etwa über eine Logdatei oder ein falsch konfiguriertes Plugin, kann jeder, der ihn hat, Conversions für dein Programm melden. Auf dieser Seite lernst du, wie du jede Anfrage mit einem separaten Signatur-Secret signierst, das deinen Server nie verlässt, und wie cli.gs danach alles Unsignierte ablehnt.
Kurz gesagt:
- Das Signatur-Secret kommt mit deinem ersten API-Schlüssel und wird nur einmal angezeigt.
- Jede Anfrage trägt drei Header: einen Zeitstempel, eine einmalige Nonce und eine HMAC-SHA256-Signatur.
- Signiere genau die Bytes, die du sendest. Die meisten Signaturfehler entstehen durch neu kodierte Bodys.
- Teste zuerst mit signierten Anfragen, dann setze den Haken bei Unsignierte Anfragen ablehnen.
Woher das Secret kommt
Das Signatur-Secret wird zusammen mit deinem ersten API-Schlüssel angezeigt. Du findest es im Abschnitt API-Schlüssel auf der Integrationsseite, mit der Beschriftung Signatur-Secret. Wie der Schlüssel wird es nur einmal angezeigt. Speichere es neben dem Schlüssel in der Secret-Konfiguration deines Shops.

Erzeugst du später einen neuen Schlüssel, bleibt das Signatur-Secret gleich. Anfragen bleiben gültig, während du den Schlüssel wechselst.
Die drei Header
Eine signierte Anfrage trägt drei zusätzliche Header:
| Header | Wert |
|---|---|
X-Cligs-Timestamp | Aktuelle Zeit in Unix-Sekunden, z. B. 1790000000 |
X-Cligs-Nonce | Ein zufälliger Einmalwert, 8 bis 128 Zeichen (Buchstaben, Ziffern und . _ ~ : + / = -) |
X-Cligs-Signature | Die HMAC-SHA256-Signatur als Hex in Kleinbuchstaben |
Eine Nonce ist ein Wert, den du nur einmal verwendest. Sie verhindert, dass eine abgefangene Anfrage erneut abgespielt wird.
Was signiert wird
Die Signatur wird über fünf Zeilen berechnet, verbunden mit einem Zeilenumbruch (\n):
POST
/api/v1/track/conversion
1790000000
3f9c2b7e41d04a8b9e6f0c1d2a3b4c5d
<hex SHA-256 of the exact request body>Die fünf Zeilen sind: die HTTP-Methode in Großbuchstaben, der Pfad ohne Host und Query-String, der Zeitstempel, die Nonce und der SHA-256-Hash des Bodys als Hex. Der HMAC-Schlüssel ist das Signatur-Secret genau so, wie es angezeigt wurde, als einfacher String.
Wann wir eine Anfrage akzeptieren
- Der Zeitstempel weicht höchstens fünf Minuten von unserer Uhr ab. Synchronisiere die Uhr deines Servers per NTP.
- Die Nonce wurde von deinem Programm noch nie verwendet.
- Die Signatur passt Byte für Byte zu dem Body, den wir empfangen haben.
Helfer für Node.js
import crypto from "node:crypto";
const BASE_URL = "https://cli.gs";
const API_KEY = process.env.CLIGS_API_KEY;
const SIGNING_SECRET = process.env.CLIGS_SIGNING_SECRET;
// Returns the three X-Cligs-* headers for one request.
export function cligsSignatureHeaders(method, path, body, secret = SIGNING_SECRET) {
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = crypto.createHash("sha256").update(body, "utf8").digest("hex");
const canonical = [method.toUpperCase(), path, timestamp, nonce, bodyHash].join("\n");
const signature = crypto.createHmac("sha256", secret).update(canonical, "utf8").digest("hex");
return {
"X-Cligs-Timestamp": timestamp,
"X-Cligs-Nonce": nonce,
"X-Cligs-Signature": signature,
};
}
// Sends a signed POST to the cli.gs API.
export async function cligsPost(path, payload) {
const body = JSON.stringify(payload); // sign exactly the bytes you send
const res = await fetch(BASE_URL + path, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
...cligsSignatureHeaders("POST", path, body),
},
body,
signal: AbortSignal.timeout(5000),
});
return { status: res.status, data: await res.json().catch(() => null) };
}
// Usage:
// await cligsPost("/api/v1/track/conversion", { ref, orderId: "100234", amount: "149.99", currency: "EUR" });
// await cligsPost("/api/v1/track/reversal", { orderId: "100234", reason: "RETURN" });Helfer für PHP
<?php
// Returns the three X-Cligs-* headers for one request, ready for CURLOPT_HTTPHEADER.
function cligs_signature_headers(string $method, string $path, string $body, string $secret): array {
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$canonical = implode("\n", [strtoupper($method), $path, $timestamp, $nonce, hash('sha256', $body)]);
$signature = hash_hmac('sha256', $canonical, $secret);
return [
'X-Cligs-Timestamp: ' . $timestamp,
'X-Cligs-Nonce: ' . $nonce,
'X-Cligs-Signature: ' . $signature,
];
}
// Sends a signed POST to the cli.gs API.
function cligs_post(string $path, array $payload): array {
$body = json_encode($payload); // sign exactly the bytes you send
$ch = curl_init('https://cli.gs' . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => array_merge([
'Authorization: Bearer ' . getenv('CLIGS_API_KEY'),
'Content-Type: application/json',
], cligs_signature_headers('POST', $path, $body, getenv('CLIGS_SIGNING_SECRET'))),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return ['status' => $status, 'body' => json_decode((string) $response, true)];
}In WordPress übergibst du dieselben Header als assoziatives Array an wp_remote_post(). Rufe wp_json_encode() nur einmal auf und verwende das Ergebnis für Body und Signatur.
„Unsignierte Anfragen ablehnen“ einschalten
Bis du es einschaltest, ist Signieren optional. Eine Anfrage mit Signatur wird geprüft. Eine Anfrage ohne Signatur wird allein mit dem API-Schlüssel angenommen. Sobald dein Shop jede Anfrage signiert:
- Sende ein paar signierte Test-Conversions und prüfe, dass sie angenommen werden.
- Setze unter Signierte Anfragen den Haken bei Unsignierte Anfragen ablehnen.
- Klicke auf Speichern.
Ab dann wird jede Conversion- und Storno-Anfrage ohne gültige Signatur abgelehnt.
Wenn eine Signatur abgelehnt wird
Ein Signaturproblem wird mit HTTP 401 und einem kurzen Grund in error beantwortet:
error | Ursache |
|---|---|
signature required | Unsignierte Anfragen ablehnen ist an, und die Anfrage hatte keine Signatur |
stale or missing timestamp | Zeitstempel fehlt, ist nicht in Sekunden oder weicht mehr als fünf Minuten ab |
invalid nonce | Nonce fehlt, ist zu kurz, zu lang oder enthält andere Zeichen |
nonce already used | Dieselbe Nonce kam zweimal. Erzeuge für jede Anfrage eine neue, auch bei Wiederholungen |
invalid signature | Die Signatur passt nicht |
no signing secret issued for this programme | Du hast eine Signatur gesendet, aber das Programm hat kein Secret |
Die meisten invalid signature-Fehler entstehen, weil etwas anderes signiert als gesendet wird. Typische Ursachen: JSON nach dem Signieren neu kodiert, die volle URL statt des Pfads signiert, oder ein Framework fügt dem Body Leerzeichen hinzu. Erzeuge den Body-String einmal und verwende ihn für beides.
Weiter
Mit signierten Anfragen geht es weiter zu Rückgaben und Stornos.