Was du in diesem Tutorial baust, ist kein Anrufbeantworter. Es ist ein KI-Telefonassistent, der Anrufe vollautomatisch entgegennimmt, ein strukturiertes Gespräch führt – nach Name, Anliegen und Dringlichkeit fragt – und dir danach eine fertige E-Mail-Zusammenfassung schickt. Alles in natürlicher Sprache, auf Deutsch.
Das Ganze läuft auf deiner Synology NAS zuhause, ohne Cloud-Abhängigkeiten für die Infrastruktur. Als Telefonanlage dient die Fritz!Box 7590, als Telefonie-Server Asterisk im Docker-Container, als KI-Gehirn die OpenAI Realtime API. Die Automatisierung übernimmt n8n.
Klingt komplex? An einigen Stellen ist es das auch – diese Anleitung führt dich aber Schritt für Schritt durch. Mit SSH-Grundkenntnissen und etwas Geduld ist das Ergebnis nachbaubar.
Um diese Anleitung verständlicher zu machen, begleiten dich drei Personen:
Die typischen Büro-Charaktere: Die kompetente IT-Kollegin, der selbsternannte Experte und der ehrliche Anfänger. Diese drei Perspektiven helfen dir, typische Stolperfallen zu erkennen und n8n wirklich zu verstehen.
Tanja ist die IT-Expertin. Sie weiß, wie n8n funktioniert, erklärt geduldig und strukturiert – und lässt sich von schlechten Ratschlägen nicht aus der Ruhe bringen. Wenn du eine Frage hast, hat Tanja die Antwort.
Bernd ist der selbsternannte „Experte“, der alles besser weiß – und meistens falsch liegt. Seine Abkürzungen und sein Halbwissen führen regelmäßig zu Problemen. Er steht für alle gefährlichen Mythen und schlechten Praktiken, die du vermeiden solltest.
Ulf ist der Lernende, genau wie du. Er stellt die Fragen, die dir im Kopf herumschwirren, und braucht manchmal einen Vergleich aus dem Alltag, um IT zu verstehen. Wenn Ulf etwas nicht versteht, ist das völlig in Ordnung – dafür ist Tanja da.
Diese drei Perspektiven helfen dir, typische Stolperfallen zu erkennen und n8n wirklich zu verstehen.
„Und… Action!“
Donnerstagabend. Büro. Ulf starrt auf sein Handy.
Ulf: „Schon wieder eine verpasste Anruf-Benachrichtigung. Keine Nachricht. Keine Nummer. Nix.“
Bernd ohne aufzuschauen: „Ruf zurück“,
Ulf: „Ich kenn die Nummer nicht. Und wenn ich zurückruf, ist besetzt. Oder niemand geht ran. Oder — — –„
Bernd: „Ich hab das gelöst, Anrufbeantworter. Ganz klassisch. ‚Hinterlassen Sie eine Nachricht nach dem Piepton.‘ Fertig.“
Tanja legt ihren Stift hin. „Und wie viele Leute hinterlassen heutzutage noch Nachrichten auf einem Anrufbeantworter?“
Bernd überlegt. „Na… Oma.“
Tanja: „Genau.“
1 Technische Beschreibung
1.1 Was macht dieses Projekt?
Ulf fragt: „Also die KI geht ans Telefon und redet mit dem Anrufer?“,
Tanja: „Genau, sie begrüßt ihn, fragt nach seinem Namen, hört sich das Anliegen an, klärt wie dringend es ist und verabschiedet sich. Alles auf Deutsch, alles in natürlicher Sprache.“
Ulf: „Und dann?“
Tanja: „Und dann bekommst du eine E-Mail mit dem kompletten Gesprächsverlauf. Wer hat angerufen, was wollte die Person, wie dringend war es.“
Bernd lehnt sich vor: „Ich hätte da einfach einen billigen Anrufbeantworter hingestellt.“
Tanja: „Hattest du. Der hat nichts aufgenommen, weil der Piepton zu leise war.“
Bernd sagt nichts mehr.
Der KI-Assistent übernimmt Anrufe auf deiner Festnetznummer vollautomatisch. Er begrüßt den Anrufer freundlich, fragt nach Name und Anliegen, klärt die Dringlichkeit und verabschiedet sich in natürlichem Deutsch, ohne roboterhafte Pausen. Wenige Sekunden nach dem Auflegen landet eine vollständige Zusammenfassung in deinem Posteingang.
1.2 Warum diese Architektur?
Ulf: „Moment, kann die Fritz!Box nicht einfach direkt mit OpenAI reden? Warum brauchen wir drei Systeme dazwischen?“
Tanja: „Gute Frage. Die Fritz!Box spricht SIP – das ist das klassische Telefonie-Protokoll. OpenAI spricht WebSocket – das ist ein modernes Streaming-Protokoll. Die beiden verstehen sich nicht direkt. Wir brauchen einen Übersetzer.“
Ulf: „Asterisk?“
Tanja: „Asterisk ist der Übersetzer auf der Telefonie-Seite. Und das Node.js-Relay ist die Brücke zu OpenAI. Zusammen bilden sie den Verbindungskanal.“
Ulf: „Das ist wie beim Fußball, die Fritz!Box ist der Torwart, der den Ball abschlägt. Asterisk ist der defensive Mittelfeldspieler, der ihn annimmt. Und das Relay ist der Spielmacher, der ihn zu OpenAI weiterleitet.“
Kurze Stille.
Tanja: „Das ist, überraschend präzise.“
Die Fritz!Box ist ein reines Telefon-Gateway – sie versteht kein WebSocket, das OpenAI für die Realtime API benötigt. Asterisk übernimmt daher die Rolle des Vermittlers: Er nimmt das klassische SIP/RTP-Telefonsignal entgegen und reicht es an das Node.js-Relay weiter. Das Relay öffnet seinerseits eine WebSocket-Verbindung zu OpenAI und streamt das Audio bidirektional.
Der vollständige Signalweg:
Telefon → FritzBox (SIP) → Asterisk (Docker) → [ARI/Node.js Relay] → WebSocket → api.openai.com:443
OpenAI übernimmt dabei die komplette Gesprächsintelligenz: Spracherkennung, KI-Antworten und Text-to-Speech in einem einzigen Modell mit sehr niedriger Latenz. Nach dem Gespräch sendet das Relay das Transkript per Webhook an n8n, das daraus eine formatierte E-Mail erstellt.
1.3 Technologie-Stack
| Komponente | Rolle |
|---|---|
| Fritz!Box 7590 | Telefon-Gateway: nimmt Festnetzanruf entgegen, leitet per SIP weiter |
| Asterisk 20 (Docker) | Telefonie-Server: SIP-Registrierung, Anrufsteuerung via ARI |
| Node.js Relay (Docker) | Brücke: wandelt RTP-Audio in WebSocket-Stream für OpenAI um |
| OpenAI Realtime API | KI-Gehirn: Spracherkennung, Dialog, Text-to-Speech in Echtzeit |
| n8n (Docker) | Automatisierung: empfängt Webhook, erstellt und versendet E-Mail |
| Synology NAS | Host: läuft alle Docker-Container, immer eingeschaltet |
1.4 Voraussetzungen
Bevor du loslegst – ein ehrlicher Check. Du brauchst:
Hardware:
- Synology NAS mit Intel-CPU (z. B. DS1621xs+, DS1621+, DS923+, DS720+ oder vergleichbar) – ARM-Prozessoren sind für Docker-Workloads wie diesen zu schwach
- Mindestens 4 GB RAM auf der NAS (8 GB empfohlen, wenn n8n parallel läuft)
- DSM 7.0 oder neuer
- Fritz!Box mit aktiver Festnetznummer (getestet mit Fritz!Box 7590)
Software & Dienste: - Docker bzw. Container Manager auf der Synology bereits eingerichtet
- n8n läuft bereits als Docker-Container
- OpenAI-Account mit hinterlegter Zahlungsmethode und API-Key
- Optional: Cloudflare-Tunnel für externen Zugriff auf n8n-Webhooks
Kenntnisse: - SSH-Grundkenntnisse – also: Befehle in die Konsole tippen können
- Grundlegendes Verständnis von Docker und compose.yaml
- Keine Programmierkenntnisse nötig – du bearbeitest vorhandene Konfigurationsdateien
1.5 Kosten
Ulf: „Was kostet das Ganze?
Bernd: „Ich hab schon ein ChatGPT-Plus-Abo – zählt das?“
Tanja: „Leider nein, das Plus-Abo ist für die Chat-Oberfläche. Die Realtime API läuft über die Entwicklerplattform. Die hat eine eigene Abrechnung. Kein Abo, kein Monatspreis. Du zahlst minutengenau, genau das was du verbrauchst.“
| Modell | Kosten pro Minute (ca.) |
|---|---|
| gpt-4o-realtime-preview | ~$0,12–0,24 |
Tanja: „Ein typisches Gespräch dauert ein bis drei Minuten. Das sind $0,12 bis $0,75 pro Anruf. Mit $10 Startguthaben kommst du für viele Wochen aus, wenn du das für gelegentliche Heimanrufe nutzt.“
Ulf: „Und wenn das Guthaben aufgebraucht ist?“.
Tanja: „Dann läuft nichts mehr. Deswegen setzt du in der OpenAI-Konsole einen Spending Alert und ein monatliches Kostenlimit – dann wirst du rechtzeitig benachrichtigt, bevor das Konto leer ist.“
Bernd: „Ich würde das Limit einfach weglassen. Kostet ja nicht viel.“
Tanja: „Wenn jemand deinen Webhook automatisiert aufruft, absichtlich oder aus Versehen, hast du am Monatsende eine unschöne Überraschung. Das Limit ist keine Bremse, es ist eine Sicherung.“
1.6 Ablauf im Überblick
Bevor wir bauen, die Vogelperspektive: So läuft ein kompletter Anruf durch das System.
- Anruf kommt auf der Fritz!Box-Festnetznummer an
- Fritz!Box leitet den Anruf per SIP an Asterisk weiter
- Asterisk nimmt den Anruf an und übergibt ihn an das Node.js-Relay (via ARI)
- Das Relay öffnet eine WebSocket-Verbindung zu
wss://api.openai.com/v1/realtimeund streamt Audio bidirektional - OpenAI führt das komplette Gespräch mit dem Anrufer
- Nach Gesprächsende sendet das Relay das Transkript per HTTP POST an n8n
- n8n erstellt eine formatierte E-Mail mit Anrufernummer, Name, Anliegen, Dringlichkeit und vollständigem Gesprächsverlauf
2 Implementierung
2.1 Vorbereitung: OpenAI API-Key erzeugen
Ulf: „Wo bekomme ich diesen API-Key her? Ist das irgendwo in meinen Account-Einstellungen?“
Tanja: „Fast. Du brauchst die Entwicklerplattform. Das ist eine separate Seite von OpenAI. platform.openai.com, nicht chat.openai.com.“ Tanja tippt die Adresse. „Dort erstellst du den Key und lädst Guthaben auf.“
Ulf: „Und das ist dann mit meinem normalen Account verknüpft?“
Tanja: „Selbe E-Mail, aber separate Abrechnung. Was du dort verbrauchst, wird unabhängig von allem anderen abgerechnet.“
Die OpenAI Realtime API ist das KI-Gehirn deines Telefonassistenten. Sie ermöglicht Echtzeit-Sprachkonversationen: Spracherkennung, Antwortgenerierung und Text-to-Speech laufen in einem einzigen Modell mit sehr niedriger Latenz. Ohne API-Key läuft gar nichts.
Den Key bekommst du unter platform.openai.com/account/api-keys. Wichtig: Das ist die Entwicklerplattform – ein vorhandenes ChatGPT-Abo hilft hier nicht weiter.
- In deinem Projekt: API Keys → Create new secret key
- Benenne den Key sinnvoll, z. B.
KI-Telefonassistent - Kopiere den Key sofort und speichere ihn im Passwortmanager – Keys werden nur einmal vollständig angezeigt
- Gehe zu Billing → Add payment method
- Lade ein Startguthaben auf (z. B. $10 für erste Tests)

