Webhooks: Ereignisse aus scoco in Ihre Systeme
scoco schickt jedes Ereignis, etwa einen beendeten Anruf oder einen gebuchten Termin, als signierte JSON-Nachricht an eine HTTPS-Adresse Ihrer Wahl.
Mit Webhooks meldet scoco Ihren eigenen Systemen in Echtzeit, was passiert, zum Beispiel „Anruf beendet" oder „Termin gebucht". Sie hinterlegen dafür eine HTTPS-Adresse, und scoco schickt jedes Ereignis als signierte JSON-Nachricht dorthin. Auch Zapier, Make und n8n verbinden Sie auf diesem Weg.
Einen Endpunkt anlegen
- Öffnen Sie Einstellungen
Integrationen. - Klicken Sie in der Karte Automatisierung auf Webhook-Endpunkt.
- Tragen Sie die Adresse ein. Sie muss mit
https://beginnen. - Wählen Sie bei Bedarf Ereignisse aus. Ohne Auswahl bekommen Sie alle.
- Klicken Sie auf Anlegen.
- Das Fenster Signaturschlüssel zeigt den Schlüssel (
whsec_…) genau einmal. Sichern Sie ihn mit Schlüssel kopieren und schließen Sie mit Ich habe den Schlüssel gesichert.
Der Schlüssel lässt sich danach nicht mehr abrufen. Haben Sie ihn verloren, entfernen Sie den Endpunkt über sein Menü mit Entfernen und legen ihn neu an.
Ereignisse
| Ereignis | Ausgelöst, wenn |
|---|---|
call.completed | ein Anruf beendet ist, mit Ergebnis, Dauer und Anrufernummer |
call.summary_ready | die KI-Zusammenfassung eines Anrufs vorliegt |
conversation.completed | eine WhatsApp-Unterhaltung abgeschlossen ist, mit Ergebnis und Kundennummer; nur mit aktivem WhatsApp |
conversation.summary_ready | die KI-Zusammenfassung einer WhatsApp-Unterhaltung vorliegt; nur mit aktivem WhatsApp |
ticket.status_changed | sich der Status eines Anrufs oder einer WhatsApp-Unterhaltung ändert: einmal automatisch nach jedem Gespräch (mit initial: true), danach bei jeder Änderung in der Inbox, auch beim Markieren als Spam |
ticket.assigned | ein Anruf oder eine WhatsApp-Unterhaltung jemandem zugewiesen, neu zugewiesen oder die Zuweisung entfernt wird (dann to: null) |
message.sms_received | eine SMS an Ihre scoco-Nummer eingeht |
message.sms_sent | scoco eine SMS verschickt oder es versucht: Prüfen Sie data.status, er kann failed sein |
appointment.booked | Ihre Assistenz einen Termin bucht |
appointment.cancelled | ein Termin abgesagt wird: vom Anrufer am Telefon, vom Kunden auf WhatsApp oder von Ihrem Team im Dashboard |
appointment.rescheduled | ein Termin verschoben wird: vom Anrufer am Telefon, vom Kunden auf WhatsApp oder von Ihrem Team im Dashboard |
contact.created | ein Kontakt angelegt wird, von Hand oder automatisch aus einem Gespräch; ein Import meldet nichts |
Neue Ereignisse kommen nur hinzu, es fällt keines weg. Ihr Empfänger sollte Ereignisse, die er nicht kennt, einfach ignorieren.
Testanrufe aus dem Dashboard lösen keine Ereignisse aus. Eine Änderung, die Sie direkt in Ihrem Kalender machen, auch nicht.
So sieht eine Nachricht aus
Jede Zustellung ist ein POST mit Content-Type: application/json. Der Header X-Scoco-Event nennt den Typ, hier ein call.completed:
{
"id": "5f0c…",
"type": "call.completed",
"created_at": "2025-08-06T11:58:39+00:00",
"tenant_id": "a319…",
"data": {
"call_id": "…",
"channel": "voice",
"assistant_id": "…",
"caller_e164": "+4930123456",
"started_at": "2025-08-06T11:56:01+00:00",
"ended_at": "2025-08-06T11:58:33+00:00",
"duration_seconds": 151.48,
"result": "message_taken",
"contact_id": null
}
}Was in data steht, hängt vom Ereignis ab. Es enthält immer die passenden IDs, alle Einzelheiten zeigt das Dashboard. Auch in data kommen Felder nur hinzu. Ein Feld kann null sein, wenn der Wert fehlt, und duration_seconds ist eine Kommazahl.
Die id bleibt bei jeder Wiederholung dieselbe. Haben Sie eine Nachricht schon verarbeitet, erkennen Sie die Wiederholung daran.
call.summary_ready und conversation.summary_ready enthalten language und original. Lief das Gespräch in einer anderen Sprache als der Ihrer Assistenz, ist language deren Kürzel, etwa ar, sonst null. Name, Firma und Anliegen kommen in der Sprache Ihres Kontos, Namen in lateinischer Schrift. Wo das von den Worten des Anrufers abweicht, stehen diese in original, unter denselben Schlüsseln, etwa der Name in arabischer Schrift unter caller_name. Sonst ist original null.
call.summary_ready enthält caller_silent. Es ist true, wenn der Anrufer aufgelegt hat, ohne etwas zu sagen. summary ist dann ein fester Satz statt einer KI-Zusammenfassung, und die Felder, die sonst aus dem Gespräch kommen, sind null.
Die beiden Terminänderungen tragen dieselbe appointment_id wie das appointment.booked des Termins. appointment.cancelled enthält starts_at, ends_at und cancelled_by (caller oder user). appointment.rescheduled enthält previous_starts_at, die neuen starts_at und ends_at und rescheduled_by (caller oder user). Beide kommen erst, wenn die Änderung in Ihrem Kalender angekommen ist.
Die Signatur prüfen
Jede Zustellung trägt den Header X-Scoco-Signature: t=1754481521,v1=5257a869e7…. v1 ist ein HMAC-SHA256 (hexadezimal) über die Zeichenkette <t>.<roher Request-Body>. Der Schlüssel ist Ihr Signaturschlüssel, genau so, wie er angezeigt wurde, mit whsec_ am Anfang. Bibliotheken für „Standard Webhooks" entfernen whsec_ und dekodieren den Rest; mit ihnen schlägt die Prüfung fehl.
t ist der Zeitpunkt der Signatur, nicht created_at: Jede Wiederholung wird neu signiert. Weisen Sie Nachrichten ab, deren t mehr als etwa 5 Minuten zurückliegt. Das schützt vor wiederholt eingespielten Nachrichten.
Prüfen Sie die Bytes, die ankommen, nicht ein neu erzeugtes JSON: Schon ein anderes Leerzeichen ändert die Signatur. In Node.js liefert etwa express.raw({ type: "application/json" }) den rohen Body, in PHP file_get_contents('php://input').
import hashlib, hmac, time
def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
try:
parts = dict(p.split("=", 1) for p in header.split(","))
t, v1 = int(parts["t"]), parts["v1"]
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), v1.encode())const crypto = require("node:crypto");
function verify(secret, body, header, tolerance = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split(/=(.*)/s, 2)));
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > tolerance) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(body).digest("hex");
const given = Buffer.from(parts.v1 ?? "", "utf8");
return given.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected, "utf8"), given);
}function verify(string $secret, string $body, string $header, int $tolerance = 300): bool {
$parts = [];
foreach (explode(',', $header) as $p) {
[$k, $v] = array_pad(explode('=', $p, 2), 2, '');
$parts[$k] = $v;
}
$t = (int) ($parts['t'] ?? 0);
if (abs(time() - $t) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $t . '.' . $body, $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}Zustellung, Wiederholung, Abschalten
- Eine Zustellung gilt als erfolgreich, wenn Ihr Endpunkt innerhalb von 10 Sekunden mit einem 2xx-Status antwortet. Antworten Sie am besten sofort und verarbeiten Sie die Nachricht danach.
- Schlägt eine Zustellung fehl, versucht scoco es erneut: insgesamt bis zu 5 Versuche, den ersten eingerechnet, mit Pausen von etwa 1 Minute, 5 Minuten, 15 Minuten und 1 Stunde.
- Schlagen 20 Zustellungen nacheinander fehl und liegt die erste davon mindestens 24 Stunden zurück, schaltet scoco den Endpunkt ab und schreibt den Inhabern eine E-Mail. Mit Aktivieren schalten Sie ihn wieder ein. Was in der Zwischenzeit passiert ist, wird nicht nachgeliefert.
- Unter Protokoll sehen Sie die neuesten 50 Zustellungen. Nach 30 Tagen wird eine Zustellung gelöscht, und wenn Sie den Endpunkt entfernen, auch sein Protokoll.
- Mit Testen schicken Sie ein signiertes
ping-Ereignis an Ihren Endpunkt. Es erscheint nicht im Protokoll und zählt nicht als Fehlschlag.
Datenschutz
Die Nachrichten enthalten personenbezogene Daten wie Anrufernummern, Namen und Zusammenfassungen. Mit einem Endpunkt beauftragen Sie scoco, diese Daten an das von Ihnen genannte System zu schicken. Sorgen Sie dort für angemessenen Schutz.