Webhooks, HTTP und Credentials — der produktive Eingang
Artikel 5 · Serie: Einstieg in n8n
Der Workflow aus Artikel 4 akzeptiert jeden Request ohne Prüfung und klassifiziert Tickets zuverlässig, solange die Keywords passen. Was fehlt, ist ein produktiver Eingang: Authentifizierung, Payload-Validierung und eine Antwortstruktur, die dem aufrufenden System sagt, was mit dem Ticket passiert ist. In diesem Artikel bauen wir genau das.
Der Code zu diesem Artikel liegt auf Codeberg, Tag v0.5: codeberg.org/rotecodefraktion/n8n-einstieg.
Test-URL und Production-URL
n8n stellt für jeden Webhook-Knoten zwei URLs bereit, und der Unterschied zwischen ihnen verursacht mehr Verwirrung als jedes andere Konzept in dieser Serie.
Die Test-URL (/webhook-test/<path>) ist nur aktiv, solange der Workflow offen ist und der Knoten im „Listen for Test Event"-Modus steht. Sie erlaubt es, den Workflow interaktiv zu testen: Eingehende Daten landen direkt im Execution-View, jeder Knoten zeigt seine Inputs und Outputs. Das ist praktisch für die Entwicklung.
Die Production-URL (/webhook/<path>) ist aktiv, sobald der Workflow per „Publish" veröffentlicht wurde. Sie verarbeitet Requests auch dann, wenn der Workflow im Canvas geschlossen ist, und sie gibt den Response-Body zurück, den der Respond to Webhook-Knoten produziert.
Wer den Webhook eines externen Systems auf die Test-URL zeigen lässt, wundert sich, warum die Antwort im produktiven Betrieb leer bleibt. Die Test-URL führt den Workflow aus und protokolliert ihn — gibt aber den Response-Body des Respond to Webhook-Knotens nicht zurück.
Konsequenz für diesen Artikel: Alle curl-Beispiele und Tests verwenden die Production-URL. Der Workflow muss nach jeder Änderung explizit über „Publish" veröffentlicht werden. Cmd+S allein reicht nicht.
Header-Authentifizierung
n8n unterstützt mehrere Authentifizierungsformen am Webhook-Knoten: Basic Auth, Header Auth, JWT und Bearer Token. Für Internal-System-zu-n8n-Wege ist Header Auth die einfachste Lösung, die in der Praxis ausreicht.
Das Prinzip: der Aufrufer schickt einen vereinbarten Header mit einem Token. n8n prüft den Wert. Bei Mismatch: HTTP 403.
Token erzeugen:
openssl rand -hex 32
# Beispiel: 9d016c979dd36af7c08a23cebf5ea41e5815f6af717fae43acad69cab792ecb1
Credential in n8n anlegen: Credentials → Add credential → Header Auth
| Feld | Wert |
|---|---|
| Name | Ticket Ingest Token |
| Header Name | X-Ingest-Token |
| Header Value | Token aus dem openssl-Befehl |
Die Credential taucht danach in der Webhook-Knoten-Konfiguration unter „Authentication" auf. Auswählen, speichern.
Ein Nebenbefund aus dem Build: n8n antwortet auf einen Request ohne gültigen Token mit HTTP 403 und dem Header WWW-Authenticate: Basic realm="Webhook" — auch wenn der Knoten auf Header Auth konfiguriert ist. Das ist ein bekannter Quirk. Der Statuscode 403 ist eindeutig; der Header verwirrt nur dann, wenn man ihn für eine Konfigurationsaussage hält.
Credentials in n8n — Speicherung und Backup
Credentials werden in n8n AES-verschlüsselt in der Postgres-Datenbank gespeichert. Der Schlüssel für diese Verschlüsselung ist die Umgebungsvariable N8N_ENCRYPTION_KEY aus Artikel 2.
Das hat eine konkrete Konsequenz für Backups: ein Postgres-Dump allein ist nicht ausreichend, um eine Instanz vollständig wiederherzustellen. Ohne den passenden Encryption Key lässt sich der Dump importieren, aber alle Credentials sind unlesbar. n8n meldet in diesem Fall Mismatching encryption keys. Es gibt keine Recovery ohne den ursprünglichen Schlüssel.
Empfehlung: Postgres-Dump und Encryption Key in getrennten Backup-Pfaden mit unterschiedlichen Zugriffsrechten aufbewahren. Wer beides zusammen hat, kann alles entschlüsseln.
# Postgres-Dump
docker compose -f docker/docker-compose.yml exec postgres \
pg_dump -U n8n n8n > backup-$(date +%Y%m%d-%H%M%S).sql
Beim Restore auf einer neuen Maschine: Postgres hochfahren, Dump einspielen, Encryption Key in docker/.env eintragen, dann n8n starten. Reihenfolge ist wichtig — n8n versucht beim Start sofort, auf Credentials zuzugreifen.
Ein weiteres Detail zum Workflow-Export: wenn ein Workflow als JSON heruntergeladen wird, enthält das JSON eine Referenz auf die Credential (credentials.<type>.name), aber nicht den Credential-Inhalt. Beim Import auf einer anderen Instanz ist die Referenz zunächst ungültig. Der Knoten muss in der UI neu mit der Credential verbunden werden.
HTTP-Request-Node
Der HTTP-Request-Node ist das Gegenstück zum Webhook: er schickt Requests aus dem Workflow heraus, an externe APIs, interne Services oder andere Systeme.
Grundkonfiguration für einen POST mit JSON-Body:
| Feld | Wert |
|---|---|
| Method | POST |
| URL | Ziel-URL |
| Authentication | Credential auswählen (Basic, Bearer, OAuth2, Header) |
| Body | JSON |
Query-Parameter lassen sich direkt in der URL schreiben oder über den „Query Parameters"-Bereich des Knotens eintragen — letzteres hat den Vorteil, dass n8n die Werte korrekt URL-encoded.
Pagination konfiguriert man im Abschnitt „Pagination". n8n unterstützt Offset-basierte Pagination (nextPage.offset) und cursor-basierte Pagination. Der Knoten führt dann automatisch mehrere Requests durch, bis keine weiteren Seiten vorhanden sind, und gibt alle Items in einem Array zurück.
Fehlerverhalten. Das Default-Verhalten des HTTP-Request-Knotens bei einem HTTP-Fehler (4xx, 5xx): der Workflow bricht ab. Das ist für Entwicklung und Tests akzeptabel. Für produktive Workflows gibt es zwei relevante Optionen.
„Retry on Fail" (unter „Options" im Knoten): automatischer Retry bei Netzwerkfehler oder 5xx-Antwort, mit konfigurierbarer Anzahl Versuche und Wartezeit zwischen den Versuchen. Für transienten Fehler geeignet — ein Dienst ist kurz nicht erreichbar, der nächste Versuch klappt.
„Continue on Fail" (Node-Setting, erreichbar über das drei-Punkte-Menü am Knoten): bei aktivierter Option wird ein Fehler nicht als Workflow-Abbruch behandelt, sondern als Output-Item mit einem error-Objekt. Der nachfolgende Knoten bekommt dieses Item und kann gezielt reagieren. Das ist der Ansatz, wenn man jeden Request-Fehler individuell behandeln will, zum Beispiel um fehlerhafte Tickets in eine Dead-Letter-Queue zu schreiben.
Timeouts konfiguriert man unter „Options → Timeout". Standard ist kein Timeout — der Knoten wartet unbegrenzt auf eine Antwort. Für externe APIs mit gelegentlich langer Response-Zeit empfiehlt sich ein expliziter Wert (z. B. 10 000 Millisekunden), damit ein hängender Request nicht den gesamten Workflow blockiert.
Sub-Workflows
Ein Sub-Workflow in n8n ist ein eigenständiger Workflow, der von anderen Workflows aufgerufen werden kann — ähnlich einer Funktion. Der aufrufende Workflow übergibt Items, der Sub-Workflow verarbeitet sie und gibt Items zurück.
Warum lohnt sich die Extraktion? Wenn dieselbe Logik in mehreren Workflows verwendet werden soll, liegt die Wahl auf der Hand. In diesem Fall steckt die Klassifikator-Logik aus Artikel 4 jetzt in einem eigenen Sub-Workflow (v0.5 Sub-Classifier). Beide Workflows — v0.4 Rule-Based Classifier und v0.5 Webhook Ingest — rufen ihn auf. Änderungen an der Keyword-Liste geschehen an einer Stelle.

