Webhooks
Lassen Sie sich benachrichtigen, wenn etwas passiert: ein Lead wird erfasst, ein Besucher verlangt einen Menschen, ein Gespräch ändert seinen Status. OneType sendet einen signierten POST an Ihren HTTPS-Endpunkt.
Status: Verfügbar (Premium). Das Signaturverfahren unten ist final, und der Verifizierungscode auf dieser Seite ist gegen den offiziellen Standard-Webhooks-Testvektor getestet.
Endpunkt einrichten
Gehen Sie zu Developer › Webhooks (als Inhaber oder Administrator):
- Fügen Sie eine HTTPS-URL hinzu und wählen Sie die Ereignisse oder alle aus (
*). Wählen Sie optional Agents: Der Endpunkt erhält dann nur die Ereignisse dieser Agenten und keine workspaceweiten Ereignisse. Ohne ausgewählte Agenten erhält er die Ereignisse aller Agenten und die workspaceweiten. - Kopieren Sie das Signing Secret (
whsec_…). Es wird nur einmal angezeigt. Sie können es später rotieren. - Klicken Sie auf Send test , um einen
pingzuzustellen, und sehen Sie die Antwort im Zustellprotokoll.
Ereignisse
| Typ | Wann |
|---|---|
conversation.created | Ein Besucher startet ein Gespräch |
conversation.message.created | Eine Nachricht wird abgeschlossen (Besucher, Agent oder Ihr Team) |
conversation.handoff.requested | Der Besucher oder der Agent verlangt einen Menschen |
conversation.status.changed | Der Status hat sich geändert: open, escalated, resolved, unresolved oder failed (siehe Gesprächsstatus) |
lead.captured | Ein Besucher hinterlässt Kontaktdaten |
usage.threshold.reached | Ihr Workspace erreicht 50 %, 80 % oder 100 % seiner monatlichen Credits (Antworten in Tarifen mit eigenem Schlüssel) |
csat.submitted | Ein Besucher hat ein Gespräch bewertet (einmalig, endgültig). data.object ist ein csat_rating mit conversation, agent, rating (1 bis 5), handled_by (ai oder human), comment (die eigenen Worte des Besuchers oder null) und created_at. Es enthält den Kommentar, behandeln Sie es daher als personenbezogene Daten |
booking.made | Ein Besucher hat über die Buchungskarte des Chats einen Termin gebucht (erfordert Cal.com oder Calendly, bald verfügbar). data.object ist ein booking mit event_type, start_at, status, contact, conversation und matched_by (token: über die Karte dieses Gesprächs gebucht; email: nur eine verifizierte E-Mail stimmte überein, kein Beweis) |
Im Lauf der Zeit werden weitere Ereignistypen hinzugefügt. Ignorieren Sie Typen, die Sie nicht verarbeiten. Auch dem Umschlag und den Objekten können im Lauf der Zeit Felder hinzugefügt werden: Ignorieren Sie die, die Sie nicht verwenden.
Payload
{
"id": "evt_01J9…",
"type": "lead.captured",
"created_at": "2026-09-22T10:00:00Z",
"api_version": "v1",
"workspace": "wsp_…",
"agent": "agt_…",
"data": { "object": { "id": "con_…", "object": "contact", "email": "jane@example.com", "name": "Jane" } }
}data.object ist dasselbe Objekt, das die REST API für diese Ressource zurückgibt. Nachrichtenobjekte enthalten is_ai_generated.
agent ist die ID des Agenten, zu dem das Ereignis gehört, in derselben agt_… -Form, die die REST API verwendet. Sie ist bei Ereignissen zu den Gesprächen und Besuchern eines Agenten vorhanden: conversation.created, conversation.message.created, conversation.handoff.requested, conversation.status.changed, csat.submitted, lead.captured und contact.identified. Sie steht auch bei booking.made , wenn die Buchung aus dem Gespräch oder von einer Person eines Agenten stammt. Sie fehlt, wenn kein Agent beteiligt ist: bei workspaceweiten Ereignissen wie usage.threshold.reached, bei einem booking.made , das keinem Agenten zugeordnet wurde, und bei Kontaktänderungen, die nicht von einem Besucher stammen (zum Beispiel eine Änderung durch Ihr Team). Behandeln Sie sie als optional und lesen Sie die Ressource aus data.object. Das Feld ist eine Ergänzung: Bestehende Endpunkte erhalten es ohne Änderung; ein Empfänger, der unbekannte Felder ablehnt, muss daher folgendes Feld zulassen: agent.
Signaturen prüfen
OneType signiert jede Zustellung mit Standard Webhooks, einem offenen Verfahren, das auch mehrere große API-Anbieter nutzen. Jede Anfrage hat drei Header:
webhook-id: evt_01J9… (the same on every retry of this event)
webhook-timestamp: 1790000000 (Unix seconds)
webhook-signature: v1,K5oZfz…= (can hold several, space-separated, during secret rotation)Die Signatur ist base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}")), wobei key Ihr Geheimnis ist, ohne das Präfix whsec_ , base64-dekodiert.
Immer:
- gegen den rohen Request-Body prüfen, vor jedem JSON-Parsing oder erneuten Serialisieren
- Zeitstempel ablehnen, die mehr als 5 Minuten von jetzt abweichen, was Replays verhindert
- in konstanter Zeit vergleichen
- jede aufgeführte
v1-Signatur akzeptieren, damit Rotationen bei Ihnen nie zu Fehlern führen
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyOnetypeSignature(rawBody, headers, secret, toleranceSeconds = 300)
{
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures)
{
throw new Error("missing webhook headers");
}
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds)
{
throw new Error("timestamp outside tolerance");
}
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
for (const entry of signatures.split(" "))
{
const [version, value] = entry.split(",");
const candidate = Buffer.from(value ?? "", "base64");
if (version === "v1" && candidate.length === expected.length && timingSafeEqual(candidate, expected))
{
return JSON.parse(rawBody);
}
}
throw new Error("no matching signature");
}
// Express: keep the raw body for this route
app.post("/hooks/onetype", express.raw({ type: "application/json" }), (req, res) => {
let event;
try { event = verifyOnetypeSignature(req.body.toString("utf8"), req.headers, process.env.ONETYPE_WEBHOOK_SECRET); }
catch { return res.sendStatus(400); }
if (event.type === "lead.captured") crm.upsert(event.data.object);
res.sendStatus(204);
});Zum Routen nach Agent lesen Sie event.agent (zum Beispiel if (event.agent === "agt_…") …); bei workspaceweiten Ereignissen fehlt es. Oder geben Sie jedem Agenten einen eigenen Endpunkt mit Agents (agentIds in der API): Eine erneute Zustellung, die der Endpunkt nicht mehr annimmt, wird abgelehnt, und ein Test wird als einer seiner Agenten signiert.
Python
import base64, hashlib, hmac, json, time
def verify_onetype_signature(raw_body: bytes, headers, secret: str, tolerance_seconds: int = 300):
msg_id = headers.get("webhook-id")
timestamp = headers.get("webhook-timestamp")
signatures = headers.get("webhook-signature")
if not msg_id or not timestamp or not signatures:
raise ValueError("missing webhook headers")
if abs(time.time() - int(timestamp)) > tolerance_seconds:
raise ValueError("timestamp outside tolerance")
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
for entry in signatures.split(" "):
version, _, value = entry.partition(",")
if version == "v1" and hmac.compare_digest(value, expected):
return json.loads(raw_body)
raise ValueError("no matching signature")
# Flask: request.get_data() is the raw body; request.headers is case-insensitivePHP
<?php
function verifyOnetypeSignature(string $rawBody, array $headers, string $secret, int $toleranceSeconds = 300): array
{
$id = $headers['webhook-id'] ?? null;
$timestamp = $headers['webhook-timestamp'] ?? null;
$signatures = $headers['webhook-signature'] ?? null;
if (!$id || !$timestamp || !$signatures) {
throw new RuntimeException('missing webhook headers');
}
if (abs(time() - (int) $timestamp) > $toleranceSeconds) {
throw new RuntimeException('timestamp outside tolerance');
}
$key = base64_decode(preg_replace('/^whsec_/', '', $secret), true);
$expected = base64_encode(hash_hmac('sha256', "{$id}.{$timestamp}.{$rawBody}", $key, true));
foreach (explode(' ', $signatures) as $entry) {
[$version, $value] = array_pad(explode(',', $entry, 2), 2, '');
if ($version === 'v1' && hash_equals($expected, $value)) {
return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
}
}
throw new RuntimeException('no matching signature');
}
// Usage: $raw = file_get_contents('php://input');
// $headers = array_change_key_case(getallheaders(), CASE_LOWER);No-Code-Tools (Zapier, Make)
Deren einfache Catch-Hooks akzeptieren jede Anfrage und können die Signatur nicht prüfen. Behandeln Sie die Hook-URL als Geheimnis. Bietet das Tool einen Code-Schritt, führen Sie dort dieselbe Prüfung aus: Sie benötigt den rohen Body, die drei webhook-* -Header und Ihr Geheimnis. Deduplizieren Sie anhand von webhook-id in jedem Fall, weil ein erneuter Versuch ein Ereignis zweimal zustellen kann. Eine Schritt-für-Schritt-Anleitung für Inhaber finden Sie im Hilfecenter: Zapier oder Make verbinden.
Prüfen Sie Ihre Implementierung mit dem Standard-Webhooks-Testvektor:
- Secret
whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw - ID
msg_p5jXN8AQM9LWM0D4loKWxJek - Zeitstempel
1614265330 - Body
{"test": 2432232314} - erwartetes Ergebnis
v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
Antworten und Wiederholungen
- Antworten Sie mit einem beliebigen 2xx innerhalb von 10 Sekunden. Erledigen Sie langsame Arbeit nach der Antwort, z. B. über eine Warteschlange.
- Bei einer Antwort ohne 2xx oder einem Timeout versuchen wir es über etwa 3 Tage: nach 5 s, 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h, 12 h, dann dreimal alle 24 h.
- Die Zustellung erfolgt mindestens einmal. Speichern Sie
webhook-idund ignorieren Sie Wiederholungen. - Die Reihenfolge ist nicht garantiert. Verwenden Sie
created_at, wenn die Reihenfolge wichtig ist. - Ein Endpunkt, der 3 Tage lang nur fehlschlägt, ist deaktiviert, und Inhaber sehen ein Banner. Aktivieren Sie ihn im Dashboard wieder; neue Ereignisse fließen ab dann.
- Das Zustellprotokoll (30 Tage) zeigt jeden Versuch mit Status und Zeitangabe. Redeliver sendet ein Ereignis erneut.
- Wir stellen nur an öffentliche HTTPS-Adressen zu und folgen keinen Weiterleitungen.