CallwiseDocs

Vertrag des Sprachdienstes

Der signierte Webhook, den der Sprachdienst aufruft, um Anrufe anzulegen, Transkriptwechsel hinzuzufügen und Anrufe zu schließen.

Nur ein Vertrag

Dies ist ein Vertrag. Der Python-Sprachdienst (Sprache zu Text, das Sprachmodell und Text zu Sprache über WebRTC und SIP) ist noch nicht gebaut. Die Callwise-Seite des Webhooks ist gebaut und getestet; in der Produktion ruft ihn nichts auf.

Der Sprachdienst ist der einzige Schreiber von Anrufdatensätzen. Er meldet jeden Anruf der App als eine Folge von Ereignissen. Die Quelle der Wahrheit für die Nutzlast ist das Zod-Schema voiceEventSchema in apps/web/lib/callwise/voice-events.ts.

Endpunkt

POST /api/voice/events
Content-Type: application/json
x-callwise-timestamp: 1760530212
x-callwise-signature: 5f0c2e…(64 hex characters)

Signierung

HeaderWert
x-callwise-timestampUnix-Zeit in Sekunden, zu der die Anfrage signiert wurde
x-callwise-signatureHex-HMAC-SHA256 von `${timestamp}.${rawBody}` mit dem gemeinsamen Geheimnis CALLWISE_VOICE_SECRET

Signieren Sie genau die Bytes, die Sie senden. Die App berechnet die Signatur über den rohen Body neu, bevor sie ihn parst.

import { createHmac } from 'node:crypto';

const timestamp = Math.floor(Date.now() / 1000).toString();
const rawBody = JSON.stringify(event);
const signature = createHmac('sha256', process.env.CALLWISE_VOICE_SECRET!)
  .update(`${timestamp}.${rawBody}`)
  .digest('hex');
import hmac, hashlib, json, os, time

timestamp = str(int(time.time()))
raw_body = json.dumps(event, separators=(",", ":"))
signature = hmac.new(
    os.environ["CALLWISE_VOICE_SECRET"].encode(),
    f"{timestamp}.{raw_body}".encode(),
    hashlib.sha256,
).hexdigest()

Anfragen, deren Zeitstempel mehr als 5 Minuten von der Uhr des Servers abweicht, werden abgelehnt; das verhindert Wiederholungen alter Anfragen.

Antworten

StatusWann
200 { "ok": true, "duplicate": false }Ereignis angewendet
200 { "ok": true, "duplicate": true }Ein Ereignis mit dieser id wurde bereits angewendet; nichts hat sich geändert
400Der Body ist kein gültiges JSON oder entspricht nicht dem Schema (zum Beispiel ein unbekannter Ereignistyp)
401Signatur fehlt oder ist falsch, oder der Zeitstempel liegt außerhalb des Fensters von 5 Minuten

Idempotenz

Jedes Ereignis hat seine eigene id. Dasselbe Ereignis zweimal zu senden ist sicher: Das zweite wird mit duplicate: true bestätigt und ignoriert. Wiederholen Sie bei Netzwerkfehlern und 5xx mit derselben id.

Form des Ereignisses

Jedes Ereignis hat denselben Umschlag:

FeldTypHinweise
idstring, 1–200 ZeichenEreignis-ID, eindeutig je Ereignis
typecall.started | call.turn | call.ended
accountIduuidDer Arbeitsbereich (Teamkonto)
callobjectcall.id (uuid) ist erforderlich. Der Sprachdienst wählt sie und verwendet dieselbe ID für jedes Ereignis dieses Anrufs.
turnobjectNur bei call.turn

call.started

Legt einen laufenden Anruf an. Der Anrufer wird anhand der Telefonnummer zugeordnet oder neu angelegt. Wenn agentId weggelassen wird, wird der Agent aus der gewählten Nummer bestimmt.

{
  "id": "evt_01JA8S0K6Q2M3",
  "type": "call.started",
  "accountId": "3c1d6c1e-5c55-4f3e-9d0b-6b1f6f2a7a10",
  "call": {
    "id": "9b2f7d4e-1a7c-4e0f-8f5e-2d6c4b9a1e33",
    "to": "+15552108842",
    "callerPhone": "+15551237890",
    "callerName": "Emily Johnson",
    "direction": "inbound",
    "startedAt": "2026-10-15T13:41:02Z",
    "isTest": false,
    "afterHours": false
  }
}