Der Sub-Workflow hat drei Knoten. Trigger-Knoten (Execute Workflow Trigger) empfängt die übergebenen Felder, der Set-Node Normalize baut text_normalized, der Code-Node Classify Keywords führt das Keyword-Matching aus und gibt das Ergebnis zurück.
Der refaktorierte v0.4 Rule-Based Classifier ruft den Sub-Workflow auf und routet das Ergebnis weiter:

Die Konfiguration des Aufruf-Knotens:
| Feld | Wert |
|---|---|
| Source | Database |
| Workflow | Sub-Workflow aus Dropdown auswählen |
| Workflow Inputs | erscheinen automatisch |
Die Workflow-ID in der Konfiguration ist instance-spezifisch. Beim Export des JSON und Re-Import auf einer anderen Instanz ist sie ungültig — der Knoten zeigt „Workflow not found" und muss neu mit dem Sub-Workflow verbunden werden. Reihenfolge beim Import: erst den Sub-Workflow importieren, dann die Caller-Workflows.
Fehlerfall im Sub-Workflow. Wenn ein Sub-Workflow abbricht, bleibt der aufrufende Workflow im ausstehenden Zustand. Um das abzufangen, empfiehlt sich auf Caller-Seite ein Error-Zweig oder — wenn es eine kritische Operation ist — ein separater Error Workflow, den man in den n8n-Einstellungen unter „Settings → Error Workflow" konfigurieren kann.
Der Workflow Schritt für Schritt

