Appearance
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:
- URL des Empfängers eintragen, z. B.
https://crm.example.com/hooks/pollyou. Erlaubt ist nurhttps://; 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. - Ereignisse auswählen (siehe Ereignisse).
- Optional auf bestimmte Umfragen beschränken. Ohne Auswahl gelten die Ereignisse für alle Umfragen der Organisation.
- 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":{…}}| Header | Inhalt |
|---|---|
PollYou-Event | Ereignistyp, z. B. response.completed |
PollYou-Delivery | ID dieser Zustellung (neu bei jedem erneuten Senden) |
PollYou-Signature | Zeitstempel und Signatur(en), siehe Signatur prüfen |
Jeder Body hat dieselbe Hülle:
| Feld | Inhalt |
|---|---|
id | Eindeutige ID des Ereignisses. Bleibt bei Wiederholungen und erneutem Senden gleich. |
type | Ereignistyp |
created_at | Zeitpunkt des Ereignisses (ISO 8601, UTC) |
organization_id | Organisation, zu der das Ereignis gehört |
data | Inhalt 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
| Ereignis | Wann |
|---|---|
response.completed | Eine Antwort wurde abgeschlossen. Antworten, die PollYou beim Abschluss als Spam erkennt, werden nicht gesendet. |
survey.published | Eine Umfrage wurde veröffentlicht (neue Version; Status live oder – mit Startdatum – scheduled). |
survey.closed | Eine Umfrage wurde beendet: manuell (manual), durch ihr Enddatum (schedule) oder ihr Antwortlimit (response_limit). |
alert.triggered | Ein Schwellenwert-Alarm hat ausgelöst. |
webhook.test | Nur ü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):
| Versuch | Abstand zum vorherigen Versuch |
|---|---|
| 1 | sofort |
| 2 | 1 Minute |
| 3 | 5 Minuten |
| 4 | 30 Minuten |
| 5 | 2 Stunden |
| 6 | 6 Stunden |
| 7 | 12 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…tist der Unix-Zeitstempel des Versands.v1istHMAC-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:
- Den unveränderten Body lesen (nicht erst als JSON parsen und wieder serialisieren – schon ein anderes Leerzeichen ändert die Signatur).
tund allev1aus dem Header lesen.- Ablehnen, wenn
tmehr als 5 Minuten von der aktuellen Zeit abweicht (Schutz vor wiederholt eingespielten Anfragen). HMAC-SHA256(secret, t + "." + body)berechnen und zeitkonstant mit jedemv1vergleichen.
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
| Variable | Bedeutung |
|---|---|
WEBHOOK_ALLOW_HTTP | true erlaubt http://-URLs. In Produktion false. |
WEBHOOK_ALLOWED_PRIVATE_NETWORKS | CIDR-Liste, die vom Schutz vor privaten Adressen ausgenommen ist. In Produktion leer lassen. |
WEBHOOK_RETRY_DELAYS | Abstä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.