Skip to content

Webhooks ​

Mit Webhooks meldet PollYou Ereignisse sofort an deine Systeme, statt dass diese die öffentliche API regelmäßig abfragen müssen: Eine neue Antwort landet direkt im CRM, das Beenden einer Umfrage stößt einen Bericht an, ein Schwellenwert-Alarm erscheint im Chat.

PollYou sendet dafür eine signierte POST-Anfrage mit einem JSON-Body an eine URL, die du festlegst.

Webhook einrichten ​

Admins und Inhaber legen Webhooks im Dashboard unter Einstellungen → Webhooks an:

  1. URL des Empfängers eintragen, z. B. https://crm.example.com/hooks/pollyou. Erlaubt ist nur https://; Adressen in privaten oder lokalen Netzen (z. B. 10.0.0.0/8, 192.168.0.0/16, 127.0.0.1, 169.254.169.254) lehnt PollYou ab – auch dann, wenn erst der DNS-Name dorthin zeigt.
  2. Ereignisse auswählen (siehe Ereignisse).
  3. Optional auf bestimmte Umfragen beschränken. Ohne Auswahl gelten die Ereignisse für alle Umfragen der Organisation.
  4. Nach dem Speichern zeigt das Dashboard das Signing-Secret (whsec_…). Hinterlege es in deinem Empfänger, um die Signatur zu prüfen.

Das Secret kannst du später jederzeit wieder anzeigen oder rotieren. Eine Organisation kann bis zu 20 Webhooks anlegen. Anlegen, Ändern, Deaktivieren, Löschen, Anzeigen und Rotieren des Secrets stehen im Audit-Log.

Mit Testereignis senden schickt PollYou sofort ein Ereignis webhook.test an die URL und zeigt das Ergebnis (Statuscode, Dauer, Antwort) an – ideal, um die Signaturprüfung einzurichten.

Aufbau einer Anfrage ​

http
POST /hooks/pollyou HTTP/1.1
Host: crm.example.com
Content-Type: application/json
User-Agent: PollYou-Webhooks/1.0
PollYou-Event: response.completed
PollYou-Delivery: 01a12c20-759e-7740-8712-db1274ba67ac
PollYou-Signature: t=1760170500,v1=6f1c0c3c0a4b…

{"id":"01a12c20-7513-7b38-9685-7081e4d7d453","type":"response.completed","created_at":"2026-10-11T08:15:00+00:00","organization_id":"…","data":{…}}
HeaderInhalt
PollYou-EventEreignistyp, z. B. response.completed
PollYou-DeliveryID dieser Zustellung (neu bei jedem erneuten Senden)
PollYou-SignatureZeitstempel und Signatur(en), siehe Signatur prüfen

Jeder Body hat dieselbe Hülle:

FeldInhalt
idEindeutige ID des Ereignisses. Bleibt bei Wiederholungen und erneutem Senden gleich.
typeEreignistyp
created_atZeitpunkt des Ereignisses (ISO 8601, UTC)
organization_idOrganisation, zu der das Ereignis gehört
dataInhalt je nach Ereignistyp

Idempotent verarbeiten

Ein Ereignis kann mehr als einmal ankommen (Wiederholung nach einem Timeout, manuelles erneutes Senden). Merke dir die verarbeiteten ids und ignoriere Duplikate. Die Reihenfolge der Ereignisse ist nicht garantiert; nutze created_at.

Ereignisse ​

EreignisWann
response.completedEine Antwort wurde abgeschlossen. Antworten, die PollYou beim Abschluss als Spam erkennt, werden nicht gesendet.
survey.publishedEine Umfrage wurde veröffentlicht (neue Version; Status live oder – mit Startdatum – scheduled).
survey.closedEine Umfrage wurde beendet: manuell (manual), durch ihr Enddatum (schedule) oder ihr Antwortlimit (response_limit).
alert.triggeredEin Schwellenwert-Alarm hat ausgelöst.
webhook.testNur über „Testereignis senden“; kann nicht abonniert werden.

data.survey und data.response haben dieselbe Form wie in der öffentlichen API (GET /api/v1/surveys, GET /api/v1/responses/{id}). Wie dort gilt: Es kommen nur Felder hinzu, ignoriere unbekannte Felder.

response.completed ​