v0.5 Webhook Ingest hat sechs Knoten. Bauen in dieser Reihenfolge.
Knoten 1 — Webhook
- HTTP Method:
POST - Path:
ticket-ingest - Authentication:
Header Auth→ CredentialTicket Ingest Token - Respond:
Using Respond to Webhook Node
Production-URL nach Publish: https://<host>/webhook/ticket-ingest.
Knoten 2 — Validate Payload (If)
Knotentyp: „If", Kombinator: AND, vier Bedingungen:
| Feld | Operator | Wert |
|---|---|---|
{{ $json.body.id }} | String is not empty | — |
{{ $json.body.subject }} | String is not empty | — |
{{ $json.body.body }} | String is not empty | — |
{{ $json.body.language }} | String is not empty | — |
Wichtig: alle vier Conditions müssen im Expression-Modus stehen ({{ ... }}). Der If-Knoten hat denselben Hover-Toggle wie der Set-Knoten. Sicherstellen, dass jedes Feld die geschweiften Klammern zeigt.
Knoten 3 — Classify via Sub-workflow (True-Output)
Knotentyp: „Execute Sub-workflow"
- Source:
Database - Workflow:
v0.5 Sub-Classifier
Workflow Inputs (erscheinen nach Auswahl des Sub-Workflows):
| Input | Expression |
|---|---|
id | {{ $json.body.id }} |
subject | {{ $json.body.subject }} |
body | {{ $json.body.body }} |
language | {{ $json.body.language }} |
Knoten 4 — Enrich
Knotentyp: „Edit Fields (Set)"
- Include in Output:
All Input Fields→ an
Drei zusätzliche Felder:
| Name | Mode | Wert |
|---|---|---|
ingested_at | Expression | {{ $now.toISO() }} |
source | Fixed | webhook |
status | Fixed | accepted |
Knoten 5 — Respond OK
Knotentyp: „Respond to Webhook"
- Respond With:
First Incoming Item - Response Code:
200
Knoten 6 — Respond Bad Request (False-Output des If-Knotens)
Knotentyp: „Respond to Webhook"
- Respond With:
JSON - Response Body:
{"error":"validation_failed"} - Response Code:
400
Publish. Test gegen Production-URL.
post-ticket.sh
Das Script scripts/post-ticket.sh im Demo-Repo lädt Token, Header-Name und URL aus .env.local und schickt ein Beispiel-Ticket:
source .env.local
curl -k -sS -i \
-X POST "$N8N_INGEST_URL" \
-H 'Content-Type: application/json' \
-H "$N8N_INGEST_HEADER: $N8N_INGEST_TOKEN" \
-d '{
"id": "TKT-0001",
"subject": "SAP HANA Backup schlägt fehl",
"body": "Backint-Schnittstelle meldet Timeout, /hana/data fast voll.",
"language": "de"
}'
Erwartete Antwort:
{
"id": "TKT-0001",
"category": "infrastruktur",
"match_count": 1,
"scores": {"sap-basis": 0, "sap-functional": 0, "infrastruktur": 1, "cloud": 0, "security-pki": 0},
"ingested_at": "2026-05-27T08:13:49.703+02:00",
"source": "webhook",
"status": "accepted"
}
Negativ-Fälle:
# Fehlendes Pflichtfeld → 400
curl -k -sS -X POST "$N8N_INGEST_URL" \
-H 'Content-Type: application/json' \
-H "$N8N_INGEST_HEADER: $N8N_INGEST_TOKEN" \
-d '{"id": "TKT-X", "subject": "nur subject"}'
# → HTTP 400, {"error":"validation_failed"}
# Kein Token → 403
curl -k -sS -X POST "$N8N_INGEST_URL" \
-H 'Content-Type: application/json' \
-d '{"id": "TKT-X", "subject": "x", "body": "x", "language": "de"}'
# → HTTP 403
HTTP-Smoke-Test
tests/test_ingest_webhook.py prüft den Ingest-Webhook mit 14 Tests: Auth-Fail (fehlender und falscher Token), Validation-Fail (fehlendes Pflichtfeld), Valid-Request mit Prüfung aller Response-Felder, und zehn Tickets aus dem gepinnten Parquet-Datensatz.
cd tests && uv run pytest test_ingest_webhook.py -v
Die Fixtures in conftest.py laden Token, Header und URL aus .env.local — kein Hardcoding von Credentials im Test-Code.
Das Ergebnis nach abgeschlossenem Build: 53 passed, 1 xfailed in 1,21 s. Die 39 Replay-Tests aus Artikel 4 laufen weiterhin durch — TKT-0005 bleibt das dokumentierte xfail.
Nächster Schritt
Artikel 4 hat einen Classifier gebaut, der auf Keyword-Matching basiert und bei englischen Tickets mit abweichender Schreibweise scheitert. Artikel 5 hat diesem Classifier einen produktiven Eingang gegeben. Artikel 6 ersetzt das Keyword-Matching durch einen KI-Klassifikator, der semantische Nähe statt exakter Zeichenketten erkennt — und damit das xfail von TKT-0005 auflöst.
← Artikel 4: Nodes, Expressions und der erste Workflow ohne KI