2.2 Vorbereitung: IP-Telefon in der Fritz!Box einrichten
Damit Asterisk Anrufe von der Fritz!Box empfangen kann, muss er sich dort wie ein normales Telefon anmelden. Wir richten dafür ein virtuelles IP-Telefon ein – die Fritz!Box behandelt es genauso wie ein echtes Schnurlostelefon, leitet Anrufe dorthin weiter und übergibt sie per SIP-Protokoll.
Ulf: „Warum ein virtuelles Telefon?“ Kann Asterisk sich nicht einfach direkt einklinken?“
Tanja: „Stell dir vor, du willst in ein Bürogebäude rein. Du brauchst einen Ausweis. Das virtuelle IP-Telefon ist der Ausweis von Asterisk bei der Fritz!Box.“
- Melde dich an der Benutzeroberfläche deiner Fritz!Box 7590 an (meist
http://fritz.box) - Navigiere zu Telefonie -> Telefoniegeräte
- Klicke auf Neues Gerät einrichten
- Wähle Telefon (mit oder ohne Anrufbeantworter) aus und klicke auf „Weiter“
- Wähle LAN/WLAN (IP-Telefon) aus und gib dem Kind einen Namen (z. B.
Asterisk_KI)

- WICHTIG (Zugangsdaten): Im nächsten Schritt vergibst du einen Benutzernamen und ein Passwort. Notiere dir diese Daten gut!
- Benutzername:
KI_ASSISTANT_USER(muss mindestens 8 Zeichen haben) - Passwort: `DEIN_SICHERES_PASSWORT
- Registrar (SIP-Server):
fritz.boxoder deine Fritz!Box-IP
- Benutzername:

- Wähle die Rufnummer aus, auf die der KI-Assistent reagieren soll – deine Festnetznummer
- Bestätige die Einrichtung (ggf. musst du dies per Telefon-Code oder Tastendruck an der Fritz!Box verifizieren).
2.3 Vorbereitung: Synology NAS
Tanja: „Bevor wir Container bauen, legen wir die Ordnerstruktur an. Das ist wie der Werkzeugschrank, bevor man anfängt zu schrauben. Wer den überspringt, sucht hinterher alles.“
Bernd hebt die Hand: „Ich fange immer direkt an. Hab keine Zeit für Vorbereitung.“
Tanja:„Du hast letzte Woche zwei Stunden gesucht, wo du die compose.yaml hingelegt hast.“
Bernd schweigt.
Wir legen jetzt die Ordnerstruktur und die .env-Datei an, die später alle API-Keys und Zugangsdaten sicher aufbewahrt.
- Erstelle folgende Verzeichnisse auf der NAS:
/volume1/docker/asterisk/config/– hier kommen später alle SIP- und Dialplan-Dateien reinlogs/– damit wir sehen, was beim Telefonieren passiert
- Erstelle darin die Datei
/volume1/docker/asterisk/.env - Öffne auf deinem PC einen Texteditor (TextEdit, VS Code oder Notepad++) und füge folgenden Inhalt ein:
# OpenAI
OPENAI_API_KEY=DEIN_OPENAI_API_KEY
# Wird später gebraucht:
OPENAI_REALTIME_MODEL=gpt-realtime
N8N_WEBHOOK_BASE_URL=https://n8n.DEINE_DOMAIN.de
- Berechtigungen setzen: Rechtsklick auf den
asterisk-Ordner -> Eigenschaften -> Berechtigungen -> Nur dein Admin-User soll lesenden Zugriff haben.
Warum .env? Weil du damit in der compose.yaml sauber auf die Zugangsdaten referenzieren kannst, ohne sie direkt in YAML-Dateien oder Screenshots sichtbar zu machen. Das ist Best Practice – nicht Paranoia.
2.4 Das richtige Modell wählen – Qualität vs. Kosten
Aktuell stehen drei Realtime-Modelle zur Verfügung:
| Modell | Status | Audio Input / 1M Token | Audio Output / 1M Token | ca. Kosten pro Anruf (3 min) |
|---|---|---|---|---|
gpt-realtime | GA – produktionsreif | $32 | $64 | ~$0,12 |
gpt-realtime-mini | GA – kostengünstig | $10 | $20 | ~$0,04 |
gpt-4o-realtime-preview | Preview – Vorgänger | $40 | $80 | ~$0,15 |
Die Kosten pro Anruf sind Schätzwerte bei ca. 2 Minuten Nutzer-Audio und 1 Minute KI-Antwort. Längere Gespräche oder ausführliche System-Prompts erhöhen den Preis entsprechend.
Für dieses Projekt empfehle ich gpt-realtime. Es ist das erste allgemein verfügbare Realtime-Modell, wurde speziell für produktive Sprachagenten optimiert und bietet gegenüber gpt-4o-realtime-preview spürbar bessere Sprachqualität, zuverlässigeres Instruction Following – und liegt dabei 20 % günstiger. Besonders praktisch: gpt-realtime unterstützt neben WebRTC und WebSocket auch native SIP-Verbindungen – also genau das Protokoll, das Asterisk und die Fritz!Box sprechen.
Wer Kosten sparen möchte und keine hochkomplexen Gespräche erwartet, kann gpt-realtime-mini ausprobieren. Bei gelegentlichen Heimanrufen ist der Qualitätsunterschied im Alltag kaum spürbar – und die Kosten pro Anruf sinken auf einen Bruchteil.
3 Asterisk als Docker-Container einrichten und mit der Fritz!Box verbinden
Ulf: „Also jetzt bauen wir Asterisk?“
Tanja: „Jetzt bauen wir Asterisk. Aber wir machen es in zwei Schritten. Erst konfigurieren, dann starten, dann testen. Erst wenn der Test grünes Licht gibt, machen wir weiter.“
Bernd: „Ich würde alles auf einmal starten. Spart Zeit.“
Tanja: „Und wenn dann was nicht funktioniert, weißt du nicht wo.“
Bernd: „Stimmt, ist mir schon passiert.“
Asterisk läuft als Docker-Container auf deiner Synology. Er übernimmt eine zentrale Rolle: Er registriert sich bei der Fritz!Box wie ein IP-Telefon, nimmt eingehende Anrufe entgegen und reicht sie an die OpenAI-Brücke weiter. Alles Folgende baut auf dieser Verbindung auf – deshalb testen wir sie explizit, bevor wir weitermachen.
3.1 Docker-Setup: Die compose.yaml
Die compose.yaml ist dein Bauplan. Anstatt kryptische Befehle in die Konsole zu tippen, definierst du hier einmalig, welche Dateien in den Container gemountet werden und wie er sich verhalten soll.
- Öffne einen Texteditor (z.B. TextEDIT, VS Code oder Notepad++) und erstelle eine neue Datei namens
compose.yaml - Kopiere den folgenden Block hinein:
Hinweis: In meinem Fall läuft n8n bereits, daher fügen wir hier nur den Asterisk-Teil hinzu. Wie man n8n installiert siehe https://www.foundic.org/n8n-selbst-hosten-synology-nas-docker-installation
version: "3.8"
services:
asterisk:
image: andrius/asterisk:20
container_name: asterisk_ki
restart: unless-stopped
network_mode: "host"
healthcheck:
disable: true
volumes:
# === Config-Dateien (read-only) ===
- /volume1/docker/asterisk/config/pjsip.conf:/etc/asterisk/pjsip.conf:ro
- /volume1/docker/asterisk/config/extensions.conf:/etc/asterisk/extensions.conf:ro
- /volume1/docker/asterisk/config/modules.conf:/etc/asterisk/modules.conf:ro
- /volume1/docker/asterisk/config/logger.conf:/etc/asterisk/logger.conf:ro
- /volume1/docker/asterisk/config/rtp.conf:/etc/asterisk/rtp.conf:ro
- /volume1/docker/asterisk/config/ari.conf:/etc/asterisk/ari.conf:ro
- /volume1/docker/asterisk/config/http.conf:/etc/asterisk/http.conf:ro
# === Logs (read-write) ===
- /volume1/docker/asterisk/logs:/var/log/asterisk:rw
environment:
- TZ=Europe/Berlin
- ASTERISK_UID=0
- ASTERISK_GID=0
- Kopiere die fertige Datei nach
/volume1/docker/asterisk/
Warum network_mode: "host"?
Ulf zeigt auf die compose.yaml :„Moment, was bedeutet das hier network_mode: host? Klingt irgendwie gefährlich.“
Tanja: „Ist es nicht, aber die Erklärung braucht eine Minute.“
Bernd: „Ich kenn das, NAT. Hab ich mal bei uns im Büro eingerichtet. Hat nicht funktioniert.“
Tanja: „Genau das ist das Problem. Stell dir vor, der Docker-Container ist ein Mitarbeiter, der in einem abgeschlossenen Glaskasten sitzt. Er kann nach draußen telefonieren, aber wenn jemand zurückruft, landet der Anruf an der Glaskasten-Tür, und der Container hört nichts. Das nennt sich One-Way-Audio: du hörst die andere Seite, aber sie hört dich nicht. Oder umgekehrt.“
Ulf: „Das klingt wie beim Fußball wenn das Headset des Torwarts kaputt ist. Der Trainer brüllt, aber der Keeper hört nix.“
Tanja: „Perfekter Vergleich. Mit network_mode: host reißen wir den Glaskasten weg. Der Container verhält sich dann so, als wäre er direkt die DiskStation – gleiche IP, gleiche Ports, kein NAT dazwischen. Asterisk und die Fritz!Box können sich problemlos gegenseitig hören.“
Ulf: „Und das ist sicher?“
Tanja: „Für diesen Anwendungsfall: ja. Asterisk lauscht nur auf den Ports, die wir ihm erlauben. Der Kompromiss ist bewusst eingegangen.“
Bernd nickt langsam. „Ich hätte einfach alle Ports geöffnet.“
Tanja: „Das, wäre nicht sicher.“
Wichtig: Port 5060 muss frei sein. Mit network_mode: "host" kann Asterisk nur dann sauber SIP sprechen, wenn auf der DiskStation niemand sonst bereits auf Port 5060 (UDP/TCP) lauscht. Wenn der Port belegt ist, startet Asterisk zwar – aber die SIP-Registrierung schlägt still fehl, oder Calls brechen unerwartet ab.
Den RTP-Portbereich definieren wir nicht in der compose.yaml, sondern in der rtp.conf – das ist der richtige Ort dafür.

3.2 Docker-Setup: Asterisk-Container-Konfiguration
Ulf: „Sechs Dateien? Für einen Telefonserver?“
Tanja: „Jede macht genau eine Sache. Stell dir vor, du richtest eine neue Werkstatt ein. Du hast einen Schrank für Werkzeuge, einen für Schrauben, einen für die Bedienungsanleitungen. Du könntest alles in eine einzige Schublade werfen.“
Bernd: „Mach ich so“
Ulf: „… aber dann weißt du hinterher nicht mehr, wo was ist, wenn etwas nicht funktioniert.“ Tanja zählt an den Fingern ab: „pjsip.conf: wie Asterisk mit der Fritz!Box redet. rtp.conf: welche Ports für Audio reserviert sind. extensions.conf: was bei einem eingehenden Anruf passiert. modules.conf: welche Asterisk-Funktionen geladen werden. logger.conf: was ins Log geschrieben wird. asterisk.conf: globale Grundeinstellungen.“
Ulf: „Also sechs Schubladen, jede beschriftet“,
Tanja: „Genau. Wenn später Audio nicht funktioniert, schaust du in rtp.conf. Wenn Asterisk sich nicht bei der Fritz!Box registriert, schaust du in pjsip.conf. Du musst nicht eine riesige Datei durchsuchen, sondern weißt sofort wo das Problem liegt.“
Bernd: „Ich hätte alles in eine Datei gepackt“.
Tanja: „Das weiß ich. Fang einfach oben an. Eine Datei nach der anderen.“
Wir erstellen im Verzeichnis /volume1/docker/asterisk/config/ diese sechs Dateien:
pjsip.conf→ SIP-Registrierung bei der Fritz!Boxrtp.conf→ RTP-Portbereich (Real-time Transport Protocol für Audiodaten)extensions.conf→ Minimaler Dialplan (was passiert, wenn ein Anruf reinkommt)modules.conf→ welche Asterisk-Module geladen werdenlogger.conf→ welche Log-Nachrichten wo landenasterisk.conf→ globale Asterisk-Einstellungen
3.2.1 pjsip.conf – SIP zur Fritz!Box
PJSIP ist der moderne SIP-Stack von Asterisk und wird von der Fritz!Box sauber unterstützt. Diese Datei definiert, wie Asterisk sich bei der Fritz!Box anmeldet und Anrufe entgegennimmt.
Datei: /volume1/docker/asterisk/config/pjsip.conf
[global]
type=global
user_agent=Asterisk PBX
; ===== UDP TRANSPORT =====
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0
; ===== FRITZBOX AUTHENTICATION =====
[fritzbox-auth]
type=auth
auth_type=userpass
username=KI_FRITZBOX_USER
password=DEIN_SICHERES_FRITZBOX_PASSWORT
; ===== OUTBOUND REGISTRATION =====
[fritzbox-reg]
type=registration
transport=transport-udp
outbound_auth=fritzbox-auth
server_uri=sip:fritz.box
client_uri=sip:KI_FRITZBOX_USER@fritz.box
retry_interval=60
expiration=3600
; ===== AOR (Address of Record) =====
[fritzbox-aor]
type=aor
max_contacts=1
remove_existing=yes
; ===== IDENTIFY - Erkennt FritzBox =====
[fritzbox-identify]
type=identify
endpoint=fritzbox
match=DEINE_FRITZBOX_IP
; ===== ENDPOINT (für ein- UND ausgehende Anrufe) =====
[fritzbox]
type=endpoint
transport=transport-udp
context=from-fritzbox
aors=fritzbox-aor
auth=fritzbox-auth
outbound_auth=fritzbox-auth
disallow=all
allow=alaw
allow=ulaw
direct_media=no
force_rport=yes
rewrite_contact=yes
rtp_symmetric=yes
from_user=KI_FRITZBOX_USER
from_domain=fritz.box
Diese Platzhalter musst du ersetzen:
KI_FRITZBOX_USER→ Benutzername aus der Fritz!Box (Schritt 2.2)DEIN_SICHERES_FRITZBOX_PASSWORT→ SIP-Passwort aus der Fritz!BoxDEINE_FRITZBOX_IP→ z. B.192.168.0.1
3.2.2 rtp.conf – Audio-Ports
Wir legen den RTP-Portbereich explizit fest, damit Audio vorhersagbar läuft. RTP ist das Protokoll, das den eigentlichen Audiostrom überträgt – ohne definierten Portbereich könnte Asterisk beliebige Ports verwenden, was Firewalls und NAT noch chaotischer macht.
Datei: /volume1/docker/asterisk/config/rtp.conf
[general]
rtpstart=20000
rtpend=20255
Firewall-Hinweis: Falls die Firewall auf der Diskstation aktiv ist, lege Regeln an für Port 5060 (UDP) und den Bereich 20000–20255 (UDP).
3.2.3 extensions.conf – Minimal-Dialplan
Ulf: „Was macht der Dialplan?“
Tanja: „Stell dir vor, Asterisk ist ein Empfangstresen. Der Dialplan ist die Arbeitsanweisung: Wenn jemand anruft, was passiert dann? Wohin wird der Anruf weitergeleitet?“ Sie tippt kurz. „Im Moment sagen wir nur: Annehmen, kurz warten, an die OpenAI-Brücke übergeben.“
Datei: /volume1/docker/asterisk/config/extensions.conf
[general]
static=yes
writeprotect=no
[from-fritzbox]
; Eingehender Anruf von FritzBox → an OpenAI Relay weiterleiten
exten => s,1,NoOp(Eingehender Anruf von FritzBox - CID=${CALLERID(all)})
same => n,Answer()
same => n,Wait(0.5)
same => n,NoOp(Uebergabe an OpenAI Realtime via Stasis...)
same => n,Stasis(asterisk_to_openai_rt)
same => n,Hangup()
; Catch-all für alle Nummern, die die FritzBox sendet
exten => _X.,1,Goto(from-fritzbox,s,1)
Ein paar Erläuterungen für den Fall, dass du später debuggen musst:
exten => _X.,1fängt jede gerufene Nummer ab – die Fritz!Box übergibt oft konkrete MSN-Nummern, und so landest du sicher im DialplanNoOp(...)schreibt ins Log, welche Nummer Asterisk tatsächlich sieht – hilft enorm beim FehlersuchenWait(0.5)entschärft bei manchen Setups Race-Conditions beim Audiostart
3.2.4 modules.conf, logger.conf und asterisk.conf
Diese drei Dateien sind der Maschinenkeller. Unspektakulär, aber ohne sie startet Asterisk nicht sauber.
Datei: /volume1/docker/asterisk/config/modules.conf
[modules]
autoload=yes
; Sicherheitshalber kritische ARI-Module explizit laden
load = res_http_websocket.so
load = res_ari.so
load = res_ari_channels.so
load = res_ari_bridges.so
load = res_ari_events.so
load = res_stasis.so
load = res_stasis_answer.so
load = res_stasis_playback.so
load = res_stasis_recording.so
load = res_stasis_snoop.so
load = chan_rtp.so
Asterisk hat hunderte Module. autoload=yes lädt alle automatisch – die kritischen ARI-Module werden zusätzlich explizit genannt, damit Asterisk sie auf jeden Fall findet.
Datei: /volume1/docker/asterisk/config/logger.conf
[general]
dateformat=%F %T
[logfiles]
console => notice,warning,error,verbose(5)
messages => notice,warning,error,verbose(5)
Datei: /volume1/docker/asterisk/config/asterisk.conf
[directories]
astetcdir => /etc/asterisk
astvarlibdir => /var/lib/asterisk
astdbdir => /var/lib/asterisk
astmoddir => /usr/lib/asterisk/modules
astspooldir => /var/spool/asterisk
astrundir => /var/run/asterisk
astlogdir => /var/log/asterisk
[options]
verbose = 3
debug = 0
Warum so minimal? Damit Asterisk sicher startet und wir die wichtigen Meldungen direkt auf der Konsole sehen. Datei-Logs können wir später gezielt ergänzen.

3.3 Asterisk im Docker starten
Alle Dateien liegen an ihrem Platz. Jetzt starten wir den Container – über den Container Manager auf der Diskstation.
- Container Manager öffnen
- DSM -> Container Manager
- Neues Projekt erstellen
- Links: Projekt
- Erstellen
- Projektname:
asterisk_ki
- compose.yml auswählen
- Von bestehender compose.yml
- Pfad auswählen:
/volume1/docker/asterisk/compose.yml - Weiter
- Projekt starten
- Erstellen & starten (oder „Fertigstellen“ -> „Starten“)

Firewall-Erinnerung: Falls die Firewall auf der Synology aktiv ist, jetzt Regeln anlegen: Port 5060 (UDP) und Port 20000–20255 (UDP).

3.4 Prüfen, ob Asterisk bei der Fritz!Box registriert ist
Ulf: „Und jetzt einfach anrufen und schauen ob’s geht?“
Tanja: „Nein, erst verifizieren wir, dass Asterisk sich registriert hat. Erst dann testen wir den Anruf. Wer diesen Schritt überspringt, verbringt anschließend eine Stunde damit, im Dunkeln zu tappen.“
Bernd: „Ich hätte einfach angerufen“
Tanja: „Das weiß ich.“
Wir schauen jetzt direkt in die Asterisk-Konsole. Dafür gibt es zwei Wege:
Weg 1 – über den Container Manager:
- Container Manager öffnen → Container
asterisk_kiauswählen - Tab Terminal → Erstellen →
bashwählen - Befehl eingeben:
asterisk -rvvv - Sobald der Prompt
asterisk*CLI>erscheint, eingeben:pjsip show registrations
Weg 2 – über SSH: - SSH auf der Diskstation aktivieren: Systemsteuerung → Terminal & SNMP → SSH-Dienst aktivieren
- Terminal öffnen und einloggen:
ssh DEIN_DSM_USER@DEINE_DISKSTATION_IP - In die Asterisk-CLI:
docker exec -it asterisk_ki asterisk -rvvv - Registrierung anzeigen:
pjsip show registrations
Das Ergebnis sollte so aussehen:
<Registration/ServerURI..............................> <Auth..........> <Status.......>
==========================================================================================
fb-trunk/sip:192.168.178.1 fb-auth Registered
Das Wort Registered ist dein grünes Licht. Alles andere – Failed, Rejected, gar keine Ausgabe – bedeutet: Zugangsdaten prüfen, Fritz!Box-Konfiguration nochmal kontrollieren, erst dann weitermachen.

4 Die OpenAI-Brücke: WebSocket-Relay einrichten und KI-Gespräche aktivieren
Ulf: „Asterisk läuft, Fritz!Box meldet ‚Registered‘, was kommt jetzt?“.
Tanja: „Jetzt, bauen wir den spannendsten Teil. Das Relay. Der zweite Docker-Container, der Asterisk und OpenAI miteinander verbindet.“
Bernd:„Ich würde das alles in einem Container zusammenstecken“.
Tanja: „Dann hast du einen Container, der alles macht und bei dem du nichts mehr einzeln debuggen kannst. Zwei Container, zwei Verantwortlichkeiten – das ist sauberer.“
Bernd: „Klingt nach mehr Arbeit.“
Tanja: „Klingt nach weniger Kopfschmerzen.“
Das Relay übersetzt zwei grundlegend verschiedene Welten: auf der einen Seite das klassische RTP-Audioprotokoll von Asterisk, auf der anderen den WebSocket-Stream, den OpenAI erwartet. Sobald diese Brücke steht, übernimmt OpenAI das komplette Gespräch – Spracherkennung, Antwortgenerierung und Text-to-Speech in Echtzeit.
4.1 Asterisk-to-OpenAI-Realtime (WebSocket Bridge)
Wir erweitern die Ordnerstruktur um den openai-relay-Container:
/volume2/docker/
├── asterisk/
│ ├── compose.yaml
│ ├── config/
│ │ ├── pjsip.conf
│ │ ├── extensions.conf
│ │ ├── ari.conf
│ │ ├── http.conf
│ │ ├── modules.conf
│ │ ├── logger.conf
│ │ └── rtp.conf
│ └── logs/
└── openai-relay/
├── compose.yaml
├── Dockerfile
└── config.conf
Drei neue Dateien im Verzeichnis openai-relay: das Dockerfile, die compose.yaml und die config.conf. Schauen wir uns alle drei an.
4.2 Das Dockerfile
Das Dockerfile baut das Docker-Image direkt aus einem öffentlichen Repository – du musst keinen Code manuell klonen oder pflegen.
Erstelle die Datei /volume1/docker/openai-relay/Dockerfile:
FROM node:20-slim
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Community Edition klonen
RUN git clone https://github.com/infinitocloud/asterisk_to_openai_rt_community.git .
RUN npm install
ENV NODE_ENV=production
CMD ["node", "index.js"]
4.3 Die compose.yaml für openai-relay
Da Asterisk im network_mode: host läuft, muss das Relay dasselbe tun – sonst erreicht es die RTP-Audio-Ports nicht.
Erstelle die Datei /volume1/docker/openai-relay/compose.yaml:
version: "3.8"
services:
openai-relay:
build: .
container_name: openai-relay
restart: unless-stopped
network_mode: "host"
volumes:
# config.conf wird in den Container gemountet
- /volume1/docker/openai-relay/config.conf:/app/config.conf:ro
environment:
- TZ=Europe/Berlin
- NODE_ENV=production
depends_on: []
# Hinweis: asterisk_ki muss ZUERST gestartet werden,
# da openai-relay sich per ARI verbindet.
# Da die Container in separaten compose-Dateien sind,
# manuell sicherstellen: erst asterisk_ki, dann openai-relay.
4.4 Asterisk Core-Konfiguration: ari.conf und http.conf
Ulf: „Was ist ARI?“.
Tanja: „ARI steht für Asterisk REST Interface. Es ist die Schnittstelle, über die das Node.js-Relay mit Asterisk kommuniziert. Ohne ARI kann das Relay keinen Anruf übernehmen.“
Ulf: „Das klingt nach dem Trainertelefon zur Bank. Der Coach kann dem Ersatzspieler Anweisungen geben, ohne auf das Feld zu müssen.“
Tanja: „Exakt“.
Diese beiden Dateien sind das Bindeglied zwischen Asterisk und dem Relay. Besonders wichtig: type=user in der ari.conf – das ist häufig der Grund, warum ARI-Verbindungen still scheitern.
Datei: /volume1/docker/asterisk/config/ari.conf
[general]
enabled=yes
pretty=yes
allowed_origins=*
[ari_user]
type=user
read_only=no
password=dein_ari_passwort
Diese Platzhalter musst du ersetzen:
- ARI_USERNAME=ari_user -> frei wählbarer Benutzername
- ARI_PASSWORD=dein_ari_passwort -> frei wählbares Passwort (merken, kommt gleich wieder)
Datei: /volume1/docker/asterisk/config/http.conf
[general]
enabled=yes
bindaddr=127.0.0.1
bindport=8088
127.0.0.1 statt 0.0.0.0: Da beide Container auf dem Host-Netz der Synology laufen, braucht der HTTP-Port nicht nach außen offen zu sein. Das ist eine einfache, effektive Sicherheitsmaßnahme.
4.5 Das config.conf – Schaltzentrale des Relays
Diese Datei ist das Herzstück des openai-relay-Containers. Hier trägst du ein, mit welchem OpenAI-Key das Relay spricht, wie es sich bei Asterisk anmeldet und was die KI beim Gespräch sagen soll.
Erstelle die Datei /volume1/docker/openai-relay/config.conf:
# OpenAI Realtime API Key
OPENAI_API_KEY=OpenAI_Projekt_API_Key
# ARI Credentials (muessen mit ari.conf uebereinstimmen)
ARI_URL=http://127.0.0.1:8088
ARI_USERNAME=ari_user
ARI_PASSWORD=dein_ari_passwort
# Anruf Limit von 5 Minuten
CALL_DURATION_LIMIT_SECONDS=300
# System-Prompt fuer den Assistenten
SYSTEM_PROMPT=Du bist ein professioneller deutschsprachiger Telefonassistent. Fuehre das Gespraech in dieser Reihenfolge: 1) Begruesse den Anrufer freundlich und sage dass gerade niemand erreichbar ist und du die KI Telefon-Assistentin bist. 2) Frage nach dem Namen des Anrufers falls nicht bereits genannt. 3) Frage worum es geht und hoere aufmerksam zu. 4) Frage wie dringend das Anliegen ist. 5) Fasse das Anliegen kurz zusammen und bestaetige dass du die Nachricht weiterleiten wirst. 6) Verabschiede dich freundlich. Antworte immer auf Deutsch, kurz und praezise. Vermeide lange Monologe.
# Logging
LOG_LEVEL=info
Diese Platzhalter ersetzen:
- OpenAI_Projekt_API_Key -> dein OpenAI API-Key aus Schritt 2.1
- ARI_USERNAME=ari_user -> vdie Werte aus
ari.conf - ARI_PASSWORD=dein_ari_passwort -> die Werte aus
ari.conf
Den SYSTEM_PROMPT kannst du frei anpassen. Das ist die Persönlichkeit deines KI-Assistenten. Halte ihn konkret und strukturiert: Je klarer die Reihenfolge, desto zuverlässiger arbeitet das Modell.

4.6 Stimme der KI ändern
OpenAI bietet verschiedene Stimmen für die Realtime API an. Standard ist alloy – du kannst aber auf andere Stimmen wie echo, fable, onyx, nova oder shimmer wechseln.
Um die Stimme zu ändern, kopierst du zunächst die Konfigurationsdatei aus dem laufenden Container auf die NAS:
sudo docker cp openai-relay:/app/config.js /volume2/docker/openai-relay/config.js
Füge in dieser config.js nach dem zweiten Eintrag folgende Zeile ein:
OPENAI_VOICE: process.env.OPENAI_VOICE || 'alloy',
Damit der Container diese Datei auch findet, erweitere die compose.yaml um den zweiten Volume-Eintrag:
volumes:
- /volume2/docker/openai-relay/config.conf:/app/config.conf:ro
- /volume2/docker/openai-relay/config.js:/app/config.js:ro
Und trage in der config.conf ein:
OPENAI_VOICE=alloy
Danach den Container neu starten . Die neue Stimme ist aktiv.
4.7 Webhook für n8n Workflow vorbereiten
Aktuell werden Transkripte nur in die Konsole geloggt und gehen nach dem Auflegen verloren. Jetzt erweitern wir das System so, dass nach jedem Gespräch automatisch ein Transkript an n8n geschickt wird.
Ulf: „Moment, wir müssen Code in fertigen Dateien ändern?“
Tanja: „Wir kopieren die Dateien erst raus, ändern sie, und mounten sie dann als Volume in den Container rein. Der Container bleibt unberührt. Wir steuern sein Verhalten von außen.“
Ulf: „Das ist wie beim Fußball“. Der Spieler ist unveränderlich, aber der Trainer gibt ihm von außen neue Anweisungen.“
Tanja: „Genau.“
Schritt 1: Es müssen folgende JavaScript-Dateien aus dem Container kopiert werden, damit wir sie anpassen und als Volume mounten können.
sudo docker cp openai-relay:/app/config.js /volume1/docker/openai-relay/config.js
sudo docker cp openai-relay:/app/asterisk.js /volume1/docker/openai-relay/asterisk.js
sudo docker cp openai-relay:/app/openai.js /volume1/docker/openai-relay/openai.js
Schritt 2: In /volume1/docker/openai-relay/config.js suche nach INITIAL_MESSAGE: und füge direkt darüber ein:
OPENAI_VOICE: process.env.OPENAI_VOICE || 'alloy',
N8N_WEBHOOK_URL: process.env.N8N_WEBHOOK_URL || '',
INITIAL_MESSAGE: process.env.INITIAL_MESSAGE || 'Hi',
Schritt 3: config.conf ergänzen
Füge am Ende der /volume1/docker/openai-relay/config.conf hinzu:
OPENAI_VOICE=alloy
N8N_WEBHOOK_URL=https://n8n.civicgem.org/webhook/NEUE_WEBHOOK_ID
Hinweis: Die NEUE_WEBHOOK_ID wird erst im nächsten Schritt beim Aufbau des n8n-Workflows definiert. Trag hier zunächst einen Platzhalter ein und ersetze ihn danach.
Schritt 4: compose.yaml aktualisieren
Erweitere die Volumes in /volume1/docker/openai-relay/compose.yaml:
volumes:
- /volume2/docker/openai-relay/config.conf:/app/config.conf:ro
- /volume2/docker/openai-relay/config.js:/app/config.js:ro
- /volume2/docker/openai-relay/asterisk.js:/app/asterisk.js:ro
- /volume2/docker/openai-relay/openai.js:/app/openai.js:ro
Aktuell werden die Transkripte nur in die Konsole geloggt und gehen nach dem Anruf verloren. Wir müssen openai.js so erweitern, dass die Transkripte während des Gesprächs gesammelt und nach Gesprächsende per HTTP POST an n8n geschickt werden.
Schritt 5: openai.js erweitern – Transkript sammeln
Suche in /volume1/docker/openai-relay/openai.js die Zeile (ca. Zeile 88):
let lastUserItemId = null;
Füge direkt darunter ein:
let transcriptLog = [];
Dann suche diesen Block (ca. Zeile 164-168):
if (role === 'User') {
logOpenAI`User command transcription for ${channelId}: ${response.transcript}`, 'info');
} else {
logOpenAI(`Assistant transcription for ${channelId}: ${response.transcript}`, 'info');
}
Ersetze ihn durch:
if (role === 'User') {
logOpenAI(`User command transcription for ${channelId}: ${response.transcript}`, 'info');
transcriptLog.push({ role: 'user', text: response.transcript, time: new Date().toISOString() });
} else {
logOpenAI(`Assistant transcription for ${channelId}: ${response.transcript}`, 'info');
transcriptLog.push({ role: 'assistant', text: response.transcript, time: new Date().toISOString() });
}
Dann suche (ca. Zeile 167)
logOpenAI`User command transcription for ${channelId}: ${response.transcript}`, 'info');
}
break;
case 'response.audio.done':
Ersetze die eine Zeile:
logOpenAI(`User command transcription for ${channelId}: ${response.transcript}`, 'info');
durch:
logOpenAI(`User command transcription for ${channelId}: ${response.transcript}`, 'info');
transcriptLog.push({ role: 'user', text: response.transcript, time: new Date().toISOString() });
Zum Schluss: Suche sipMap.set(channelId, channelData); innerhalb von connectWebSocket (ca. Zeile 246) und füge direkt davor ein:
channelData.transcriptLog = transcriptLog;
Suche diesen Block (ca. Zeile 180):
case 'conversation.item.input_audio_transcription.completed':
if (response.transcript) {
logger.debug(`User transcript completed - Full message: ${JSON.stringify(response, null, 2)}`);
logOpenAI(`User command transcription for ${channelId}: ${response.transcript}`, 'info');
}
break;
Ersetze durch:
case 'conversation.item.input_audio_transcription.completed':
if (response.transcript) {
logger.debug(`User transcript completed - Full message: ${JSON.stringify(response, null, 2)}`);
logOpenAI(`User command transcription for ${channelId}: ${response.transcript}`, 'info');
transcriptLog.push({ role: 'user', text: response.transcript, time: new Date().toISOString() });
}
break;
Suche Zeile (ca. Zeile 224):
input_audio_transcription: {
model: 'whisper-1',
language: 'en'
},
Suche language: en und ersetze durch de für Deutsche Sprache
language: 'de'
Schritt 6: asterisk.js erweitern – Transkript an n8n senden
…/docker/openai-relay/asterisk.js erweitern** — nach Gesprächsende die gesammelten Transkripte an n8n senden.
Änderung 1: Ganz oben nach der letzten require-Zeile einfügen:
const http = require('http');
const https = require('https');
Änderung 2: Suche in der cleanupChannel-Funktion diese Zeile:
} catch (e) {
logger.error(`Cleanup error for ${channelId}: ${e.message}`);
} finally {
cleanupPromises.delete(channelId);
cleanupPromises.delete(`ws_${channelId}`);
}
Füge direkt davor (also vor } finally {) ein:
// Send transcript to n8n webhook
if (config.N8N_WEBHOOK_URL && channelData.transcriptLog && channelData.transcriptLog.length > 0) {
try {
const payload = JSON.stringify({
channelId: channelId,
callerName: channelData.channel?.caller?.name || 'Unbekannt',
callerNumber: channelData.channel?.caller?.number || 'Unbekannt',
callStart: channelData.transcriptLog[0]?.time || new Date().toISOString(),
callEnd: new Date().toISOString(),
transcript: channelData.transcriptLog
});
const url = new URL(config.N8N_WEBHOOK_URL);
const client = url.protocol === 'https:' ? https : http;
const req = client.request(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
}, (res) => {
logger.info(`n8n webhook response for ${channelId}: ${res.statusCode}`);
});
req.on('error', (e) => logger.error(`n8n webhook error for ${channelId}: ${e.message}`));
req.write(payload);
req.end();
logger.info(`Transcript sent to n8n for ${channelId} (${channelData.transcriptLog.length} entries)`);
} catch (e) {
logger.error(`Failed to send transcript to n8n for ${channelId}: ${e.message}`);
}
}
4.8 Cloudfare öffenen für Webhooks
Ulf: „Ich nutze Cloudflare mit Zero Trust für n8n. Muss ich da was anpassen?“
Tanja: „Ja, Zero Trust blockiert standardmäßig alle eingehenden Requests, die nicht authentifiziert sind – also auch Webhooks von OpenAI. Wir müssen einen Bypass für den Webhook-Pfad einrichten.“
Wenn du Cloudflare mit Zero Trust für n8n nutzt, werden Webhooks grundsätzlich geblockt. Wir richten eine Ausnahme speziell für den /webhook/-Pfad ein. Zur EInrichtung von Cloudflare siehe https://www.foundic.org/n8n-cloudflare-tunnel-synology-nas.
Teil 1 – Neue Access Application anlegen:
- Cloudflare Zero Trust Dashboard öffnen:
- Gehe zu: https://one.dash.cloudflare.com/
- Melde dich an
- Wähle dein Team/Account
- Access Application finden:
- Navigiere zu: Protect & Connect -> Zero Trust -> Access controls -> Applications
- Finde deine n8n-Application in der Liste
- Wähle „+ Add an application“
- Select: Self-hosted
- Applciation name: n8n-webhook
- Session Duration: 24hours
- „+ Add public hostname“
- Subdomain: die bisher benutzte
- Domain: deine Domain
- Path:
webhook/*
- Wähle links im Menü „Policies“ aus
- Klicke auf „Add a policy“
- Policy konfigurieren:
Policy Name:n8n Webhook Bypass
Action: „Bypass“
Session duration:Same as application session timeout
Selection: Everyone
Klicke auf „Save“
- Policy konfigurieren:

Teil 2 – WAF-Regel erstellen:
Gehe im Cloudflare Dashboard zur obersten Kontoebene -> Domains -> deine Domain -> (links im Menü) Security -> Overview -> in der Mitte im Kasten „Web app exploits“ -> Manage custom rules -> + Create rule -> Custom rules
- Rule name:
Allow n8n Webhook - Field:
URI Path…, Operator:starts with, Value: ../webhook/ - Choose action: Skip
- Aktiviere alle Sicherheitsoptionen (WAF, Super Bot Fight Mode etc.), damit OpenAI ungehindert durchkommt
- Klicke auf Deploy

4.9 n8n Workflow bauen
Ulf: „Jetzt kommt der Teil, den ich kenne, n8n.“
Tanja: „Fast, erst brauchen wir noch ein App-Passwort für Gmail. Das normale Google-Passwort geht nicht.“
Ulf: „Warum nicht?“
Tanja: „Weil Google beim normalen Passwort verlangt, dass du dich per Browser verifizierst. Eine Maschine kann das nicht. Ein App-Passwort ist ein separater 16-stelliger Code, der nur für automatisierte Zugänge gedacht ist.“
Ulf: „Und wenn ich keins hab?“
Tanja: „Dann bleibt die E-Mail aus.“
Schritt 1: Gmail App-Passwort erstellen
Falls der n8n Workflow eine Zusammenfassung an Gmail bzw. Googel Mail senden soll brauchst Du ein App-Passwort (nicht Dein normales Gmail-Passwort). So richtest Du es ein:
Gmail App-Passwort erstellen:
- Gehe zu https://myaccount.google.com/apppasswords
- (2-Faktor-Authentifizierung muss aktiv sein)
- App-Name:
n8n - Du bekommst ein 16-stelliges Passwort – kopiere es
Schritt 2: SMTP Credentials in n8n anlegen
SMTP Credentials anlegen in n8n anlegen
- n8n -> Settings -> Credentials -> Add Credential -> Typ: SMTP
- Name:
SMTP Gmail account - User:
deine-adresse@gmail.com - Password: dein 16-stelliges App-Passwort
- Host:
smtp.gmail.com - Port:
465 - SSL/TLS: Ein
- Speichern

Schritt 3: Die drei Workflow-Nodes
Erstelle einen neuen n8n-Workflow mit drei Nodes hintereinander.
Node 1: Webhook
- Method:
POST - Path:
anruf-zusammenfassung - Authentication:
None

Node 2: Code in JavaScript
- Mode:
Run Once for All Items - Language:
JavaScript
// Transkript formatieren
const body = $input.first().json.body;
const transcript = body.transcript || [];
const callerName = body.callerName || 'Unbekannt';
const callerNumber = body.callerNumber || 'Unbekannt';
const callStart = body.callStart ? new Date(body.callStart).toLocaleString('de-DE', { timeZone: 'Europe/Berlin' }) : 'Unbekannt';
const callEnd = body.callEnd ? new Date(body.callEnd).toLocaleString('de-DE', { timeZone: 'Europe/Berlin' }) : 'Unbekannt';
// Gespraechsverlauf als Text
let gespraech = '';
for (const entry of transcript) {
const rolle = entry.role === 'assistant' ? 'Anna (KI)' : 'Anrufer';
gespraech += `${rolle}: ${entry.text}\n\n`;
}
// Zusammenfassung erstellen
let zusammenfassung = `ANRUF-ZUSAMMENFASSUNG\n`;
zusammenfassung += `========================\n\n`;
zusammenfassung += `Anrufer: ${callerName}\n`;
zusammenfassung += `Telefonnummer: ${callerNumber}\n`;
zusammenfassung += `Anruf Beginn: ${callStart}\n`;
zusammenfassung += `Anruf Ende: ${callEnd}\n\n`;
zusammenfassung += `GESPRAECHSVERLAUF:\n`;
zusammenfassung += `------------------------\n\n`;
zusammenfassung += gespraech;
// HTML Version fuer E-Mail
let html = `<h2>Anruf-Zusammenfassung</h2>`;
html += `<table style="border-collapse:collapse; margin-bottom:20px;">`;
html += `<tr><td style="padding:4px 12px 4px 0; font-weight:bold;">Anrufer:</td><td>${callerName}</td></tr>`;
html += `<tr><td style="padding:4px 12px 4px 0; font-weight:bold;">Telefonnummer:</td><td>${callerNumber}</td></tr>`;
html += `<tr><td style="padding:4px 12px 4px 0; font-weight:bold;">Beginn:</td><td>${callStart}</td></tr>`;
html += `<tr><td style="padding:4px 12px 4px 0; font-weight:bold;">Ende:</td><td>${callEnd}</td></tr>`;
html += `</table>`;
html += `<h3>Gespr\u00e4chsverlauf</h3>`;
for (const entry of transcript) {
const rolle = entry.role === 'assistant' ? 'Anna (KI)' : 'Anrufer';
const color = entry.role === 'assistant' ? '#2563eb' : '#059669';
html += `<p><strong style="color:${color};">${rolle}:</strong> ${entry.text}</p>`;
}
const subject = `Anruf von ${callerName} (${callerNumber}) - ${callStart}`;
return [{ json: { subject, text: zusammenfassung, html, callerName, callerNumber } }];

Als letzten Node 3: Send Email erstellen
- Credential to connect with:
SMTP Gmail account - Operations:
Send - From Email: deine E-Mail-Adresse
- To Email: deine E-Mail-Adresse
- Subject:
{{ $json.subject }} - Email Format:
HTML - HTML:
{{ $json.html }}

Schritt 4: Workflow aktivieren
Aktiviere den Workflow über Publish und kopiere die Webhook-URL aus dem Webhook-Node. Sie sieht etwa so aus:
https://n8n.deine-domain.de/webhook/anruf-zusammenfassung

Schritt 5: URL in config.conf eintragen
Öffne /volume1/docker/openai-relay/config.conf und trage die URL ein:
N8N_WEBHOOK_URL=https://n8n.deine-domain.de/webhook/anruf-zusammenfassung
Starte den openai-relay-Container neu. Jetzt sollte es funktionieren.
4.10 Optional: KI legt nach der Verabschiedung selbst auf
Ulf: „Legt die KI eigentlich selbst auf?“
Tanja: „Standardmäßig nicht. Sie verabschiedet sich und wartet dann. Der Anrufer muss auflegen.“
Ulf: „Das ist unhöflich. Ich würde das ändern.“
Tanja: „Du kannst. Es ist optional, aber es macht das Gespräch deutlich runder.“
Die Logik ist einfach: Wenn die KI sich verabschiedet – erkannt an Schlüsselwörtern im Transkript – startet ein Timer. Sagt der Anrufer innerhalb von 8 Sekunden nichts mehr, legt das System auf.
Alle Änderungen in /volume1/docker/openai-relay/openai.js:
Änderung 1: Nach der Zeile let transcriptLog = []; (ca. Zeile 95) einfügen:
let goodbyeTimer = null;
let goodbyeDetected = false;
const GOODBYE_TIMEOUT_MS = 8000;
const goodbyePattern = /wiedersehen|wiederh.ren|tsch.ss|sch.nen tag|guten tag noch/i;
Änderung 2: Im response.audio_transcript.done-Block, nach dem transcriptLog.push-Aufruf im else-Zweig (KI-Antwort), einfügen:
// Check if assistant said goodbye
if (goodbyePattern.test(response.transcript)) {
logger.info(`Goodbye detected for ${channelId}, starting ${GOODBYE_TIMEOUT_MS}ms hangup timer`);
if (goodbyeTimer) clearTimeout(goodbyeTimer);
goodbyeTimer = setTimeout(async () => {
logger.info(`Goodbye timeout reached for ${channelId}, hanging up`);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close();
}
try {
const cd = sipMap.get(channelId);
if (cd && cd.channel) {
await cd.channel.hangup();
logger.info(`Channel ${channelId} hung up after goodbye`);
}
} catch (e) {
logger.error(`Error hanging up channel ${channelId} after goodbye: ${e.message}`);
}
}, GOODBYE_TIMEOUT_MS);
}
Änderung 3: Im conversation.item.created-Block, nach if (response.item.role === 'user') { und vor lastUserItemId = ..., einfügen:
if (goodbyeTimer) {
clearTimeout(goodbyeTimer);
goodbyeTimer = null;
logger.info(`Goodbye timer cancelled for ${channelId}, user is speaking`);
}
Dieser Teil ist wichtig: Wenn der Anrufer nach der Verabschiedung doch nochmal etwas sagt, wird der Timer abgebrochen. Die KI antwortet dann nochmals – und startet den Timer bei der nächsten Verabschiedung neu.
Danach Container neu starten.
5 Fazit
Einige Wochen später.
Ulf öffnet sein Handy. Eine E-Mail. Betreff: „Anruf von Klaus Müller (0171-…) – heute, 14:32 Uhr.“
Ulf öffnet sie. Vollständiger Gesprächsverlauf. Name, Anliegen, Dringlichkeit, alles da. Drei Minuten Gespräch, in einer lesbaren E-Mail zusammengefasst.
Ulf: „Hat funktioniert“.
Tanja: „Das weiß ich“.
Bernd schaut kurz rüber: „Ich habe auch eine Lösung gebaut. Ich habe meiner Frau gesagt, sie soll Anrufe entgegennehmen.“
Ulf: „Und?“
Bernd „Sie nimmt das Telefon nicht mehr ab.“
Was als Heimserver-Bastelprojekt begann, ist am Ende ein erstaunlich leistungsfähiges System. Ein Anrufer merkt kaum einen Unterschied zu einem echten Gespräch – die KI begrüßt freundlich, hört zu, fragt nach und verabschiedet sich. Wenige Sekunden später landet die vollständige Zusammenfassung im Postfach.
Was gut funktioniert: Der größte Vorteil dieser Architektur ist die vollständige Kontrolle über die eigene Infrastruktur. Asterisk, n8n und das Relay laufen lokal auf der Synology – nur die KI-Verarbeitung findet bei OpenAI statt. Keine monatlichen Abokosten für Telefonie-Dienste, keine Abhängigkeit von externen SIP-Anbietern, volle Transparenz über die Gesprächsdaten.
Wo es knifflig werden kann: Der schwierigste Teil ist erfahrungsgemäß die initiale Netzwerkkonfiguration – insbesondere network_mode: host, die Firewall-Regeln auf der Synology und die Cloudflare-Webhook-Freigabe. Wer hier sorgfältig vorgeht und jeden Schritt einzeln testet, spart sich viel Fehlersuche. Das Motto gilt auch hier: erst testen, dann weitermachen.
Mögliche nächste Schritte:
- Unterschiedliche System-Prompts je nach Rufnummer oder Tageszeit
- Mehrsprachige Unterstützung
- Integration in einen Kalender, damit die KI direkt Rückruftermine vereinbaren kann
- Erweiterung des n8n-Workflows mit einer KI-basierten Zusammenfassung via Claude oder ChatGPT
Bei Fragen oder Verbesserungsvorschlägen: Kommentar hinterlassen.
