# 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.

https://scoco.ai/de/docs/webhooks/

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](https://scoco.ai/de/docs/zapier-make-n8n/) verbinden Sie auf diesem Weg.

## Einen Endpunkt anlegen

1. Öffnen Sie **Einstellungen › Integrationen**.
2. Klicken Sie in der Karte **Automatisierung** auf **Webhook-Endpunkt**.
3. Tragen Sie die **Adresse** ein. Sie muss mit `https://` beginnen.
4. Wählen Sie bei Bedarf **Ereignisse** aus. Ohne Auswahl bekommen Sie alle.
5. Klicken Sie auf **Anlegen**.
6. 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`:

```json
{
  "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')`.

**Python:**

```python
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())
```

**Node.js:**

```js
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);
}
```

**PHP:**

```php
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.

_Stand: 29. Oktober 2026_