Optionale call-Felder: numberId (uuid) oder to (E.164), agentId, locationId, callerPhone, callerName, direction (inbound oder outbound), startedAt (ISO 8601), isTest, afterHours.

call.turn

Fügt eine Zeile des Transkripts hinzu. call trägt nur die ID. Wechsel werden nach (call id, seq) gespeichert, sodass das erneute Senden eines Wechsels ihn ersetzt.

{
  "id": "evt_01JA8S0M1T7Z4",
  "type": "call.turn",
  "accountId": "3c1d6c1e-5c55-4f3e-9d0b-6b1f6f2a7a10",
  "call": { "id": "9b2f7d4e-1a7c-4e0f-8f5e-2d6c4b9a1e33" },
  "turn": {
    "seq": 1,
    "atMs": 4200,
    "speaker": "caller",
    "text": "Hi, I need a cleaning next week."
  }
}
turn-FeldTyp
seqinteger ≥ 0, Reihenfolge innerhalb des Anrufs
atMsinteger ≥ 0, Millisekunden seit Beginn des Anrufs
speakeragent | caller | staff | system
textstring

call.ended

Schließt den Anruf. Abgerechnete Minuten sind ceil(durationS / 60) und werden zur Nutzung des Tages addiert. Die App belastet den Anruf dann mit Credits: abgerechnete Minuten × der Satz des Modell-Stacks des Agenten + eine Zusammenfassung (siehe KI-Modelle und Credits), entnommen aus dem gemeinsamen Credit-Pool. Wenn summary weggelassen wird, wird eine Vorlagenzusammenfassung eingesetzt.

Bei call.started sagt die Antwort dem Sprachdienst, wie der Anruf geroutet wird: route: "agent" oder route: "fallback" mit einem reason, wenn der Credit-Pool, das Parallelitätslimit oder das Stundenlimit Nein sagt. Anrufe werden niemals verworfen.

{
  "id": "evt_01JA8S2B9W0K8",
  "type": "call.ended",
  "accountId": "3c1d6c1e-5c55-4f3e-9d0b-6b1f6f2a7a10",
  "call": {
    "id": "9b2f7d4e-1a7c-4e0f-8f5e-2d6c4b9a1e33",
    "endedAt": "2026-10-15T13:42:26Z",
    "durationS": 84,
    "intent": "booking",
    "topic": "Dental cleaning",
    "outcome": "booked",
    "sentiment": "positive",
    "confidence": 0.96,
    "summary": "New caller booked a dental cleaning for Tuesday at 10:00.",
    "recordingPath": "3c1d6c1e-5c55-4f3e-9d0b-6b1f6f2a7a10/9b2f7d4e-1a7c-4e0f-8f5e-2d6c4b9a1e33.mp3"
  }
}
call-FeldWerte
endedAtISO 8601
durationSinteger Sekunden
intentbooking reschedule cancel pricing insurance billing hours emergency general spam
topicFreitext
outcomebooked rescheduled cancelled info transferred voicemail missed spam
sentimentpositive neutral negative
confidence0–1
summaryFreitext
handoffReasonFreitext, wenn das Ergebnis transferred ist
recordingPath<accountId>/<callId>.mp3 im privaten Bucket cw-recordings

Wie es angewendet wird

Die Route prüft die Signatur und das Schema und ruft dann die Postgres-Funktion cw_apply_voice_event(jsonb) mit der Service-Role auf. Nur die Service-Role kann diese Funktion ausführen, und angemeldete Benutzer haben kein Einfügerecht auf Anrufe oder Wechsel; der Webhook ist damit der einzige Weg, auf dem Anrufdatensätze entstehen.

Vor dem Start

Siehe den Eintrag zum Sprachdienst unter Verbindungen dafür, was geprüft werden muss: Latenz, Barge-in-Behandlung und Rotation des Geheimnisses.

On this page