Webhooks
Selgeo sendet Echtzeit-HTTP-POST-Benachrichtigungen an Ihren Server, wenn Ereignisse in Ihrem Konto auftreten. Verwenden Sie Webhooks, um Workflows zu automatisieren, wie z. B. den Zugriff zu gewähren, wenn ein Partner genehmigt wird, Provisionen mit Ihrem Buchhaltungssystem zu synchronisieren oder Ihr Team über Betrug zu benachrichtigen.
Alle Webhook-Payloads verwenden API-Version v1.
Endpunkt-Registrierung
Registrieren Sie Webhook-Endpunkte auf der Seite Einstellungen > Webhooks im Händler-Dashboard.
- Klicken Sie auf Endpunkt hinzufügen.
- Geben Sie die URL ein, unter der Sie Ereignisse empfangen möchten. Live-Modus-Endpunkte müssen HTTPS verwenden. Test-Modus-Endpunkte dürfen HTTP für die lokale Entwicklung verwenden.
- Wählen Sie die Ereignisse aus, die Sie abonnieren möchten.
- Klicken Sie auf Erstellen. Ihr Signierungsgeheimnis (
whsec_...) wird einmal angezeigt – kopieren Sie es und speichern Sie es sicher.
Sie können bis zu 10 Endpunkte pro Modus (Test und Live separat) registrieren.
Test-Pings
Verwenden Sie nach der Erstellung eines Endpunkts die Schaltfläche Test-Ping senden, um die Konnektivität zu überprüfen. Der Ping sendet ein webhook.test-Ereignis an Ihre URL und meldet den HTTP-Statuscode.
Geheimnisse rotieren
Wenn Ihr Signierungsgeheimnis kompromittiert ist, verwenden Sie die Aktion Geheimnis rotieren am Endpunkt. Dadurch wird sofort ein neues Geheimnis generiert – das alte funktioniert nicht mehr. Aktualisieren Sie Ihren Verifizierungscode, bevor Sie rotieren.
Signaturverifizierung
Jede Webhook-Anfrage enthält einen X-Selgeo-Signature-Header im Format:
t=<unix_timestamp>,v1=<hmac_hex>
Der HMAC wird berechnet als HMAC-SHA256(signing_secret_bytes, "<timestamp>.<raw_json_body>"). Das Signierungsgeheimnis hat ein whsec_-Präfix, gefolgt von 64 Hex-Zeichen. Entfernen Sie das Präfix und hex-dekodieren Sie, um den 32-Byte-Schlüssel zu erhalten.
Überprüfen Sie immer die Signatur, bevor Sie die Payload verarbeiten. Dies schützt vor gefälschten Anfragen.
Verifizierungsbeispiele
- Node.js
- Python
- PHP
import crypto from 'node:crypto';
function verifyWebhookSignature(signingSecret, signatureHeader, rawBody) {
if (!signatureHeader) {
return false;
}
// whsec_-Präfix entfernen und hex-dekodieren, um rohe Schlüssel-Bytes zu erhalten
const rawSecret = signingSecret.replace(/^whsec_/, '');
const secretBytes = Buffer.from(rawSecret, 'hex');
// Header parsen: t=<ts>,v1=<hmac>
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => {
return part.split('=', 2);
})
);
const timestamp = parts.t;
const receivedHmac = parts.v1;
if (!timestamp || !receivedHmac) {
return false;
}
// Zeitstempel ablehnen, die älter als 5 Minuten oder in der Zukunft sind (mit 30s Toleranz)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300 || age < -30) {
return false;
}
// Den erwarteten HMAC berechnen
const expectedHmac = crypto
.createHmac('sha256', secretBytes)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
// Zeitkonstanter Vergleich (Buffer müssen gleiche Länge haben)
const receivedBuf = Buffer.from(receivedHmac, 'hex');
const expectedBuf = Buffer.from(expectedHmac, 'hex');
if (receivedBuf.length !== expectedBuf.length) {
return false;
}
return crypto.timingSafeEqual(receivedBuf, expectedBuf);
}
// Verwendung in einem Express-Handler
app.post('/webhooks/selgeo', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-selgeo-signature'];
const rawBody = req.body.toString();
if (!verifyWebhookSignature('whsec_YOUR_SIGNING_SECRET', signature, rawBody)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
console.log('Ereignis empfangen:', event.event_name);
// Ereignis verarbeiten...
res.status(200).send('OK');
});
import hashlib
import hmac
import time
def verify_webhook_signature(signing_secret: str, signature_header: str, raw_body: str) -> bool:
if not signature_header:
return False
# whsec_-Präfix entfernen und hex-dekodieren, um rohe Schlüssel-Bytes zu erhalten
raw_secret = signing_secret.removeprefix("whsec_")
secret_bytes = bytes.fromhex(raw_secret)
# Header parsen: t=<ts>,v1=<hmac>
try:
parts = dict(part.split("=", 1) for part in signature_header.split(","))
except ValueError:
return False
timestamp = parts.get("t")
received_hmac = parts.get("v1")
if not timestamp or not received_hmac:
return False
# Zeitstempel ablehnen, die älter als 5 Minuten oder in der Zukunft sind (mit 30s Toleranz)
try:
age = int(time.time()) - int(timestamp)
except ValueError:
return False
if age > 300 or age < -30:
return False
# Den erwarteten HMAC berechnen
message = f"{timestamp}.{raw_body}".encode()
expected_hmac = hmac.new(secret_bytes, message, hashlib.sha256).hexdigest()
# Zeitkonstanter Vergleich
return hmac.compare_digest(received_hmac, expected_hmac)
# Verwendung in einem Flask-Handler
@app.route("/webhooks/selgeo", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Selgeo-Signature", "")
raw_body = request.get_data(as_text=True)
if not verify_webhook_signature("whsec_YOUR_SIGNING_SECRET", signature, raw_body):
return "Invalid signature", 401
event = request.get_json()
print(f"Ereignis empfangen: {event['event_name']}")
# Ereignis verarbeiten...
return "OK", 200
<?php
function verifyWebhookSignature(
string $signingSecret,
string $signatureHeader,
string $rawBody
): bool {
if ($signatureHeader === '') {
return false;
}
// whsec_-Präfix entfernen und hex-dekodieren, um rohe Schlüssel-Bytes zu erhalten
$rawSecret = str_starts_with($signingSecret, 'whsec_')
? substr($signingSecret, 6)
: $signingSecret;
$secretBytes = hex2bin($rawSecret);
if ($secretBytes === false) {
return false;
}
// Header parsen: t=<ts>,v1=<hmac>
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
$segments = explode('=', $part, 2);
if (count($segments) !== 2) {
continue;
}
[$key, $value] = $segments;
$parts[$key] = $value;
}
$timestamp = $parts['t'] ?? null;
$receivedHmac = $parts['v1'] ?? null;
if (!$timestamp || !$receivedHmac) {
return false;
}
// Zeitstempel ablehnen, die älter als 5 Minuten oder in der Zukunft sind (mit 30s Toleranz)
$age = time() - (int) $timestamp;
if ($age > 300 || $age < -30) {
return false;
}
// Den erwarteten HMAC berechnen
$expectedHmac = hash_hmac('sha256', "{$timestamp}.{$rawBody}", $secretBytes);
// Zeitkonstanter Vergleich
return hash_equals($expectedHmac, $receivedHmac);
}
// Verwendung
$signature = $_SERVER['HTTP_X_SELGEO_SIGNATURE'] ?? '';
$rawBody = file_get_contents('php://input');
if (!verifyWebhookSignature('whsec_YOUR_SIGNING_SECRET', $signature, $rawBody)) {
http_response_code(401);
echo 'Invalid signature';
exit;
}
$event = json_decode($rawBody, true);
error_log("Ereignis empfangen: " . $event['event_name']);
// Ereignis verarbeiten...
http_response_code(200);
echo 'OK';
Verwenden Sie immer den rohen Anfragekörper-String für die Signaturverifizierung, nicht eine re-serialisierte Version des geparsten JSON. Re-Serialisierung kann die Schlüsselreihenfolge oder Leerzeichen ändern, was den HMAC ungültig macht.
Ereigniskatalog
Jede Webhook-Payload enthält diese Envelope-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
event_id | string | Eindeutige ID für dieses Ereignis (nanoid, 21 Zeichen) |
delivery_id | string | Eindeutige ID für diesen Zustellungsversuch |
event_name | string | Ereignistyp (siehe Tabelle unten) |
occurred_at | string | ISO 8601 Zeitstempel |
merchant_id | string | Ihre Händlerkonto-ID |
mode | "test" | "live" | Ob dieses Ereignis im Test- oder Live-Modus aufgetreten ist |
api_version | "v1" | API-Version |
Partner-Ereignisse
| Ereignis | Beschreibung |
|---|---|
participant.created | Ein neuer Partner hat sich beworben oder wurde einem Programm hinzugefügt |
participant.approved | Ein Partner wurde genehmigt (manuell oder durch Auto-Genehmigung) |
participant.rejected | Eine Partner-Bewerbung wurde abgelehnt |
participant.suspended | Ein aktiver Partner wurde gesperrt |
participant.reinstated | Ein gesperrter Partner wurde wiederhergestellt |
participant.erased | Die Daten eines Partners wurden gelöscht (DSGVO) |
participant.data_exported | Die Daten eines Partners wurden exportiert (DSGVO) |
Beispiel: participant.approved
{
"event_id": "evt_abc123def456ghi78",
"delivery_id": "dlv_xyz789abc012def34",
"event_name": "participant.approved",
"occurred_at": "2026-03-15T10:30:00.000Z",
"merchant_id": "mch_your_merchant_id",
"mode": "live",
"api_version": "v1",
"participant_id": "prt_partner_id",
"program_id": "prg_program_id",
"approved_at": "2026-03-15T10:30:00.000Z"
}
Attributionsereignisse
| Ereignis | Beschreibung |
|---|---|
attribution.created | Ein Klick wurde einem Partner zugeordnet |
attribution.converted | Ein zugeordneter Klick führte zu einer Konversion |
attribution.expired | Ein Attributionsfenster ist ohne Konversion abgelaufen |
attribution.duplicate_detected | Eine doppelte Attribution wurde erkannt |
Konversionsereignisse
| Ereignis | Beschreibung |
|---|---|
conversion.created | Eine neue Konversion wurde aufgezeichnet |
conversion.fraud_detected | Betrug wurde erkannt (Selbst-Empfehlung oder doppelte Konversion) |
Beispiel: conversion.created
{
"event_id": "evt_abc123def456ghi78",
"delivery_id": "dlv_xyz789abc012def34",
"event_name": "conversion.created",
"occurred_at": "2026-03-15T14:22:00.000Z",
"merchant_id": "mch_your_merchant_id",
"mode": "live",
"api_version": "v1",
"conversion_id": "cnv_conversion_id",
"attribution_id": "att_attribution_id",
"participant_id": "prt_partner_id",
"program_id": "prg_program_id",
"amount_cents": 9900,
"currency": "EUR",
"conversion_scope": "first_conversion",
"created_at": "2026-03-15T14:22:00.000Z"
}
Beispiel: conversion.fraud_detected
{
"event_id": "evt_abc123def456ghi78",
"delivery_id": "dlv_xyz789abc012def34",
"event_name": "conversion.fraud_detected",
"occurred_at": "2026-03-15T14:25:00.000Z",
"merchant_id": "mch_your_merchant_id",
"mode": "live",
"api_version": "v1",
"fraud_type": "self_referral",
"program_id": "prg_program_id",
"program_name": "My SaaS Affiliates",
"participant_id": "prt_partner_id",
"attribution_event_id": "att_attribution_id",
"conversion_id": null,
"external_transaction_id": "cs_live_abc123",
"masked_context": "us***@example.com",
"explanation": "Customer email matches partner email after RFC 5233 normalization",
"detected_at": "2026-03-15T14:25:00.000Z"
}
Provisionsereignisse
| Ereignis | Beschreibung |
|---|---|
commission.created | Eine neue Provision wurde berechnet |
commission.approved | Eine Provision wurde für die Auszahlung genehmigt |
commission.rejected | Eine Provision wurde abgelehnt |
commission.paid | Eine Provision wurde in eine Auszahlung einbezogen (demnächst verfügbar) |
commission.refunded | Eine Provision wurde aufgrund einer Rückerstattung storniert |
Beispiel: commission.created
{
"event_id": "evt_abc123def456ghi78",
"delivery_id": "dlv_xyz789abc012def34",
"event_name": "commission.created",
"occurred_at": "2026-03-15T14:22:05.000Z",
"merchant_id": "mch_your_merchant_id",
"mode": "live",
"api_version": "v1",
"commission_id": "cms_commission_id",
"conversion_id": "cnv_conversion_id",
"participant_id": "prt_partner_id",
"program_id": "prg_program_id",
"amount_cents": 1980,
"currency": "EUR",
"commission_type": "percentage",
"commission_rate": 20,
"needs_review": false,
"created_at": "2026-03-15T14:22:05.000Z"
}
Auszahlungsereignisse (demnächst verfügbar)
Auszahlungsereignisse werden verfügbar sein, wenn die Auszahlungsverarbeitungsfunktion startet.
| Ereignis | Beschreibung |
|---|---|
payout.created | Ein Auszahlungs-Batch wurde erstellt |
payout.initiated | Eine Auszahlungsüberweisung wurde initiiert |
payout.paid | Eine Auszahlung wurde erfolgreich überwiesen |
payout.failed | Eine Auszahlungsüberweisung ist fehlgeschlagen |
payout.retried | Eine fehlgeschlagene Auszahlung wurde wiederholt |
payout.cancelled | Eine Auszahlung wurde storniert |
Tracking-Ereignisse
| Ereignis | Beschreibung |
|---|---|
tracking_identifier.created | Ein neuer Tracking-Link wurde für einen Partner erstellt |
Promo-Code-Ereignisse
| Ereignis | Beschreibung |
|---|---|
promo_code.created | Ein Promo-Code wurde erstellt |
promo_code.stripe_synced | Ein Promo-Code wurde mit Stripe synchronisiert |
promo_code.deactivated | Ein Promo-Code wurde deaktiviert |
Programm-Einladungsereignisse
| Ereignis | Beschreibung |
|---|---|
program_invite.created | Eine Einladung wurde erstellt |
program_invite.accepted | Eine Einladung wurde von einem Partner angenommen |
program_invite.revoked | Eine Einladung wurde widerrufen |
program_invite.expired | Eine Einladung ist abgelaufen |
Wiederholungsrichtlinie
Wenn Ihr Endpunkt einen Nicht-2xx-Statuscode zurückgibt oder die Anfrage eine Zeitüberschreitung hat (30 Sekunden), wiederholt Selgeo mit exponentiellem Backoff:
| Versuch | Verzögerung nach Fehler |
|---|---|
| 1. Wiederholung | 1 Minute |
| 2. Wiederholung | 2 Minuten |
| 3. Wiederholung | 4 Minuten |
| 4. Wiederholung | 15 Minuten |
Nach 5 Gesamtversuchen (1 initial + 4 Wiederholungen) wechselt die Zustellung in den Dead-Letter-Status. Dead-Letter-Zustellungen sind im Händler-Dashboard unter dem Zustellungslog des Endpunkts sichtbar, werden aber nicht automatisch wiederholt.
Zustellungsstatus
| Status | Bedeutung |
|---|---|
pending | Für die Zustellung eingereiht |
delivered | Ihr Endpunkt hat 2xx zurückgegeben |
failed | Zustellung fehlgeschlagen, Wiederholung geplant |
dead_letter | Alle Wiederholungsversuche erschöpft |
Best Practices
- Schnell 200 zurückgeben. Bestätigen Sie den Empfang mit einem
200-Statuscode, sobald Sie den Webhook erhalten. Verarbeiten Sie das Ereignis asynchron in einem Hintergrundauftrag. Selgeo hat eine Zeitüberschreitung nach 30 Sekunden. - Duplikate behandeln. Verwenden Sie das
event_id-Feld zur Deduplizierung. Dasselbe Ereignis kann in seltenen Fällen mehr als einmal zugestellt werden (z. B. Netzwerkzeitüberschreitung direkt nachdem Ihr Server es verarbeitet hat). - Signaturen verifizieren. Überprüfen Sie immer den
X-Selgeo-Signature-Header. Verarbeiten Sie niemals unverifizierte Payloads. - Zeitstempel prüfen. Lehnen Sie Signaturen mit Zeitstempeln ab, die älter als 5 Minuten sind oder zu weit in der Zukunft liegen, um Replay-Angriffe zu verhindern.
- HTTPS in der Produktion verwenden. Live-Modus-Endpunkte erfordern HTTPS. Test-Modus-Endpunkte erlauben HTTP für die lokale Entwicklung.
- Dead-Letter-Zustellungen überwachen. Prüfen Sie das Zustellungslog im Dashboard regelmäßig. Dead-Letter-Ereignisse können auf einen Endpunktausfall oder einen Fehler in Ihrem Handler hinweisen.