json
{
  "id": "01a12c20-7513-7b38-9685-7081e4d7d453",
  "type": "response.completed",
  "created_at": "2026-10-11T08:15:00+00:00",
  "organization_id": "01a12b70-1111-7222-8333-944455556666",
  "data": {
    "survey": {
      "id": "01a12b79-3855-7453-b69a-a847cfe69ce1",
      "publicId": "Abc123XyZ0",
      "title": "Kundenzufriedenheit"
    },
    "response": {
      "id": "01a12c20-6e10-7c55-9a1d-1b2c3d4e5f60",
      "surveyId": "01a12b79-3855-7453-b69a-a847cfe69ce1",
      "status": "completed",
      "startedAt": "2026-10-11T08:13:41+00:00",
      "completedAt": "2026-10-11T08:15:00+00:00",
      "durationMs": 79012,
      "channel": "script",
      "embedId": null,
      "deviceType": "desktop",
      "browserFamily": "Firefox",
      "osFamily": "Linux",
      "country": "DE",
      "locale": "de-DE",
      "referrerHost": "www.example.com",
      "utm": { "source": "newsletter" },
      "meta": { "crm_id": "4711" },
      "identified": false,
      "versionNumber": 2,
      "excluded": false,
      "answers": [
        {
          "questionKey": "nps",
          "type": "nps",
          "title": "Wie wahrscheinlich empfiehlst du uns weiter?",
          "answered": true,
          "value": 9,
          "displayValue": 9,
          "otherText": null,
          "answeredAt": "2026-10-11T08:14:02+00:00"
        }
      ]
    }
  }
}

survey.published ​

json
{
  "id": "01a12c21-0b2a-7d11-8e4f-5a6b7c8d9e0f",
  "type": "survey.published",
  "created_at": "2026-10-11T09:00:00+00:00",
  "organization_id": "01a12b70-1111-7222-8333-944455556666",
  "data": {
    "survey": {
      "id": "01a12b79-3855-7453-b69a-a847cfe69ce1",
      "publicId": "Abc123XyZ0",
      "type": "survey",
      "title": "Kundenzufriedenheit",
      "description": null,
      "status": "live",
      "questionCount": 5,
      "publishedVersion": 3,
      "publishedAt": "2026-10-11T09:00:00+00:00",
      "hasUnpublishedChanges": false,
      "createdAt": "2026-09-20T10:00:00+00:00",
      "updatedAt": "2026-10-11T09:00:00+00:00"
    },
    "version": {
      "number": 3,
      "publishedAt": "2026-10-11T09:00:00+00:00",
      "note": "Frage 4 umformuliert",
      "startsAt": null,
      "endsAt": "2026-10-31T23:59:59+00:00"
    }
  }
}

survey.closed ​

json
{
  "id": "01a12c22-4c5d-7e6f-8a9b-0c1d2e3f4a5b",
  "type": "survey.closed",
  "created_at": "2026-10-31T23:59:59+00:00",
  "organization_id": "01a12b70-1111-7222-8333-944455556666",
  "data": {
    "survey": {
      "id": "01a12b79-3855-7453-b69a-a847cfe69ce1",
      "publicId": "Abc123XyZ0",
      "type": "survey",
      "title": "Kundenzufriedenheit",
      "description": null,
      "status": "closed",
      "questionCount": 5,
      "publishedVersion": 3,
      "publishedAt": "2026-10-11T09:00:00+00:00",
      "hasUnpublishedChanges": false,
      "createdAt": "2026-09-20T10:00:00+00:00",
      "updatedAt": "2026-10-31T23:59:59+00:00"
    },
    "reason": "schedule"
  }
}

alert.triggered ​

json
{
  "id": "01a12c23-9d8e-7f6a-8b5c-4d3e2f1a0b9c",
  "type": "alert.triggered",
  "created_at": "2026-10-12T07:00:00+00:00",
  "organization_id": "01a12b70-1111-7222-8333-944455556666",
  "data": {
    "alert": {
      "id": "01a12c00-aaaa-7bbb-8ccc-dddddddddddd",
      "name": "NPS unter 30",
      "metric": "nps_below",
      "condition": { "npsAnswers": 42, "score": 12.5 },
      "threshold": 30,
      "windowHours": 168
    },
    "survey": { "id": "01a12b79-3855-7453-b69a-a847cfe69ce1", "title": "Kundenzufriedenheit" },
    "value": 12.5,
    "triggeredAt": "2026-10-12T07:00:00+00:00"
  }
}

