cli.gs
Sign in

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.

API-Schlüssel und signierte Anfragen
Der Abschnitt Signierte Anfragen unter dem API-Schlüssel

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:

HeaderWert
X-Cligs-TimestampAktuelle Zeit in Unix-Sekunden, z. B. 1790000000
X-Cligs-NonceEin zufälliger Einmalwert, 8 bis 128 Zeichen (Buchstaben, Ziffern und . _ ~ : + / = -)
X-Cligs-SignatureDie 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):

text
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

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
<?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:

  1. Sende ein paar signierte Test-Conversions und prüfe, dass sie angenommen werden.
  2. Setze unter Signierte Anfragen den Haken bei Unsignierte Anfragen ablehnen.
  3. 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:

errorUrsache
signature requiredUnsignierte Anfragen ablehnen ist an, und die Anfrage hatte keine Signatur
stale or missing timestampZeitstempel fehlt, ist nicht in Sekunden oder weicht mehr als fünf Minuten ab
invalid nonceNonce fehlt, ist zu kurz, zu lang oder enthält andere Zeichen
nonce already usedDieselbe Nonce kam zweimal. Erzeuge für jede Anfrage eine neue, auch bei Wiederholungen
invalid signatureDie Signatur passt nicht
no signing secret issued for this programmeDu 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.