Postbacks an deinen Shop
Statuswechsel empfangen, Signatur prüfen, Wiederholungen behandeln.
cli.gs kann deinem Shop jede Statusänderung einer Conversion mitteilen: erfasst, freigegeben, abgelehnt oder storniert. Auf dieser Seite lernst du, wie du diese Status-Rückmeldung einrichtest, wie du ihre Signatur prüfst und wie Wiederholungen funktionieren. So folgen die Daten deines Shops unseren, ohne dass du abfragen musst, etwa um eine Bestellung im Backoffice als „Provision freigegeben“ zu markieren. Das ist optional; die Integration funktioniert auch ohne.
Kurz gesagt:
- Trage unter Status-Rückmeldung (optional) eine HTTPS-URL als Postback-URL ein und klicke auf Speichern.
- Wir senden pro Statusänderung einen signierten POST, mit denselben drei
X-Cligs-*-Headern, die du selbst verwendest. - Prüfe die Signatur über den rohen Body und antworte innerhalb von 5 Sekunden mit 2xx.
- Bei 5xx, 408, 429 oder Timeout wiederholen wir bis zu sieben Mal. Jeder andere 4xx-Status stoppt die Zustellung sofort.
Einrichten
- Öffne die Integrationsseite und scrolle zu Status-Rückmeldung (optional).
- Trage deinen Endpunkt im Feld Postback-URL ein.
- Klicke auf Speichern.
Anforderungen an die URL:
- Sie muss HTTPS verwenden.
- Sie muss auf eine öffentliche Adresse zeigen. Adressen in privaten oder internen Netzen werden abgelehnt, beim Speichern und bei jeder Zustellung.
- Weiterleitungen werden nicht verfolgt. Trage die endgültige URL ein.
- Zustellungen werden mit dem Signatur-Secret deines Programms signiert, du brauchst also eines. Es kommt mit deinem ersten API-Schlüssel: siehe Signierte Anfragen. Ohne Secret wird nichts gesendet.
- Gesendet werden nur Statusänderungen nach dem Speichern der URL. Frühere werden nicht nachgeliefert.
Ereignisse
| Ereignis | Gesendet, wenn |
|---|---|
conversion.recorded | eine Conversion erfasst wurde, auch die von deinem eigenen Postback gemeldeten und Test-Conversions |
conversion.approved | eine Conversion freigegeben wurde: automatisch nach dem Freigabefenster oder von dir unter Conversions |
conversion.rejected | du (oder unser Prüfteam) eine Conversion abgelehnt hast |
conversion.reversed | dein Shop einen Storno gemeldet hat |
Jedes Ereignis wird pro Conversion einmal gesendet. Bestätigt ein Server-Postback eine geparkte Pixel-Conversion, kommt kein neues conversion.recorded. Das Ergebnis steht schon in der Antwort auf deinen Postback.
Die Anfrage, die wir senden
POST https://shop.example.com/cligs/postback
Content-Type: application/json
User-Agent: cligs-postback/1
X-Cligs-Event: conversion.approved
X-Cligs-Timestamp: 1790000000
X-Cligs-Nonce: 9b1f0c6e2a4d4f8e8c3a7b5d1e2f3a4b
X-Cligs-Signature: 5c0d…e19a{
"event": "conversion.approved",
"conversionId": "cmg3x1q2w0001abcd9876efgh",
"orderId": "100234",
"programId": "cmf8a7b6c0002wxyz1234lmno",
"status": "APPROVED",
"eventType": "SALE",
"basketValue": "149.99",
"commission": "12",
"networkFee": "1.8",
"currency": "EUR",
"reason": null,
"test": false,
"occurredAt": "2026-09-28T14:05:00.000Z",
"sentAt": "2026-10-12T03:00:41.512Z"
}statusist der Status der Conversion zum Zeitpunkt des Ereignisses.reasonist bei Ablehnungen und Stornos gesetzt (z. B.RETURN).- Beträge sind Strings, um Rundungsfehler zu vermeiden.
test: truekennzeichnet Ereignisse zu Test-Conversions. Ignoriere sie im Livebetrieb, oder nutze sie, um diesen Endpunkt zu testen.occurredAtist der Zeitpunkt des Verkaufs.sentAtist der Zeitpunkt dieses Zustellversuchs.
Signatur prüfen
Die Signatur entsteht genauso wie die deiner eigenen Anfragen, mit denselben Headern und demselben Signatur-Secret. Signiert wird Pfad und Query-String der eingetragenen URL. Zum Beispiel /cligs/postback, oder /cligs/postback?shop=1, wenn deine URL einen Query-String hat.
- Lehne die Anfrage ab, wenn
X-Cligs-Timestampmehr als fünf Minuten von deiner Uhr abweicht. - Bilde
POST\n<Pfad>\n<Zeitstempel>\n<Nonce>\n<Hex-SHA-256 des rohen Bodys>. - Berechne HMAC-SHA256 mit deinem Signatur-Secret. Vergleiche das Ergebnis zeitkonstant mit
X-Cligs-Signature. - Optional: Merke dir Nonces fünf Minuten lang und lehne Wiederholungen ab.
Hashe immer den rohen Body, so wie er ankommt. JSON parsen und wieder serialisieren verändert die Bytes und macht die Signatur ungültig.
Node.js (Express)
import express from "express";
import crypto from "node:crypto";
const SECRET = process.env.CLIGS_SIGNING_SECRET;
// The exact URL you entered as Postback URL. Used for the signed path, so a proxy
// that rewrites paths cannot break verification.
const POSTBACK_URL = new URL("https://shop.example.com/cligs/postback");
const SIGNED_PATH = POSTBACK_URL.pathname + POSTBACK_URL.search;
const app = express();
// express.raw keeps the body as bytes: we need them unchanged for the hash.
app.post(POSTBACK_URL.pathname, express.raw({ type: "application/json" }), (req, res) => {
const body = req.body.toString("utf8");
const timestamp = req.get("X-Cligs-Timestamp") ?? "";
const nonce = req.get("X-Cligs-Nonce") ?? "";
const signature = req.get("X-Cligs-Signature") ?? "";
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(401).send("stale timestamp");
}
const bodyHash = crypto.createHash("sha256").update(body, "utf8").digest("hex");
const canonical = ["POST", SIGNED_PATH, timestamp, nonce, bodyHash].join("\n");
const expected = Buffer.from(crypto.createHmac("sha256", SECRET).update(canonical).digest("hex"), "hex");
const given = Buffer.from(signature, "hex");
if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) {
return res.status(401).send("bad signature");
}
const event = JSON.parse(body);
res.status(204).end(); // answer first ...
setImmediate(() => handleCligsEvent(event).catch(console.error)); // ... then do the work
});
async function handleCligsEvent(event) {
// Deduplicate on conversionId + event: a delivery can arrive twice.
// e.g. await db.orders.update({ id: event.orderId }, { affiliateStatus: event.status });
}
app.listen(3000);PHP
<?php
// cligs-postback.php — the endpoint behind your Postback URL.
$secret = getenv('CLIGS_SIGNING_SECRET');
$signedPath = '/cligs/postback'; // path (and query string, if any) of the URL you entered
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_CLIGS_TIMESTAMP'] ?? '';
$nonce = $_SERVER['HTTP_X_CLIGS_NONCE'] ?? '';
$signature = strtolower($_SERVER['HTTP_X_CLIGS_SIGNATURE'] ?? '');
if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
exit('stale timestamp');
}
$canonical = implode("\n", ['POST', $signedPath, $timestamp, $nonce, hash('sha256', $body)]);
$expected = hash_hmac('sha256', $canonical, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('bad signature');
}
$event = json_decode($body, true);
// Store the event quickly and process it later (queue, cron job).
// Deduplicate on conversionId + event: a delivery can arrive twice.
store_cligs_event($event['conversionId'], $event['event'], $body);
http_response_code(204);Schnell antworten, und Wiederholungen
Was wir von deinem Endpunkt erwarten
- Antworte innerhalb von 5 Sekunden mit einem beliebigen 2xx-Status. Speichere das Ereignis und verarbeite es danach. Langsame Arbeit in der Anfrage riskiert einen Timeout, und der zählt als Fehlschlag.
- Antworte bei vorübergehenden Problemen auf deiner Seite mit 5xx, nicht mit 4xx.
Wann wir wiederholen
- Bei Timeout, Netzwerkfehler, HTTP 5xx, 408 oder 429 versuchen wir es erneut nach 1 Minute, 5 Minuten, 30 Minuten, 2 Stunden, 6 Stunden und 24 Stunden. Das sind sieben Versuche über etwa anderthalb Tage. Jeder Versuch wird neu signiert, mit neuem Zeitstempel und neuer Nonce.
- Jeder andere 4xx-Status (etwa 400, 401, 404) gilt als bewusstes „Nein“. Wir hören sofort auf.
- Scheitert eine Zustellung endgültig, benachrichtigen wir dich. Jede Zustellung mit ihren Versuchen und dem letzten HTTP-Status siehst du unter An deinen Shop gesendete Postbacks auf der Integrationsseite.
Weiter
Sende eine Test-Conversion, um deinen Endpunkt von Anfang bis Ende zu prüfen: siehe Testen und Diagnose.