metric ist die Art des Alarms (nps_below, new_detractors, response_drop, limit_near), condition enthält die Zahlen der Auswertung, value den gemessenen Wert (NPS, Anzahl neuer Kritiker, Rückgang bzw. Anteil am Limit in Prozent). Das Ereignis kommt einmal pro Überschreitung – zur selben Zeit wie die Alarm-E-Mail. survey ist null bei Alarmen, die nicht zu einer Umfrage gehören; solche Ereignisse erhalten nur Webhooks ohne Umfrage-Filter.

webhook.test ​

json
{
  "id": "01a12c24-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "type": "webhook.test",
  "created_at": "2026-10-11T08:00:00+00:00",
  "organization_id": "01a12b70-1111-7222-8333-944455556666",
  "data": {
    "message": "This is a test event sent from the PollYou dashboard.",
    "endpoint": {
      "id": "01a12c10-0000-7000-8000-000000000001",
      "url": "https://crm.example.com/hooks/pollyou"
    }
  }
}

Zustellung und Wiederholungen ​

Dein Empfänger muss innerhalb von 10 Sekunden mit einem 2xx-Status antworten. Alles andere – Timeout, Verbindungsfehler, 4xx, 5xx und auch Weiterleitungen (3xx, PollYou folgt ihnen nicht) – gilt als Fehlschlag. Antworte deshalb sofort und verarbeite das Ereignis danach (z. B. über eine Queue).

Fehlgeschlagene Zustellungen wiederholt PollYou mit wachsenden Abständen (jeweils ±10 % zufällig gestreut):

VersuchAbstand zum vorherigen Versuch
1sofort
21 Minute
35 Minuten
430 Minuten
52 Stunden
66 Stunden
712 Stunden

Nach dem 7. Versuch (rund 21 Stunden nach dem Ereignis) gilt die Zustellung als fehlgeschlagen. Du kannst sie im Zustellprotokoll erneut senden.

Automatische Deaktivierung: Schlägt ein Webhook seit mindestens 24 Stunden ohne einen einzigen erfolgreichen Versuch fehl (und mindestens 10-mal in Folge), deaktiviert PollYou ihn. Alle Admins und Inhaber der Organisation erhalten eine E-Mail. Ausstehende Zustellungen werden dabei verworfen; nach der Fehlerbehebung aktivierst du den Webhook im Dashboard wieder und sendest verpasste Ereignisse bei Bedarf erneut.

Zustellprotokoll ​

In den Einstellungen siehst du je Webhook alle Zustellungen der letzten 30 Tage: Ereignis, Status (ausstehend, zugestellt, fehlgeschlagen), Anzahl der Versuche, Statuscode, Dauer und den nächsten geplanten Versuch. Die Detailansicht zeigt den gesendeten Body und für jeden Versuch die ersten 1 KB der Antwort deines Servers.

Erneut senden stellt dasselbe Ereignis (gleiche id, gleicher Body) als neue Zustellung ein. Das geht nur für aktive Webhooks.

Signatur prüfen ​

Jede Anfrage ist mit dem Secret des Webhooks signiert. Prüfe die Signatur, bevor du den Body verarbeitest – nur so weißt du, dass die Anfrage von PollYou stammt und unterwegs nicht verändert wurde.

PollYou-Signature: t=1760170500,v1=6f1c0c3c0a4b…
  • t ist der Unix-Zeitstempel des Versands.
  • v1 ist HMAC-SHA256 über die Zeichenkette <t>.<Body> mit dem vollständigen Secret (whsec_…) als Schlüssel, hexadezimal.
  • Kurz nach einer Rotation enthält der Header zwei v1-Werte (neues und altes Secret). Akzeptiere die Anfrage, wenn einer davon passt.

So prüfst du:

  1. Den unveränderten Body lesen (nicht erst als JSON parsen und wieder serialisieren – schon ein anderes Leerzeichen ändert die Signatur).
  2. t und alle v1 aus dem Header lesen.
  3. Ablehnen, wenn t mehr als 5 Minuten von der aktuellen Zeit abweicht (Schutz vor wiederholt eingespielten Anfragen).
  4. HMAC-SHA256(secret, t + "." + body) berechnen und zeitkonstant mit jedem v1 vergleichen.

