Webhooks
Aufbau, Signatur und Zustellung der Webhook-Requests für Antworten und abgeschlossene Teilnahmen.
PollHero sendet pro Event einen signierten POST-Request mit JSON-Body an die Endpoint-URL deines Projekts. So landen Antworten ohne eigene Abfrage in deinem CRM, deinem Data Warehouse, in Slack oder in einem Automatisierungstool wie Zapier, Make oder n8n. Die übrige Data Export API holt Daten auf Anfrage ab, Webhooks bringen sie dir, sobald sie entstehen.
| Eigenschaft | Wert |
|---|---|
| Verfügbarkeit | Enterprise-Plan |
| Ebene | Projekt, gilt für alle Umfragen des Projekts |
| Events | question_answered, survey_completed |
| Format | POST mit JSON-Body, UTF-8 |
| Verifizierung | Token-Header und HMAC-SHA256-Signatur |
Webhook einrichten
Webhooks richtest du pro Projekt ein. Die Einstellungen können Inhaber des Accounts und Admins des Projekts ändern.
Schritt 1: Öffne die Projekteinstellungen, scrolle zum Abschnitt Webhook und setze den Toggle Webhook aktivieren auf AN.
Schritt 2: Aktiviere eines oder beide Events: Antworten senden (Event question_answered) und Abschluss der Umfrage senden (Event survey_completed). Unter jedem aktiven Event erscheint ein Feld Endpoint-URL. Trag dort die URL deines Empfängers ein. Beide Events können an dieselbe oder an verschiedene URLs gehen.
Schritt 3: Klick neben dem Feld auf Test-Request senden. PollHero schickt einen Beispiel-Request mit "test": true an die eingetragene URL und zeigt dir an, ob dein Endpoint erfolgreich geantwortet hat. Der Test funktioniert schon vor dem Speichern.
Schritt 4: Speichere die Projekteinstellungen. Beim ersten Speichern mit aktivem Event erzeugt PollHero den Verification Token und das Signing Secret. Beide findest du danach im Abschnitt Verifizierung und kannst sie mit Kopieren übernehmen.
Solange Webhook aktivieren auf AUS steht, wird nichts gesendet, auch wenn einzelne Events aktiv sind.
Anforderungen an den Endpoint
- Die URL muss öffentlich über
httpoderhttpserreichbar sein. Adressen in privaten Netzen sowie Hostnamen wielocalhostlehnt PollHero ab. - Der Endpoint muss innerhalb von 15 Sekunden mit einem Status
2xxantworten. Für den Verbindungsaufbau gelten 5 Sekunden. - Weiterleitungen folgt PollHero nicht. Ein Status
301oder302zählt als fehlgeschlagene Zustellung. - Antworte am besten sofort mit
200und verarbeite den Inhalt danach im Hintergrund.
Events
| Event | Wird gesendet | Inhalt |
|---|---|---|
question_answered | sobald eine Antwort auf eine Frage gespeichert wurde | die eine Antwort mit Frage, Teilnahme und Teilnehmenden |
survey_completed | sobald eine Teilnahme abgeschlossen ist | alle Antworten der Teilnahme |
- Übersprungene Fragen lösen kein Event aus.
- Ändert jemand innerhalb derselben Teilnahme eine Antwort, wird
question_answerederneut gesendet, mit neuermessageId. survey_completedkommt einmal pro Teilnahme.- Ist PII Masking aktiv, kommen die Antworten bereits maskiert an.
- Antworten aus dem CSV-Import lösen keine Events aus.
- Die Stimmungsanalyse läuft erst nach dem Versand. Das Sentiment einer Freitextantwort bekommst du über die Data Export API.
Aufbau des Requests
Header
| Header | Inhalt |
|---|---|
Content-Type | application/json |
User-Agent | PollHero-Webhook/1.0 |
X-PollHero-Event | question_answered oder survey_completed |
X-PollHero-Delivery | ID der Nachricht, identisch mit messageId im Body. Bleibt bei Wiederholungen gleich. |
X-PollHero-Token | Verification Token des Projekts |
X-PollHero-Signature | HMAC-SHA256 des Request-Bodys, siehe Signatur prüfen |
Gemeinsame Felder
Jeder Body beginnt mit denselben Feldern:
| Feld | Typ | Beschreibung |
|---|---|---|
messageId | String | Eindeutige ID der Nachricht. Bleibt bei Wiederholungen gleich, damit du Duplikate erkennst. |
deliveryTries | Integer | Nummer des Zustellversuchs, beginnend bei 1. |
event | String | question_answered oder survey_completed |
timestamp | String | Zeitpunkt des Events, ISO 8601 in UTC |
test | Boolean | Nur im Test-Request enthalten, dort true. |
Event question_answered
{
"messageId": "0e0938c0-1e85-413b-8ffd-5a0e3a26340a",
"deliveryTries": 1,
"event": "question_answered",
"timestamp": "2026-10-27T11:51:20.000000Z",
"survey": {
"id": "21cba834-ad20-4f3b-929d-5abfff8c96ae",
"name": "Post-Purchase-Umfrage"
},
"question": {
"id": "1165ac0a-1fcf-451a-a361-db75602d1283",
"body": "Wie wahrscheinlich ist es, dass du uns weiterempfiehlst?",
"type": "nps"
},
"response": {
"user_id": "23decc90-820a-40bf-92ea-db493c71bfa0",
"collected_at": "2026-10-27T11:51:19.000000Z",
"completed": false,
"url": "https://shop.example.com/checkout/thank-you",
"order_id": "1232"
},
"answer": {
"answer": { "content": 9, "tag": "promoter" }
},
"respondent": {
"id": "23decc90-820a-40bf-92ea-db493c71bfa0",
"attributes": {
"email": "jane@example.com",
"customer_type": "returning",
"total_purchases": 3,
"plan": "pro"
}
}
}| Feld | Beschreibung |
|---|---|
survey.id, survey.name | ID und Name der Umfrage. Die ID ist dieselbe wie in der Data Export API. |
question.id | ID der Frage, wie in der Data Export API |
question.body | Fragetext |
question.type | Fragetyp in der Schreibweise der API, zum Beispiel nps, single, text |
response | Die Teilnahme, zu der die Antwort gehört, siehe unten |
answer | Die Antwort. answer enthält einen einzelnen Wert, answers eine Liste, zum Beispiel bei Multi Select, Ranking und Matrix. Der Aufbau je Fragetyp entspricht der API, siehe Antworten nach Fragetyp. |
respondent | Die teilnehmende Person, siehe unten |
Event survey_completed
{
"messageId": "7c1d2a9e-5b7f-4d0e-9a3c-1f2e3d4c5b6a",
"deliveryTries": 1,
"event": "survey_completed",
"timestamp": "2026-10-27T11:52:01.000000Z",
"survey": {
"id": "21cba834-ad20-4f3b-929d-5abfff8c96ae",
"name": "Post-Purchase-Umfrage"
},
"response": {
"user_id": "23decc90-820a-40bf-92ea-db493c71bfa0",
"collected_at": "2026-10-27T11:51:19.000000Z",
"completed": true,
"url": "https://shop.example.com/checkout/thank-you",
"order_id": "1232",
"completed_at": "2026-10-27T11:52:01.000000Z",
"time_taken": 42,
"device_type": "Desktop",
"browser": "Chrome",
"country": "Germany",
"utm": {
"source": "newsletter",
"medium": "email",
"campaign": "autumn",
"term": null,
"content": null
}
},
"answers": [
{
"question_id": "1165ac0a-1fcf-451a-a361-db75602d1283",
"question_type": "nps",
"question_body": "Wie wahrscheinlich ist es, dass du uns weiterempfiehlst?",
"answer": { "content": 9, "tag": "promoter" }
},
{
"question_id": "8d2f6b1c-3a4e-4f5d-9c7b-2e1f0a9b8c7d",
"question_type": "text",
"question_body": "Was ist der Grund für deine Bewertung?",
"answer": "Schnelle Lieferung, gerne wieder."
}
],
"respondent": {
"id": "23decc90-820a-40bf-92ea-db493c71bfa0",
"attributes": {
"email": "jane@example.com",
"customer_type": "returning",
"total_purchases": 3,
"plan": "pro"
}
}
}answers enthält alle beantworteten Fragen in der Reihenfolge, in der sie beantwortet wurden. Jeder Eintrag trägt zusätzlich question_id, question_type und question_body.
Objekt response
| Feld | Typ | Beschreibung |
|---|---|---|
user_id | String | Anonyme PollHero-User-ID des Browsers, identisch mit respondent.id |
collected_at | String | Zeitpunkt der ersten Antwort in dieser Teilnahme |
completed | Boolean | Die Teilnahme ist abgeschlossen. |
url | String oder null | Seite, auf der die Umfrage beantwortet wurde |
order_id | String oder null | Order-ID (transaction_id) der Bestellung, die zu dieser Teilnahme gehört, siehe Purchase-Tracking. null, wenn es keine passende Bestellung gibt. |
Nur bei survey_completed:
| Feld | Typ | Beschreibung |
|---|---|---|
completed_at | String | Zeitpunkt des Abschlusses |
time_taken | Integer oder null | Dauer der Teilnahme in Sekunden |
device_type, browser, country | String oder null | Gerät, Browser und Land der teilnehmenden Person |
utm | Objekt | source, medium, campaign, term, content der Seite, auf der die Umfrage angezeigt wurde |
Die Bestellung wird über die PollHero-User-ID zugeordnet: Es zählt die Bestellung derselben Person im selben Projekt, die zeitlich am nächsten am Beginn der Teilnahme liegt, höchstens eine Stunde davor oder danach. Meldet dein Shop die Bestellung erst nach der ersten Antwort, kann order_id bei question_answered noch null sein, während survey_completed und die Data Export API sie bereits enthalten.
Objekt respondent
| Feld | Typ | Beschreibung |
|---|---|---|
id | String | Anonyme PollHero-User-ID |
attributes | Objekt | Die Benutzerattribute der Person: die festen Attribute email, customer_id, customer_type, total_revenue, total_purchases, total_aov, country und language, sofern gesetzt, plus alle eigenen Attribute aus identify. Ohne Attribute ist das Objekt leer. |
Anders als in der Data Export API bekommst du hier immer alle Attribute, ohne sie anzufordern, und als Objekt mit den Attributnamen als Schlüssel.
Signatur prüfen
Jeder Request trägt zwei Merkmale, an denen dein Endpoint erkennt, dass er wirklich von PollHero kommt:
X-PollHero-Tokenenthält den Verification Token des Projekts. Vergleiche ihn mit dem Wert aus den Projekteinstellungen.X-PollHero-Signatureenthält den HMAC-SHA256 des unveränderten Request-Bodys, berechnet mit dem Signing Secret, als Hexadezimalzahl in Kleinbuchstaben.
Berechne die Signatur über die rohen Bytes des Bodys, bevor du das JSON parst. Ein neu serialisiertes JSON ergibt eine andere Signatur. Vergleiche mit einer zeitkonstanten Funktion.
// Node.js mit Express: den rohen Body behalten
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/pollhero-webhook", express.raw({ type: "application/json" }), (req, res) => {
const expected = crypto
.createHmac("sha256", process.env.POLLHERO_SIGNING_SECRET)
.update(req.body)
.digest("hex");
const given = req.get("X-PollHero-Signature") ?? "";
if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.sendStatus(401);
}
const payload = JSON.parse(req.body);
// Verarbeitung im Hintergrund anstoßen ...
res.sendStatus(200);
});<?php
$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $body, getenv('POLLHERO_SIGNING_SECRET'));
$given = $_SERVER['HTTP_X_POLLHERO_SIGNATURE'] ?? '';
if (! hash_equals($expected, $given)) {
http_response_code(401);
exit;
}
$payload = json_decode($body, true);
http_response_code(200);Mit Neu generieren im Abschnitt Verifizierung erzeugst du neuen Token und neues Secret. Die alten Werte gelten ab diesem Moment nicht mehr, dein Empfänger muss die neuen Werte sofort bekommen.
Zustellung und Wiederholungen
- Eine Zustellung gilt als erfolgreich, wenn dein Endpoint mit Status
2xxantwortet. Jeder andere Status, ein Timeout oder ein Verbindungsfehler zählt als Fehlschlag. - Nach einem Fehlschlag versucht PollHero es bis zu vier weitere Male: nach 1, 5, 15 und 60 Minuten.
deliveryTrieszählt die Versuche,messageIdbleibt gleich. Danach wird die Nachricht verworfen. - Die Reihenfolge ist nicht garantiert. Zwei Antworten können in anderer Reihenfolge ankommen, als sie gegeben wurden. Nutze
timestampodercollected_at, wenn die Reihenfolge wichtig ist. - Webhooks laufen asynchron. In der Regel kommt der Request wenige Sekunden nach der Antwort an.
Schlagen 100 Zustellversuche in Folge fehl, schaltet PollHero den Webhook des Projekts automatisch ab. In den Projekteinstellungen siehst du dann einen Hinweis mit dem Zeitpunkt. Events aus der Zeit der Abschaltung werden nicht nachgesendet. Prüfe deinen Endpoint und setze Webhook aktivieren danach wieder auf AN.
Tipps für den Empfänger
- Beantworte jeden Request sofort mit
200und verarbeite die Daten anschließend. So vermeidest du Timeouts und Wiederholungen. - Speichere
messageIdund ignoriere Nachrichten, die du schon kennst. Bei Wiederholungen kann dieselbe Nachricht mehr als einmal ankommen. - Für die vollständige Historie oder einen Abgleich nutze die Data Export API. Webhooks liefern nur Ereignisse ab dem Zeitpunkt der Aktivierung.