# Telofy für Entwickler: Schnittstelle, Webhooks, MCP

Referenz der öffentlichen Telofy-Schnittstelle. Telofy ist ein KI-Telefonassistent aus Deutschland.
Die Seite zum Lesen: https://telofy.de/entwickler

## Überblick

Die Telofy-Schnittstelle richtet sich an Server, nicht an Browser. Sie ist für den Zugriff von Ihrer Software, Ihrer Agentur oder Ihrem KI-Assistenten gedacht. Browser-Zugriff (CORS) ist nicht vorgesehen.

| | |
|---|---|
| Basis-Adresse | `https://api.telofy.app` |
| Format | JSON, UTF-8 |
| Anmeldung | `Authorization: Bearer tlfy_…` |
| Lesen | Anrufe, Anliegen, Termine |
| Schreiben | Ausgehenden Anruf auslösen (ab Business) |
| Ratenlimit | 60 Anfragen pro Minute und Schlüssel |
| Ereignisse | Webhooks, signiert |
| KI-Assistenten | MCP, Streamable HTTP unter `/mcp` |

Eine OpenAPI-Beschreibung gibt es derzeit nicht. Diese Seite ist die Referenz. Sie gibt es auch als reinen Markdown-Text unter [/entwickler.md](https://telofy.de/entwickler.md).

## Authentifizierung

Jede Anfrage trägt einen API-Schlüssel im Kopf-Feld `Authorization`.

```http
Authorization: Bearer tlfy_IhrSchluessel
```

- Einen Schlüssel erstellen Sie im Portal unter **Integrationen > API-Schlüssel** (Knopf „Schlüssel erstellen“). Er beginnt mit `tlfy_` und wird **nur einmal** vollständig angezeigt. Bewahren Sie ihn wie ein Passwort auf.
- Es sind bis zu drei aktive Schlüssel möglich. Ein widerrufener Schlüssel funktioniert nicht mehr.
- Der Betrieb, auf den sich eine Anfrage bezieht, ergibt sich allein aus dem Schlüssel. Eine Angabe in der Anfrage kann ihn nicht ändern.
- Es gibt keine Berechtigungsstufen. Derselbe Schlüssel liest und kann, ab dem Tarif Business, Anrufe auslösen. Geben Sie ihn deshalb nur an Systeme, denen Sie beides zutrauen.
- Fehlt der Schlüssel, ist er falsch oder widerrufen, antwortet Telofy immer gleich:

```json
{ "error": "unauthorized" }
```


## Endpunkte

| Methode | Pfad | Zweck |
|---|---|---|
| GET | `/api/v1/calls` | Anrufe, neueste zuerst |
| GET | `/api/v1/outcomes` | Anliegen aus Anrufen: Lead, Ticket, Termin, Auskunft, Bestellung, Sonstiges |
| GET | `/api/v1/appointments` | Nur Anliegen vom Typ Termin |
| POST | `/api/v1/outbound-calls` | Ausgehenden Anruf auslösen (ab Business) |
| POST | `/mcp` | MCP für KI-Assistenten |

### Anrufe abrufen

`GET /api/v1/calls`

```bash
curl -H "Authorization: Bearer tlfy_IhrSchluessel" "https://api.telofy.app/api/v1/calls?limit=2"
```

```json
{
  "page": 1,
  "limit": 2,
  "calls": [
    {
      "id": "call_9f2a1b",
      "startedAt": "2026-07-14T10:15:00.000Z",
      "endedAt": "2026-07-14T10:19:30.000Z",
      "direction": "inbound",
      "status": "completed",
      "callerNumber": "+4930555099",
      "durationSeconds": 270,
      "summary": "Anrufer möchte einen Termin für nächsten Dienstag.",
      "outcomeType": "appointment",
      "employeeName": "Anna"
    },
    {
      "id": "call_3c7d2e",
      "startedAt": "2026-07-14T09:00:00.000Z",
      "endedAt": "2026-07-14T09:04:12.000Z",
      "direction": "inbound",
      "status": "completed",
      "callerNumber": "+4930555010",
      "durationSeconds": 252,
      "summary": "Anrufer fragt nach Öffnungszeiten am Samstag.",
      "outcomeType": "lead",
      "employeeName": "Anna"
    }
  ]
}
```

| Feld | Bedeutung |
|---|---|
| `id` | Kennung des Anrufs |
| `startedAt`, `endedAt` | Beginn und Ende, ISO 8601 |
| `direction` | `inbound` oder `outbound` |
| `status` | Status des Anrufs, zum Beispiel `completed` |
| `callerNumber` | Rufnummer des Anrufers |
| `durationSeconds` | Dauer in Sekunden |
| `summary` | Zusammenfassung des Gesprächs |
| `outcomeType` | Art des Anliegens, falls eines entstand |
| `employeeName` | Name des Mitarbeiters, der den Anruf geführt hat |

Das Gesprächsprotokoll ist nicht Teil der Antwort. Es steht nur im Portal und nur, wenn Sie es dort eingeschaltet haben.

### Anliegen abrufen

`GET /api/v1/outcomes`

```bash
curl -H "Authorization: Bearer tlfy_IhrSchluessel" "https://api.telofy.app/api/v1/outcomes?limit=2"
```

```json
{
  "page": 1,
  "limit": 2,
  "outcomes": [
    {
      "id": "outcome_7b1a4c",
      "type": "appointment",
      "title": "Termin am Dienstag, 10:00 Uhr",
      "structuredData": {
        "requestedTime": "2026-07-21T10:00:00.000Z",
        "contactName": "Frau Beispiel"
      },
      "createdAt": "2026-07-14T10:19:35.000Z",
      "callId": "call_9f2a1b",
      "callerNumber": "+4930555099",
      "callStartedAt": "2026-07-14T10:15:00.000Z",
      "callSummary": "Anrufer möchte einen Termin für nächsten Dienstag.",
      "handlingStatus": "open"
    },
    {
      "id": "outcome_2e9f31",
      "type": "lead",
      "title": "Interesse an Beratungstermin",
      "structuredData": {
        "interesse": "Erstberatung"
      },
      "createdAt": "2026-07-14T09:04:20.000Z",
      "callId": "call_3c7d2e",
      "callerNumber": "+4930555010",
      "callStartedAt": "2026-07-14T09:00:00.000Z",
      "callSummary": "Anrufer fragt nach Öffnungszeiten am Samstag.",
      "handlingStatus": "open"
    }
  ]
}
```

| Feld | Bedeutung |
|---|---|
| `type` | `lead`, `ticket`, `appointment`, `info`, `other`, `order` oder `emergency` |
| `title` | Kurzer Titel des Anliegens |
| `structuredData` | Erfasste Angaben. Die Felder hängen von der Art des Anliegens ab |
| `callId` | Verweis auf den Anruf |
| `handlingStatus` | Bearbeitungsstand im Portal: `open` (offen), in Arbeit oder erledigt |

### Termine abrufen

`GET /api/v1/appointments`

Wie `/api/v1/outcomes`, aber auf Anliegen vom Typ Termin beschränkt. Die Liste heißt in der Antwort `appointments`.

```bash
curl -H "Authorization: Bearer tlfy_IhrSchluessel" "https://api.telofy.app/api/v1/appointments"
```

```json
{
  "page": 1,
  "limit": 50,
  "appointments": [
    {
      "id": "outcome_7b1a4c",
      "type": "appointment",
      "title": "Termin am Dienstag, 10:00 Uhr",
      "structuredData": {
        "requestedTime": "2026-07-21T10:00:00.000Z",
        "contactName": "Frau Beispiel"
      },
      "createdAt": "2026-07-14T10:19:35.000Z",
      "callId": "call_9f2a1b",
      "callerNumber": "+4930555099",
      "callStartedAt": "2026-07-14T10:15:00.000Z",
      "callSummary": "Anrufer möchte einen Termin für nächsten Dienstag.",
      "handlingStatus": "open"
    }
  ]
}
```

## Paginierung

Alle Listen liefern die neuesten Einträge zuerst.

- `page` beginnt bei 1 (Vorgabe 1).
- `limit` ist 50 (Vorgabe) und höchstens 100.
- Die Antwort enthält `page` und `limit`, aber keinen Gesamtzähler und keinen Cursor. Blättern Sie weiter, bis eine Seite weniger Einträge als `limit` liefert.
- Filterparameter gibt es nicht. Es werden nur `page` und `limit` gelesen.

```bash
curl -H "Authorization: Bearer tlfy_IhrSchluessel" "https://api.telofy.app/api/v1/calls?page=2&limit=25"
```

## Ratenlimit

Pro Schlüssel sind 60 Anfragen pro Minute erlaubt, zusammengezählt über alle Endpunkte und alle MCP-Aufrufe. Gezählt wird erst nach erfolgreicher Anmeldung. Wird das Limit überschritten, antwortet Telofy bis zum Ende der Minute mit Status 429:

```json
{ "error": "rate_limited" }
```

Warten Sie nach einer 429-Antwort kurz, bevor Ihr Programm es erneut versucht.

## Fehlercodes

Fehler kommen als JSON mit dem Feld `error`, zum Beispiel `{ "error": "unauthorized" }`.

| Status | `error` | Bedeutung |
|---|---|---|
| 400 | `invalid_request`, `purpose_required`, `invalid_number`, `employee_id_required` | Die Anfrage ist unvollständig oder fehlerhaft |
| 401 | `unauthorized` | Schlüssel fehlt, ist falsch oder widerrufen |
| 403 | `plan_not_eligible` | Die Funktion gibt es im aktuellen Tarif nicht |
| 404 | `not_found`, `employee_not_found` | Pfad unbekannt, oder die `employeeId` passt zu keinem Mitarbeiter |
| 405 | `method_not_allowed` | Falsche Methode. Der Kopf `Allow` nennt die erlaubte |
| 409 | `duplicate_external_reference` | Die `externalReference` wurde schon verwendet |
| 429 | `rate_limited` | Mehr als 60 Anfragen in einer Minute |
| 500 | `internal_error` | Fehler bei Telofy |
| 502 | `provider_request_failed` | Der Anruf konnte technisch nicht gestartet werden |

## Ausgehender Anruf

`POST /api/v1/outbound-calls`. Verfügbar ab dem Tarif **Business**. In anderen Tarifen antwortet Telofy mit 403 `plan_not_eligible`.

| Feld | Pflicht | Bedeutung |
|---|---|---|
| `toNumber` | ja | Zielnummer im internationalen Format, zum Beispiel `+4930123456` |
| `purpose` | ja | Freitext: worum es im Anruf geht |
| `employeeId` | nein | Nur nötig, wenn mehr als ein Mitarbeiter eine Nummer hat |
| `externalReference` | nein | Ihre eigene Kennung, zum Beispiel eine Auftragsnummer. Ein zweiter Aufruf mit demselben Wert wird mit 409 abgelehnt |

Der Mitarbeiter, der anruft, braucht eine zugewiesene Rufnummer.

```bash
curl -X POST -H "Authorization: Bearer tlfy_IhrSchluessel" -H "Content-Type: application/json" \
  -d '{"toNumber": "+4930111111", "purpose": "Ihr Auto ist fertig zur Abholung."}' \
  "https://api.telofy.app/api/v1/outbound-calls"
```

```json
{ "callId": "call_9f2a1b" }
```

Die Antwort (Status 202) bestätigt nur, dass der Anruf gestartet wurde. Was besprochen wurde, erfahren Sie asynchron über den Webhook `call.completed`, sobald das Gespräch beendet und ausgewertet ist. Der MCP-Server löst keine Anrufe aus.

## Webhook-Ereignisse

Webhooks legen Sie im Portal unter **Integrationen > Webhooks** an (Knopf „Webhook anlegen“). Sie tragen eine https-Adresse ein, wählen die Ereignisse und optional einen einzelnen Mitarbeiter. Das Signatur-Geheimnis (`whsec_…`) erscheint nur einmal in der Antwort.

Telofy sendet die Ereignisse **nach der Anrufauswertung**, nicht im Moment des Auflegens, und stellt sie asynchron zu. Planen Sie also mit einigen Minuten Abstand zum Ende des Gesprächs und nicht mit „in Echtzeit“.

| Ereignis | Wann |
|---|---|
| `call.completed` | Nach jedem ausgewerteten Anruf |
| `outcome.created` | Wenn der Anruf ein Anliegen vom Typ `lead`, `ticket`, `appointment` oder `order` ergab |
| `appointment.created` | Zusätzlich zu `outcome.created`, wenn das Anliegen ein Termin war |

Jede Nachricht ist ein `POST` mit `Content-Type: application/json` und dem Kopf `X-Telofy-Signature`. Der Rumpf hat die Form `{ "event": …, "data": … }`. Ein Gesprächsprotokoll enthält er nie, nur den Link `callUrl` ins Portal. Weitere Köpfe oder eine eigene Ereignis-Kennung gibt es nicht. Entdoppeln Sie über `callId` und `outcomeId` im Rumpf.

### call.completed

```json
{
  "event": "call.completed",
  "data": {
    "callId": "call_9f2a1b",
    "completedAt": "2026-07-14T09:04:12.000Z",
    "durationSeconds": 252,
    "employeeName": "Anna",
    "summary": "Anrufer fragt nach Öffnungszeiten am Samstag.",
    "outcomeType": "lead",
    "callUrl": "https://telofy.app/calls/call_9f2a1b"
  }
}
```

### outcome.created

```json
{
  "event": "outcome.created",
  "data": {
    "outcomeId": "outcome_2e9f31",
    "callId": "call_3c7d2e",
    "createdAt": "2026-07-14T09:04:20.000Z",
    "outcomeType": "lead",
    "summary": "Interesse an Beratungstermin",
    "callUrl": "https://telofy.app/calls/call_3c7d2e"
  }
}
```

### appointment.created

```json
{
  "event": "appointment.created",
  "data": {
    "outcomeId": "outcome_7b1a4c",
    "callId": "call_9f2a1b",
    "createdAt": "2026-07-14T10:19:35.000Z",
    "requestedTime": "2026-07-21T10:00:00.000Z",
    "contactName": "Frau Beispiel",
    "contactPhone": "+4930555099",
    "callUrl": "https://telofy.app/calls/call_9f2a1b"
  }
}
```

### Zustellung

- Als Erfolg gilt jeder Status 2xx. Antworten Sie innerhalb von 10 Sekunden.
- Bei Misserfolg gibt es bis zu fünf Versuche insgesamt, mit Wartezeiten von 1, 5, 30 und 120 Minuten dazwischen. Danach gilt die Zustellung als gescheitert.
- Weiterleitungen (Redirects) werden nicht verfolgt. Die Adresse muss direkt antworten.
- Nur `https://`, ohne Zugangsdaten in der Adresse und ohne private oder interne Hostnamen wie `localhost` oder `.local`. Telofy prüft das beim Versand erneut.
- Die Reihenfolge ist nicht garantiert. Ein Wiederholungsversuch trägt denselben Rumpf, aber einen neuen Zeitstempel in der Signatur.
- Bis zu fünf Webhooks je Betrieb. Das Zustellprotokoll zeigt die letzten 50 Zustellungen je Webhook und wird nach 30 Tagen gelöscht.
- Der Knopf „Test-Ereignis senden“ im Portal schickt ein `call.completed` mit `"test": true` auf der obersten Ebene, `callId` `test` und `outcomeType` `test`. Höchstens einmal pro Minute und Webhook. Ein Wiederholen vergangener Ereignisse gibt es nicht.

## Signatur prüfen

Jede Nachricht trägt den Kopf

```http
X-Telofy-Signature: t=1752482652,v1=<64 Hexzeichen>
```

`t` ist die Unix-Zeit in Sekunden des Versandversuchs. `v1` ist der HMAC-SHA256 als Hexwert über den Text `<t>.<Rohkörper>`, gebildet mit dem Geheimnis Ihres Webhooks (`whsec_…`).

So prüfen Sie:

1. Lesen Sie den Anfragetext **unverändert** als Text. Wenn Ihr Framework ihn schon in ein Objekt umgewandelt hat, stimmt die Prüfsumme nicht mehr. In Express nutzen Sie dafür `express.raw({ type: "application/json" })`, in Flask `request.get_data()`.
2. Trennen Sie `t` und `v1` aus dem Kopf.
3. Bilden Sie den HMAC-SHA256 über `t`, einen Punkt und den Rohkörper.
4. Vergleichen Sie mit `v1` in konstanter Zeit.
5. Lehnen Sie Nachrichten ab, deren Zeitstempel mehr als fünf Minuten von Ihrer Uhr abweicht.

### Node.js

```js
import { createHmac, timingSafeEqual } from "node:crypto";

// rohKoerper: der unveränderte Anfragetext als String, nicht das geparste Objekt.
export function istTelofySignaturGueltig(
  geheimnis,
  rohKoerper,
  kopfzeile,
  jetzt = Math.floor(Date.now() / 1000)
) {
  const teile = {};
  for (const stueck of kopfzeile.split(",")) {
    const i = stueck.indexOf("=");
    if (i > 0) teile[stueck.slice(0, i).trim()] = stueck.slice(i + 1).trim();
  }

  const t = Number(teile.t);
  if (!Number.isFinite(t) || !teile.v1) return false;
  if (Math.abs(jetzt - t) > 300) return false; // höchstens fünf Minuten alt

  const erwartet = createHmac("sha256", geheimnis)
    .update(`${teile.t}.${rohKoerper}`, "utf8")
    .digest("hex");

  const a = Buffer.from(erwartet);
  const b = Buffer.from(teile.v1);
  return a.length === b.length && timingSafeEqual(a, b);
}
```

### Python

```python
import hashlib
import hmac
import time


def telofy_signatur_gueltig(geheimnis, roh_koerper, kopfzeile, jetzt=None):
    """roh_koerper: der unveränderte Anfragetext als bytes."""
    teile = {}
    for stueck in kopfzeile.split(","):
        schluessel, _, wert = stueck.partition("=")
        teile[schluessel.strip()] = wert.strip()

    t_text = teile.get("t")
    v1 = teile.get("v1")
    if not t_text or not v1:
        return False
    try:
        t = int(t_text)
    except ValueError:
        return False

    jetzt = time.time() if jetzt is None else jetzt
    if abs(jetzt - t) > 300:  # höchstens fünf Minuten alt
        return False

    erwartet = hmac.new(
        geheimnis.encode("utf-8"),
        t_text.encode("utf-8") + b"." + roh_koerper,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(erwartet.encode("utf-8"), v1.encode("utf-8"))
```

## MCP für KI-Assistenten

Der MCP-Server liegt unter `https://api.telofy.app/mcp` (Streamable HTTP, ohne Sitzungszustand, Antwort als JSON). Er nutzt denselben API-Schlüssel und dasselbe Ratenlimit wie die Schnittstelle.

| Werkzeug | Zweck | Parameter |
|---|---|---|
| `list_calls` | Anrufe, neueste zuerst | `page`, `limit` (optional) |
| `list_outcomes` | Anliegen aus Anrufen | `page`, `limit` (optional) |
| `list_appointments` | Termine | `page`, `limit` (optional) |

Alle drei Werkzeuge lesen nur. Der MCP-Server löst keine Anrufe aus und ändert nichts.

Eine übliche Konfiguration für Assistenten, die MCP-Server über eine Adresse einbinden:

```json
{
  "mcpServers": {
    "telofy": {
      "url": "https://api.telofy.app/mcp",
      "headers": {
        "Authorization": "Bearer tlfy_IhrSchluessel"
      }
    }
  }
}
```

Wo Sie das eintragen, unterscheidet sich je Assistent. Schauen Sie in dessen Anleitung nach der Stelle für entfernte MCP-Server. Ein Fehler beim Werkzeugaufruf, etwa durch einen ungültigen Schlüssel, kommt als Werkzeugfehler zurück und nicht als HTTP-Fehler.

## Eigene Werkzeuge

Mit eigenen Werkzeugen ruft Telofy **während eines Gesprächs** eine https-Adresse Ihres Systems auf und liest die Antwort vor. Das ist die umgekehrte Richtung: Sie liefern Telofy Daten, statt welche abzuholen.

Anlegen im Portal unter **Integrationen > Eigene Werkzeuge** (Knopf „Werkzeug anlegen“). Sie legen fest:

- **Funktionsname**: Nur der Assistent sieht ihn, ohne Leerzeichen, zum Beispiel `lookup_shipping_status`.
- **Beschreibung**: Sie entscheidet, ob Telofy das Werkzeug im richtigen Moment aufruft. Schreiben Sie, wann es genutzt werden soll.
- **Parameter-Schema** (JSON): Welche Angaben Telofy aus dem Gespräch mitschickt.
- **Ziel-Adresse**: https, öffentlich erreichbar.
- **Auth-Header-Name und Geheimnis** (optional): Telofy sendet sie bei jedem Aufruf mit. Das Geheimnis wird verschlüsselt gespeichert.

Beispiel für ein Parameter-Schema:

```json
{
  "type": "OBJECT",
  "properties": {
    "order_number": { "type": "STRING", "description": "Bestellnummer" }
  },
  "required": ["order_number"]
}
```

Telofy sendet einen `POST` mit den Angaben unter `arguments`:

```http
POST /versandstatus HTTP/1.1
Host: ihr-system.de
Content-Type: application/json

{ "arguments": { "order_number": "A-1042" } }
```

Ihr System antwortet mit Status 2xx und genau diesem Aufbau:

```json
{ "result": "Die Bestellung A-1042 wurde heute mit DHL verschickt und kommt voraussichtlich morgen an." }
```

Grenzen:

- Bis zu 10 Werkzeuge je Betrieb.
- 4 Sekunden Zeit je Aufruf. Die Antwort darf höchstens 4.000 Byte groß sein, der Text in `result` wird auf 800 Zeichen gekürzt.
- Weiterleitungen werden nicht verfolgt. Private und interne Adressen werden abgelehnt.
- Schlägt ein Aufruf fehl, sagt Telofy dem Anrufer ehrlich, dass es das gerade nicht nachsehen kann, und bietet an, das Anliegen aufzunehmen. Das Gespräch bricht nicht ab.
- Reservierte Namen lassen sich nicht vergeben.

## Was die Schnittstelle nicht kann

- Sie schreibt keine Termine, Kontakte, Katalogpositionen oder Wissenseinträge. Die Pflege dieser Daten läuft über das Portal und den Import. Schreibend ist nur der ausgehende Anruf.
- Es gibt keine Filter, keinen Gesamtzähler und keine Berechtigungsstufen je Schlüssel.
- MCP liest nur.
- Webhooks haben keine Ereignis-Kennung im Kopf, keine Reihenfolgegarantie und keine Wiederholung vergangener Ereignisse auf Knopfdruck.
- Es gibt kein Programmierpaket (SDK) und keine OpenAPI-Datei.
- Eine eigene App für Zapier, Make oder n8n gibt es nicht. Die Anbindung läuft über deren Webhook-Bausteine, siehe [Integrationen](https://telofy.de/integrationen).

## Häufige Fragen

### Wie lautet die Basis-Adresse der Schnittstelle?

https://api.telofy.app. Die Anfragen gehen an Pfade wie /api/v1/calls und /mcp. Jede Anfrage braucht den Kopf Authorization: Bearer mit Ihrem Schlüssel.

### Wie bekomme ich einen API-Schlüssel?

Im Portal unter Integrationen > API-Schlüssel erstellen Sie bis zu drei Schlüssel. Er wird nur einmal vollständig angezeigt und beginnt mit tlfy_.

### Kann ich per Schnittstelle einen Anruf auslösen?

Ja, ab dem Tarif Business mit POST /api/v1/outbound-calls. Das Ergebnis des Gesprächs kommt anschließend über den Webhook call.completed.

### Wie prüfe ich, dass ein Webhook von Telofy kommt?

Über den Kopf X-Telofy-Signature. Er enthält die Zeitangabe t und den HMAC-SHA256 v1 über t, einen Punkt und den Rohkörper, gebildet mit dem Geheimnis Ihres Webhooks. Beispiele für Node.js und Python stehen auf dieser Seite.

### Sind Webhooks in Echtzeit?

Nein. Telofy sendet sie nach der Anrufauswertung und stellt sie asynchron zu. Bei Misserfolg gibt es bis zu fünf Versuche.