PHP ​

php
<?php

/**
 * Checks the PollYou-Signature header of a webhook request.
 */
function pollyou_verify_signature(string $payload, string $header, string $secret, int $tolerance = 300, ?int $now = null): bool
{
    $timestamp = null;
    $signatures = [];
    foreach (explode(',', $header) as $part) {
        [$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
        if ($key === 't' && ctype_digit($value)) {
            $timestamp = (int) $value;
        } elseif ($key === 'v1') {
            $signatures[] = $value;
        }
    }
    if ($timestamp === null || abs(($now ?? time()) - $timestamp) > $tolerance) {
        return false;
    }

    $expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
    foreach ($signatures as $signature) {
        if (hash_equals($expected, $signature)) {
            return true;
        }
    }

    return false;
}

Verwendung, z. B. in einem einfachen PHP-Skript:

php
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_POLLYOU_SIGNATURE'] ?? '';

if (!pollyou_verify_signature($payload, $header, getenv('POLLYOU_WEBHOOK_SECRET'))) {
    http_response_code(400);
    exit;
}

$event = json_decode($payload, true);
// … Ereignis in eine Queue legen, Duplikate über $event['id'] erkennen
http_response_code(204);

Node.js ​

js
import { createHmac, timingSafeEqual } from 'node:crypto'

/**
 * Checks the PollYou-Signature header of a webhook request.
 * rawBody: the unparsed request body (string or Buffer).
 */
export function verifyPollYouSignature(
  rawBody,
  header,
  secret,
  toleranceSeconds = 300,
  now = Date.now() / 1000,
) {
  let timestamp = null
  const signatures = []
  for (const part of String(header ?? '').split(',')) {
    const [key, value = ''] = part.trim().split('=', 2)
    if (key === 't' && /^\d+$/.test(value)) timestamp = Number(value)
    else if (key === 'v1') signatures.push(value)
  }
  if (timestamp === null || Math.abs(now - timestamp) > toleranceSeconds) return false

  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest()
  return signatures.some((signature) => {
    const given = Buffer.from(signature, 'hex')
    return given.length === expected.length && timingSafeEqual(given, expected)
  })
}

Verwendung mit Express – wichtig ist express.raw(), damit der Body unverändert ankommt:

js
import express from 'express'

const app = express()

app.post('/hooks/pollyou', express.raw({ type: 'application/json' }), (req, res) => {
  const body = req.body.toString('utf8')
  if (
    !verifyPollYouSignature(body, req.get('PollYou-Signature'), process.env.POLLYOU_WEBHOOK_SECRET)
  ) {
    return res.status(400).end()
  }
  const event = JSON.parse(body)
  // … Ereignis in eine Queue legen, Duplikate über event.id erkennen
  res.status(204).end()
})

Beide Beispiele werden automatisch gegen echte Signaturen von PollYou getestet.

Secret rotieren ​

Secret rotieren erzeugt ein neues Secret und zeigt es an. Das alte signiert noch 24 Stunden weiter (zweiter v1-Wert im Header). In dieser Zeit hinterlegst du das neue Secret im Empfänger – ohne dass ein Ereignis verloren geht. Rotiere sofort, wenn ein Secret nach außen gelangt sein könnte.

Für Entwicklerinnen und Entwickler ​

Im Docker-Compose-Entwicklungsstack erlaubt PollYou zusätzlich http:// und Empfänger auf dem Host-Rechner (http://host.docker.internal:<port>/…). Gesteuert wird das über

VariableBedeutung
WEBHOOK_ALLOW_HTTPtrue erlaubt http://-URLs. In Produktion false.
WEBHOOK_ALLOWED_PRIVATE_NETWORKSCIDR-Liste, die vom Schutz vor privaten Adressen ausgenommen ist. In Produktion leer lassen.
WEBHOOK_RETRY_DELAYSAbstände der Wiederholungen in Sekunden, Standard 60,300,1800,7200,21600,43200.

Zugestellt wird vom Worker über die Prioritäts-Queue (async_priority, vor Auswertungs-Nachrichten), fällige Wiederholungen plant der Scheduler; ohne laufenden Worker bleiben Zustellungen ausstehend und werden nachgeholt, sobald er wieder läuft.