
# Termine

Mit der Appointments API können Sie Termine für Ihre Kontakte für Ihre Ereignistypen buchen sowie diese abrufen, auflisten, aktualisieren, stornieren oder löschen. Sie beantwortet auch die Frage, die in den meisten Buchungsabläufen zuerst gestellt wird – welche Zeiten sind tatsächlich frei – und deckt die Kalenderseite ab: Auflisten der von Ihnen verbundenen Google-Kalender und Importieren von Ereignissen, die bereits darin enthalten sind. Wenn eine Google-Kalender-Verbindung aktiv ist, wird das entsprechende Kalenderereignis automatisch im Hintergrund erstellt und synchronisiert. Restaurants, die Zenchef, Formitable, OpenTable oder TheFork für ihr eigenes Reservierungssystem nutzen, können hier ebenfalls verifiziert und verbunden werden, sodass der KI-Agent echte Tische anstelle von internen Terminen bucht – und Unternehmen, die ihre Termine über Trafft verwalten, können dies auf die gleiche Weise verbinden.

Alle Pfade auf dieser Seite beziehen sich auf die Basis-URL `https://api.dmchamp.com/v1`. Jede Anfrage erfordert Ihren API-Schlüssel – siehe [Authentifizierung](authentication.md) für die vollständige Liste der Möglichkeiten, diesen zu senden. Die folgenden Beispiele verwenden den Header `X-API-Key`, wobei ein cURL-Beispiel auch das Abfrageformular `?apiKey=` zeigt.

> **Ereignisse vs. Termine:** Ein *Ereignistyp* ist eine Definition für einen buchbaren Slot (die Art des Meetings, seine Dauer, seine Räume). Ein *Termin* ist eine gebuchte Instanz eines Ereignistyps für einen bestimmten Kontakt. Sie buchen einen Termin, indem Sie auf den Kontakt und den Ereignistyp verweisen.

---

## Das Terminobjekt

Jeder Endpunkt, der einen Termin zurückgibt, verwendet dieselbe Struktur:

| Feld | Beschreibung |
|---|---|
| `id` | Eindeutige ID des Termins. |
| `contact_id` | ID des Kontakts, für den der Termin gebucht wurde. |
| `event_id` | ID des Ereignistyps, für den der Termin gebucht wurde. |
| `status` | `Confirmed` oder `Canceled`. |
| `start_time` | Beginn des Termins, ISO 8601 in UTC. |
| `end_time` | Ende des Termins, ISO 8601 in UTC. |
| `created_at` | Zeitpunkt, zu dem der Termin erstellt wurde. |
| `last_modified_at` | Zeitpunkt, zu dem der Termin zuletzt geändert wurde. |
| `room_name` | Raum oder Ressource, in dem/der der Termin gebucht ist, wenn der Ereignistyp Räume verwendet. |
| `description` | Freitextbeschreibung des Termins. |
| `summary` | Kurze Zusammenfassung oder Titel. |
| `cancelation_reason` | Grund für die Stornierung des Termins, falls vorhanden. |
| `google_calendar_event_id` | ID des verknüpften Google Kalender-Ereignisses. Wird gesetzt, sobald die Kalendersynchronisierung abgeschlossen ist; `null`, wenn kein Kalender verbunden ist oder die Synchronisierung noch läuft. |
| `calendar_synced` | `true`, sobald der Termin mit einem Kalenderereignis verknüpft ist. |
| `imported` | `true`, wenn der Termin aus einem externen Kalender importiert wurde, anstatt direkt gebucht zu werden. |
| `is_recurring` | `true`, wenn der Termin Teil einer wiederkehrenden Serie ist. |
| `recurrence_frequency` | Häufigkeit der Wiederholung des Termins bei wiederkehrenden Terminen. |
| `recurring_event_id` | ID der wiederkehrenden Serie, zu der dieser Termin gehört. |
| `recurring_interval` | Intervall zwischen den Wiederholungen bei wiederkehrenden Terminen. |
| `recurring_sequence` | Position dieses Termins innerhalb seiner wiederkehrenden Serie. |
| `end_after_x_occurrences` | Anzahl der Vorkommen, nach denen die wiederkehrende Serie endet. |
| `booking_provider` | Quellsystem, aus dem die Buchung stammt, wenn sie über einen verbundenen Reservierungsanbieter gebucht wurde. |

> **Über die Kalendersynchronisierung:** Direkt nach dem Buchen oder Ändern eines Termins können `google_calendar_event_id` noch `null` und `calendar_synced` noch `false` sein, da die Synchronisierung einen Moment später im Hintergrund ausgeführt wird. Rufen Sie den Termin kurz darauf erneut ab, um die ausgefüllten Kalenderfelder zu sehen.

---

## Verfügbare Zeitfenster finden

`GET /appointments/available-slots`

Gibt die Zeiten zurück, die für einen Ereignistyp zwischen zwei Zeitpunkten tatsächlich frei sind. Dies ist normalerweise der **erste** Aufruf in einem Buchungsablauf: Zeigen Sie diese Zeitfenster an, lassen Sie die Person eines auswählen und senden Sie dann die gewählte Zeit an [Termin buchen](#book-an-appointment).

Die Antwort berücksichtigt bereits die Öffnungszeiten und die Dauer des Zeitfensters des Ereignistyps, seine Räume, bereits gebuchte Termine sowie alle auf den verbundenen Google Kalendern blockierten Zeiten – ein hier zurückgegebenes Zeitfenster ist also eines, das Sie buchen können.

| Abfrageparameter | Erforderlich | Beschreibung |
|---|---|---|
| `event_id` | Ja | Der zu prüfende Ereignistyp. Muss zu Ihrem Konto gehören. |
| `start_time` | Ja | Beginn des Zeitfensters, für das Sie Termine suchen, als ISO 8601-Datum/Uhrzeit. |
| `end_time` | Ja | Ende des Zeitfensters, als ISO 8601-Datum/Uhrzeit. Der gesamte Endtag ist inbegriffen. |

Die Ergebnisse werden nach Tagen gruppiert zurückgegeben – und wenn der Ereignistyp Räume verwendet, eine Gruppe pro Raum pro Tag:

| Feld | Beschreibung |
|---|---|
| `date` | Der Tag, den die Gruppe abdeckt, geschrieben als `DD/MM/YYYY`. |
| `day` | Wochentagsname in Kleinbuchstaben, zum Beispiel `monday`. |
| `room_name` | Der Raum oder die Ressource, zu der diese Gruppe gehört, wenn der Ereignistyp Räume verwendet. |
| `available_slots` | Die buchbaren Blöcke an diesem Tag, beginnend mit dem frühesten. |

Jeder Eintrag in `available_slots` enthält:

| Feld | Beschreibung |
|---|---|
| `start_time` | Blockbeginn als `HH:mm`. |
| `end_time` | Blockende als `HH:mm`. |
| `available` | `true` – es wird nur freie Zeit zurückgegeben. |
| `spots_left` | Wie viele Buchungen noch in diesen Block passen. Nur vorhanden bei Ereignistypen, die mehr als eine Buchung pro Zeitfenster zulassen. |

> **Die Zeiten beziehen sich auf den Ereignistyp, nicht auf UTC.** `date`, `start_time` und `end_time` sind Uhrzeitwerte in der Zeitzone des Ereignistyps (dessen Überschreibung oder Ihre Konto-Zeitzone, falls keine vorhanden ist). [Termin buchen](#book-an-appointment) erwartet einen ISO 8601 UTC-Zeitpunkt. Konvertieren Sie daher das gewählte Zeitfenster, bevor Sie es senden.

**cURL**

```bash
curl "https://api.dmchamp.com/v1/appointments/available-slots?event_id=event_xyz789&start_time=2026-06-15T00:00:00.000Z&end_time=2026-06-19T00:00:00.000Z" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const params = new URLSearchParams({
  event_id: "event_xyz789",
  start_time: "2026-06-15T00:00:00.000Z",
  end_time: "2026-06-19T00:00:00.000Z",
});
const res = await fetch(
  `https://api.dmchamp.com/v1/appointments/available-slots?${params}`,
  { headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.data);
```

**Python**

```python
import requests

res = requests.get(
    "https://api.dmchamp.com/v1/appointments/available-slots",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={
        "event_id": "event_xyz789",
        "start_time": "2026-06-15T00:00:00.000Z",
        "end_time": "2026-06-19T00:00:00.000Z",
    },
)
print(res.json()["data"])
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "data": [
    {
      "date": "15/06/2026",
      "day": "monday",
      "room_name": "Room A",
      "available_slots": [
        { "start_time": "10:00", "end_time": "10:30", "available": true },
        { "start_time": "10:30", "end_time": "11:00", "available": true }
      ]
    },
    {
      "date": "16/06/2026",
      "day": "tuesday",
      "room_name": "Room A",
      "available_slots": [
        { "start_time": "09:00", "end_time": "09:30", "available": true, "spots_left": 2 }
      ]
    }
  ]
}
```

Ein Tag ohne freie Zeiten erscheint einfach nicht. Fehlende `event_id`, `start_time` oder `end_time` führen zu `400`; ein Ereignistyp, der nicht zu Ihrem Konto gehört, führt zu `404`.

---

## Einen Termin buchen

`POST /appointments`

Bucht einen neuen Termin für einen Kontakt für einen Ihrer Ereignistypen. Die Endzeit wird automatisch aus der Slot-Dauer des Ereignistyps berechnet.

Die Buchung wird auf Konflikte geprüft: Wenn sich der angeforderte Slot mit einem bestehenden bestätigten Termin für denselben Ereignistyp überschneidet, schlägt die Anfrage mit einem `409` fehl und es wird nichts erstellt.

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `contact_id` | Ja | ID des Kontakts, für den gebucht werden soll. Muss zu Ihrem Konto gehören. |
| `event_id` | Ja | ID des Ereignistyps, für den gebucht werden soll. Muss zu Ihrem Konto gehören. |
| `start_time` | Ja | Gewünschter Beginn als ISO 8601 Datum-Zeit-Format. |
| `room_name` | Nein | Name des Raums oder der Ressource, wenn der Ereignistyp Räume verwendet. |

**cURL** (unter Verwendung des Abfrageformulars `?apiKey=`)

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "contact_abc123",
    "event_id": "event_xyz789",
    "start_time": "2026-06-15T10:00:00.000Z",
    "room_name": "Room A"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.dmchamp.com/v1/appointments", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    contact_id: "contact_abc123",
    event_id: "event_xyz789",
    start_time: "2026-06-15T10:00:00.000Z",
    room_name: "Room A",
  }),
});
const data = await res.json();
console.log(data.appointment_id);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.dmchamp.com/v1/appointments",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={
        "contact_id": "contact_abc123",
        "event_id": "event_xyz789",
        "start_time": "2026-06-15T10:00:00.000Z",
        "room_name": "Room A",
    },
)
print(res.json()["appointment_id"])
```

**Antwort** (`201 Created`):

```json
{
  "success": true,
  "appointment_id": "aBcD1234eFgH5678",
  "appointment": {
    "id": "aBcD1234eFgH5678",
    "contact_id": "contact_abc123",
    "event_id": "event_xyz789",
    "status": "Confirmed",
    "start_time": "2026-06-15T10:00:00.000Z",
    "end_time": "2026-06-15T10:30:00.000Z",
    "created_at": "2026-06-10T09:00:00.000Z",
    "last_modified_at": "2026-06-10T09:00:00.000Z",
    "room_name": "Room A",
    "google_calendar_event_id": null,
    "calendar_synced": false
  }
}
```

---

## Termin abrufen

`GET /appointments/{appointmentId}`

Gibt einen einzelnen Termin anhand seiner ID zurück, einschließlich seines Kalender-Synchronisierungsstatus.

**cURL**

```bash
curl "https://api.dmchamp.com/v1/appointments/aBcD1234eFgH5678" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.dmchamp.com/v1/appointments/aBcD1234eFgH5678",
  { headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.appointment);
```

**Python**

```python
import requests

res = requests.get(
    "https://api.dmchamp.com/v1/appointments/aBcD1234eFgH5678",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["appointment"])
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "appointment": {
    "id": "aBcD1234eFgH5678",
    "contact_id": "contact_abc123",
    "event_id": "event_xyz789",
    "status": "Confirmed",
    "start_time": "2026-06-15T10:00:00.000Z",
    "end_time": "2026-06-15T10:30:00.000Z",
    "room_name": "Room A",
    "google_calendar_event_id": "abc123googleevent",
    "calendar_synced": true
  }
}
```

---

## Termine auflisten

`GET /appointments`

Listet Termine für Ihr Konto auf, beginnend mit dem neuesten, mit cursorbasierter Paginierung.

| Abfrageparameter | Erforderlich | Beschreibung |
|---|---|---|
| `contact_id` | Nein | Gibt nur Termine für diesen Kontakt zurück. Kontaktgefilterte Listen enthalten **nur bestätigte Termine**. |
| `date` | Nein | Gibt nur Termine an diesem Kalendertag zurück (`YYYY-MM-DD`). **Erfordert `contact_id`.** |
| `status` | Nein | Filtern nach `Confirmed` oder `Canceled`. Nur verfügbar **ohne** `contact_id`. |
| `limit` | Nein | Seitengröße, eine Ganzzahl zwischen 1 und 100. Standardwert `50`. |
| `cursor` | Nein | Der `next_cursor`-Wert aus einer vorherigen Antwort. |

Ein paar Regeln, die Sie beachten sollten:

- **Ohne Filter** erhalten Sie jeden Termin des Kontos, Seite für Seite.
- **Nach Kontakt** — setzen Sie `contact_id`, um die bestätigten Termine eines Kontakts zu sehen. Sie können dies auf einen einzelnen Tag eingrenzen, indem Sie zusätzlich `date` übergeben.
- **Nach Status** — setzen Sie `status` (ohne `contact_id`), um nur `Confirmed` oder nur `Canceled` Termine für das gesamte Konto aufzulisten.
- Der `date`-Filter ohne `contact_id`, oder `status=Canceled` zusammen mit `contact_id`, gibt einen `400` zurück.

**cURL**

```bash
curl "https://api.dmchamp.com/v1/appointments?contact_id=contact_abc123&date=2026-06-15" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const params = new URLSearchParams({
  contact_id: "contact_abc123",
  date: "2026-06-15",
});
const res = await fetch(
  `https://api.dmchamp.com/v1/appointments?${params}`,
  { headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.appointments, data.next_cursor);
```

**Python**

```python
import requests

res = requests.get(
    "https://api.dmchamp.com/v1/appointments",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"contact_id": "contact_abc123", "date": "2026-06-15"},
)
data = res.json()
print(data["appointments"], data["next_cursor"])
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "appointments": [
    {
      "id": "aBcD1234eFgH5678",
      "contact_id": "contact_abc123",
      "event_id": "event_xyz789",
      "status": "Confirmed",
      "start_time": "2026-06-15T10:00:00.000Z",
      "end_time": "2026-06-15T10:30:00.000Z",
      "calendar_synced": true
    }
  ],
  "next_cursor": null
}
```

Um durch die Ergebnisse zu blättern, übergeben Sie den `next_cursor` aus einer Antwort als `cursor` der nächsten Anfrage. Fahren Sie fort, bis `next_cursor` den Wert `null` hat. Siehe [Fehler & Paginierung](errors-and-pagination.md) für das allgemeine Paginierungsmuster.

---

## Termin aktualisieren

`PUT /appointments/{appointmentId}`

Verschieben Sie einen Termin oder ändern Sie dessen Details. Senden Sie nur die Felder, die Sie ändern möchten — mindestens eines ist erforderlich. Start und Ende müssen in chronologischer Reihenfolge bleiben (`end_time` muss nach `start_time` liegen). Änderungen werden automatisch mit dem verknüpften Kalenderereignis synchronisiert.

| Feld | Beschreibung |
|---|---|
| `start_time` | Neuer Startzeitpunkt, ISO 8601 Datum/Uhrzeit. |
| `end_time` | Neues Enddatum, ISO 8601 Datum/Uhrzeit. Muss nach der Startzeit liegen. |
| `room_name` | Neuer Raum- oder Ressourcenname. |
| `description` | Neue Beschreibung oder `null` zum Löschen. |
| `summary` | Neue Zusammenfassung oder `null` zum Löschen. |

**cURL**

```bash
curl -X PUT "https://api.dmchamp.com/v1/appointments/aBcD1234eFgH5678" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "start_time": "2026-06-16T10:00:00.000Z",
    "end_time": "2026-06-16T10:30:00.000Z"
  }'
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.dmchamp.com/v1/appointments/aBcD1234eFgH5678",
  {
    method: "PUT",
    headers: {
      "X-API-Key": "YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      start_time: "2026-06-16T10:00:00.000Z",
      end_time: "2026-06-16T10:30:00.000Z",
    }),
  }
);
const data = await res.json();
console.log(data.appointment);
```

**Python**

```python
import requests

res = requests.put(
    "https://api.dmchamp.com/v1/appointments/aBcD1234eFgH5678",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={
        "start_time": "2026-06-16T10:00:00.000Z",
        "end_time": "2026-06-16T10:30:00.000Z",
    },
)
print(res.json()["appointment"])
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "appointment_id": "aBcD1234eFgH5678",
  "appointment": {
    "id": "aBcD1234eFgH5678",
    "contact_id": "contact_abc123",
    "event_id": "event_xyz789",
    "status": "Confirmed",
    "start_time": "2026-06-16T10:00:00.000Z",
    "end_time": "2026-06-16T10:30:00.000Z",
    "calendar_synced": true
  }
}
```

---

## Termin stornieren

`POST /appointments/{appointmentId}/cancel`

Storniert einen bestätigten Termin, optional mit Angabe eines Grundes. Der Termin bleibt in Ihrem Konto mit dem Status `Canceled` erhalten, und das verknüpfte Kalenderereignis wird im Hintergrund automatisch entfernt. Das Stornieren eines bereits stornierten Termins führt zu einem `400`.

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `cancellation_reason` | Nein | Grund für die Stornierung, der beim Termin gespeichert wird. |

**cURL**

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments/aBcD1234eFgH5678/cancel" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cancellation_reason": "Client asked to reschedule next month"
  }'
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.dmchamp.com/v1/appointments/aBcD1234eFgH5678/cancel",
  {
    method: "POST",
    headers: {
      "X-API-Key": "YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      cancellation_reason: "Client asked to reschedule next month",
    }),
  }
);
const data = await res.json();
console.log(data.success);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.dmchamp.com/v1/appointments/aBcD1234eFgH5678/cancel",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={"cancellation_reason": "Client asked to reschedule next month"},
)
print(res.json()["success"])
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "appointment_id": "aBcD1234eFgH5678"
}
```

---

## Termin löschen

`DELETE /appointments/{appointmentId}`

Löscht einen Termin und dessen Referenzen dauerhaft. Wenn Sie die Buchung nur absagen, den Datensatz aber behalten möchten, verwenden Sie stattdessen [stornieren](#cancel-an-appointment).

**cURL**

```bash
curl -X DELETE "https://api.dmchamp.com/v1/appointments/aBcD1234eFgH5678" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.dmchamp.com/v1/appointments/aBcD1234eFgH5678",
  { method: "DELETE", headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.success);
```

**Python**

```python
import requests

res = requests.delete(
    "https://api.dmchamp.com/v1/appointments/aBcD1234eFgH5678",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["success"])
```

**Antwort** (`200 OK`):

```json
{
  "success": true
}
```

---

## Ihre verbundenen Google Kalender auflisten

`GET /appointments/google-calendars`

Gibt die für dieses Konto verfügbaren Google Kalender direkt von Google zurück – nützlich, um dem Kontoinhaber eine Auswahl zu zeigen, aus welchem Kalender importiert werden soll, oder einfach um zu bestätigen, dass die Verbindung aktiv ist.

Dies funktioniert erst, sobald das Konto Google Calendar (Einstellungen → Integrationen) mit mindestens Lesezugriff verbunden hat. Falls dies nicht der Fall ist oder der gewährte Zugriff nicht mehr den Bereich „Kalender lesen“ umfasst, erhalten Sie eine `400`, die Sie auffordert, ihn zu (re-)verbinden.

**cURL**

```bash
curl "https://api.dmchamp.com/v1/appointments/google-calendars" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.dmchamp.com/v1/appointments/google-calendars", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.data);
```

**Python**

```python
import requests

res = requests.get(
    "https://api.dmchamp.com/v1/appointments/google-calendars",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["data"])
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "data": [
    {
      "id": "primary",
      "summary": "jane@example.com",
      "timeZone": "America/New_York",
      "accessRole": "owner",
      "primary": true
    },
    {
      "id": "abcdefg1234567890@group.calendar.google.com",
      "summary": "Bookings",
      "timeZone": "America/New_York",
      "accessRole": "writer"
    }
  ]
}
```

Jeder Eintrag entspricht Googles eigenem [`CalendarListEntry`](https://developers.google.com/calendar/api/v3/reference/calendarList)-Format, daher folgen die Feldnamen Googles `camelCase` und nicht dem üblichen `snake_case` dieser API – es handelt sich um Googles Daten, die unverändert durchgereicht werden, nicht um unsere. Eine fehlende oder widerrufene Verbindung führt zu `400` mit einer Fehlermeldung, die erklärt, dass Google Calendar (re-)verbunden werden muss.

---

## Ereignisse aus einem Google Calendar importieren

`POST /appointments/import-calendar-events`

Ruft die Ereignisse ab, die bereits im verbundenen Google Calendar einer Kampagne oder eines KI-Agenten vorhanden sind, und wandelt sie in Termine um – nützlich, wenn Sie zum ersten Mal einen Kalender verbinden, der bereits Buchungen enthält. Dies kann einige Zeit in Anspruch nehmen (jedes Ereignis durchläuft eine Extraktion, um festzustellen, für wen es bestimmt ist), daher wird es nie inline ausgeführt: Die Anfrage stellt einen Hintergrundjob in die Warteschlange und gibt Ihnen eine `job_id` zum Abfragen zurück.

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `campaign_id` | Eines von beiden | Die Kampagne, deren verbundene(r) Kalender importiert werden soll(en). |
| `agent_id` | Eines von beiden | Der KI-Agent, dessen verbundene(r) Kalender importiert werden soll(en). |
| `identifier` | Ja | `"EMAIL"` oder `"PHONE_NUMBER"` – welche Kontaktinformation aus jedem Kalenderereignis extrahiert werden soll, um den zugehörigen Kontakt abzugleichen oder zu erstellen. |

Senden Sie genau eines von `campaign_id` / `agent_id`, niemals beide und niemals keines – jede andere Kombination führt zu `400`. Das Element, das Sie senden, muss zu Ihrem Konto gehören, andernfalls erhalten Sie ein `404`.

**cURL**

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments/import-calendar-events?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agent_abc123",
    "identifier": "EMAIL"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.dmchamp.com/v1/appointments/import-calendar-events", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    agent_id: "agent_abc123",
    identifier: "EMAIL",
  }),
});
const data = await res.json();
console.log(data.job_id);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.dmchamp.com/v1/appointments/import-calendar-events",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={"agent_id": "agent_abc123", "identifier": "EMAIL"},
)
print(res.json()["job_id"])
```

**Antwort** (`202 Accepted`):

```json
{
  "success": true,
  "job_id": "jK9mQ2xR7pL4wN1t",
  "status": "queued",
  "campaign_id": null,
  "agent_id": "agent_abc123"
}
```

`campaign_id` und `agent_id` geben das zurück, was Sie gesendet haben; das andere ist immer `null`.

### Den Import-Job abfragen

`GET /appointments/import-calendar-events/{jobId}`

```bash
curl "https://api.dmchamp.com/v1/appointments/import-calendar-events/jK9mQ2xR7pL4wN1t" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "job_id": "jK9mQ2xR7pL4wN1t",
  "status": "completed",
  "message": "Imported 12 events as appointments.",
  "error": null
}
```

| `status` | Bedeutung |
|---|---|
| `queued` | Noch nicht aufgenommen. Bitte weiter abfragen. |
| `processing` | Der Import läuft. Bitte weiter abfragen. |
| `completed` | Abgeschlossen – `message` enthält eine kurze, für Menschen lesbare Zusammenfassung. |
| `failed` | Etwas ist schiefgelaufen – `error` enthält den Grund. |

`GET` für ein `jobId`, das nicht existiert (oder zu einem anderen Konto gehört), gibt `404` zurück.

---

<a id="restaurant-booking-integrations-zenchef-formitable"></a>

## Externe Buchungsintegrationen (Zenchef / Formitable / OpenTable / TheFork / Trafft)

Zenchef und Formitable sind Restaurant-Reservierungssysteme, über die Ihr KI-Agent echte Tische buchen kann; [Trafft](#trafft) ist eine Terminplanungsplattform für terminbasierte Unternehmen, die einmal pro Konto und nicht pro Restaurant verbunden wird. Die beiden Restaurant-Plattformen verfügen jeweils über ein **öffentliches, nicht authentifiziertes Buchungs-Widget** (`https://api.dmchamp.com/v1/zenchef-widget/...` und `https://api.dmchamp.com/v1/formitable-widget/...`), das im Chat für den Gast gerendert wird – diese Widget-Routen sind einfache HTML-Seiten, die in einem Browser geöffnet werden sollen, keine JSON-API-Endpunkte, daher sind sie hier nicht dokumentiert. Was folgt, sind die Endpunkte für die Kontoverwaltung: Überprüfung, ob eine Restaurant-ID dem Kontoinhaber gehört, sowie das Hinzufügen, Aktualisieren oder Entfernen derselben.

### Zenchef

Die Verbindung eines Zenchef-Restaurants erfolgt über eine zweistufige Verifizierung, damit der Kontoinhaber nachweisen kann, dass er das Restaurant tatsächlich betreibt, bevor es mit dem Bot verknüpft wird: Zuerst wird geprüft, ob die ID existiert (ohne den Namen preiszugeben), dann muss der Benutzer den Namen des Restaurants selbst eingeben, und es wird geprüft, ob dieser übereinstimmt.

**Schritt 1 — Prüfen, ob eine Restaurant-ID existiert**

`POST /appointments/zenchef-restaurants/check`

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `restaurant_id` | Ja | Die zu prüfende Zenchef-Restaurant-ID. |

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments/zenchef-restaurants/check?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restaurant_id": "12345" }'
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "data": { "exists": true, "requiresNameVerification": true }
}
```

`exists: false` bedeutet, dass kein Zenchef-Restaurant diese ID hat — es ist nichts weiter zu tun. Die Rate-Limitierung liegt bei 10 Prüfungen pro 5 Minuten pro Konto; bei Überschreitung wird `429` zurückgegeben.

**Schritt 2 — Den Namen des Restaurants verifizieren**

`POST /appointments/zenchef-restaurants/verify-name`

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `restaurant_id` | Ja | Die Zenchef-Restaurant-ID aus Schritt 1. |
| `user_input_name` | Ja | Der vom Kontoinhaber eingegebene Name — wird mit dem tatsächlichen Namen des Restaurants auf Zenchef verglichen (Groß-/Kleinschreibung und Leerzeichen werden ignoriert). |

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments/zenchef-restaurants/verify-name?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restaurant_id": "12345", "user_input_name": "The Blue Door Bistro" }'
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "data": {
    "verified": true,
    "restaurantDetails": {
      "id": "12345",
      "name": "The Blue Door Bistro",
      "address": "1 Rue de Rivoli, Paris",
      "status": "active"
    }
  }
}
```

`verified: false` bedeutet, dass der Name nicht übereinstimmte — `restaurantDetails` wird weggelassen, bitten Sie den Kontoinhaber, es erneut zu versuchen. Die Rate-Limitierung liegt bei 3 Versuchen pro 5 Minuten (strenger als die Existenzprüfung, da dies der eigentliche Nachweisschritt ist). Eine `restaurant_id`, die auf Zenchef nicht mehr aufgelöst werden kann, gibt `404` zurück.

**Schritt 3 — Das Restaurant speichern**

`POST /appointments/zenchef-restaurants`

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `restaurant_id` | Ja | 1–64 Zeichen, Buchstaben/Zahlen/Unterstrich/Bindestrich. |
| `restaurant_name` | Ja | Der verifizierte Restaurantname aus Schritt 2. |

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments/zenchef-restaurants?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restaurant_id": "12345", "restaurant_name": "The Blue Door Bistro" }'
```

**Antwort** (`201 Created`):

```json
{ "success": true, "data": { "restaurantId": "12345" } }
```

**Ein gespeichertes Zenchef-Restaurant aktualisieren**

`PUT /appointments/zenchef-restaurants/{restaurantId}`

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `restaurant_name` | Nein | Neuer Anzeigename. |
| `is_active` | Nein | Setzen Sie `false`, um den Bot daran zu hindern, Buchungen für dieses Restaurant vorzunehmen, ohne es zu entfernen. |

```bash
curl -X PUT "https://api.dmchamp.com/v1/appointments/zenchef-restaurants/12345" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "is_active": false }'
```

**Antwort** (`200 OK`): gleiche Struktur wie die Antwort beim Speichern oben.

**Ein Zenchef-Restaurant entfernen**

`DELETE /appointments/zenchef-restaurants/{restaurantId}`

```bash
curl -X DELETE "https://api.dmchamp.com/v1/appointments/zenchef-restaurants/12345" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Antwort** (`200 OK`): `{ "success": true, "data": { "restaurantId": "12345" } }`

Ein `restaurantId`, das sich derzeit nicht im Konto befindet, gibt bei einer Aktualisierung oder Löschung `404` zurück.

### Formitable

Formitable benötigt nicht den zweistufigen Namensnachweis wie Zenchef – seine Restaurant-IDs sind bereits pro Unternehmen definiert, daher reicht ein Verifizierungsaufruf aus. Es verfügt außerdem über eine Detailabfrage, die verwendet wird, um die Website-URL des Restaurants während der Einrichtung zwischenzuspeichern.

**Eine Restaurant-ID verifizieren**

`POST /appointments/formitable-restaurants/verify`

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `restaurant_id` | Ja | Die Formitable-Restaurant-ID. |
| `language` | Nein | Sprach-Tag für die Testanfrage. Standardmäßig `"nl"`. |

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments/formitable-restaurants/verify?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restaurant_id": "the-blue-door", "language": "en" }'
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "data": {
    "verified": true,
    "restaurantDetails": {
      "restaurantId": "the-blue-door",
      "productCount": 4,
      "sampleProductTitle": "Dinner for two",
      "language": "en"
    }
  }
}
```

Ein `restaurant_id`, das Formitable nicht erkennt, gibt `404` zurück. Ratenbegrenzt auf 10 Versuche pro 5 Minuten pro Konto.

**Restaurantdetails abrufen**

`GET /appointments/formitable-restaurants/{restaurantId}/details?language=en`

Ruft das öffentliche Profil des Restaurants von Formitable ab, einschließlich seiner Website – dies wird verwendet, um die Website-URL während der Einrichtung des Restaurants zwischenzuspeichern. `language` ist ein optionaler Abfrageparameter, der standardmäßig auf `"en"` gesetzt ist.

```bash
curl "https://api.dmchamp.com/v1/appointments/formitable-restaurants/the-blue-door/details?language=en" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "data": {
    "uid": "the-blue-door",
    "name": "The Blue Door Bistro",
    "website": "https://thebluedoorbistro.com",
    "email": "info@thebluedoorbistro.com",
    "telephone": "+31201234567",
    "streetAddress": "Prinsengracht 1",
    "zipcode": "1015 AB",
    "city": "Amsterdam",
    "country": "Netherlands",
    "countryCode": "NL",
    "currency": "EUR"
  }
}
```

**Restaurant speichern**

`POST /appointments/formitable-restaurants`

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `restaurant_id` | Ja | 1–64 Zeichen, Buchstaben/Zahlen/Unterstrich/Bindestrich. |
| `restaurant_name` | Ja | Anzeigename. |
| `language` | Ja | ISO-Sprach-Tag, z. B. `"en"` oder `"en-GB"`. |
| `website_url` | Nein | Die Website des Restaurants aus der obigen Detailabfrage. Muss `http(s)://` sein. |

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments/formitable-restaurants?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "restaurant_id": "the-blue-door",
    "restaurant_name": "The Blue Door Bistro",
    "language": "en",
    "website_url": "https://thebluedoorbistro.com"
  }'
```

**Antwort** (`201 Created`): `{ "success": true, "data": { "restaurantId": "the-blue-door" } }`

**Ein gespeichertes Formitable-Restaurant aktualisieren**

`PUT /appointments/formitable-restaurants/{restaurantId}`

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `restaurant_name` | Nein | Neuer Anzeigename. |
| `language` | Nein | Neues ISO-Sprach-Tag. |
| `is_active` | Nein | Setzen Sie `false`, um den Bot daran zu hindern, Buchungen für dieses Restaurant vorzunehmen, ohne es zu entfernen. |
| `website_url` | Nein | Neue Website-URL. |

```bash
curl -X PUT "https://api.dmchamp.com/v1/appointments/formitable-restaurants/the-blue-door" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "is_active": false }'
```

**Antwort** (`200 OK`): gleiche Struktur wie die Antwort beim Speichern oben.

**Ein Formitable-Restaurant entfernen**

`DELETE /appointments/formitable-restaurants/{restaurantId}`

```bash
curl -X DELETE "https://api.dmchamp.com/v1/appointments/formitable-restaurants/the-blue-door" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Antwort** (`200 OK`): `{ "success": true, "data": { "restaurantId": "the-blue-door" } }`

Ein `restaurantId`, das sich derzeit nicht im Konto befindet, gibt bei einer Aktualisierung oder Löschung `404` zurück.

### OpenTable

OpenTable-Restaurants werden über die OpenTable-Partneranmeldedaten der Plattform erreicht, und OpenTable lässt diese Anmeldedaten nur Restaurants sehen, die den Eintrag der Plattform im Integrations-Marktplatz von OpenTable verbunden haben. Wie bei Formitable reicht also ein Verifizierungsaufruf aus: Eine erreichbare Restaurant-ID (die numerische „RID“) beweist sowohl, dass das Restaurant existiert, als auch, dass es die Integration verbunden hat. Bis der OpenTable-Partnereintrag auf der Plattform aktiviert ist, antwortet der Verifizierungsaufruf mit `503`.

**Ein OpenTable-Restaurant verifizieren**

`POST /appointments/opentable-restaurants/verify`

| Feld | Erforderlich | Beschreibung |
| --- | --- | --- |
| `restaurant_id` | Ja | Die OpenTable Restaurant-ID (RID), eine Zahl wie `1038007`. |

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments/opentable-restaurants/verify?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restaurant_id": "1038007" }'
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "data": {
    "verified": true,
    "restaurantDetails": {
      "restaurantId": "1038007",
      "diningAreaCount": 2,
      "diningAreaNames": ["Main Room", "Garden"],
      "tableTypes": ["default", "outdoor", "bar"]
    }
  }
}
```

`verified: false` bedeutet, dass kein OpenTable-Restaurant diese ID hat. Ein `403` bedeutet, dass das Restaurant existiert, aber die Integration der Plattform innerhalb von OpenTable noch nicht verbunden hat. Ratenbegrenzt auf 10 Versuche pro 5 Minuten pro Konto.

**Ein OpenTable-Restaurant hinzufügen**

`POST /appointments/opentable-restaurants`

| Feld | Erforderlich | Beschreibung |
| --- | --- | --- |
| `restaurant_id` | Ja | Die verifizierte Restaurant-ID. |
| `restaurant_name` | Ja | Anzeigename (eine Bezeichnung; auch der Name, mit dem die KI das Restaurant anspricht). |
| `website_url` | Nein | Eine http(s)-URL, die Gästen angezeigt wird, wenn die KI sie an das Restaurant weiterleitet. |

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments/opentable-restaurants?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restaurant_id": "1038007", "restaurant_name": "The Blue Door", "website_url": "https://thebluedoor.example" }'
```

**Antwort** (`201 Created`): `{ "success": true, "data": { "restaurantId": "1038007" } }`

**Ein gespeichertes OpenTable-Restaurant aktualisieren**

`PUT /appointments/opentable-restaurants/{restaurantId}`

Senden Sie eines der Felder `restaurant_name`, `is_active` (pausieren mit `false`) oder `website_url` (ein leerer String löscht es); ausgelassene Felder bleiben unverändert.

```bash
curl -X PUT "https://api.dmchamp.com/v1/appointments/opentable-restaurants/1038007" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "is_active": false }'
```

**Antwort** (`200 OK`): `{ "success": true, "data": { "restaurantId": "1038007" } }`

**Ein OpenTable-Restaurant entfernen**

`DELETE /appointments/opentable-restaurants/{restaurantId}`

```bash
curl -X DELETE "https://api.dmchamp.com/v1/appointments/opentable-restaurants/1038007" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Antwort** (`200 OK`): `{ "success": true, "data": { "restaurantId": "1038007" } }`

Ein `restaurantId`, das sich derzeit nicht auf dem Konto befindet, gibt beim Update `404` zurück.

### TheFork

TheFork-Restaurants werden über die TheFork-Partneranmeldedaten der Plattform erreicht, und TheFork erlaubt diesen Anmeldedaten nur den Zugriff auf Restaurants, bei denen der Partner der Plattform in ihrem TheFork-Konto aktiviert ist. Wie bei Formitable und OpenTable reicht also ein Verifizierungsaufruf aus: Eine erreichbare Restaurant-ID beweist sowohl, dass das Restaurant existiert, als auch, dass der Partner dafür aktiviert ist. Die ID ist die UUID, die TheFork dem Restaurant im TheFork Manager zuweist, und wird als Zeichenfolge gesendet. Bis TheFork die Plattform als Partner genehmigt und die Anmeldedaten ausgestellt hat, antwortet der Verifizierungsaufruf mit `503` – siehe [TheFork](../integrations/thefork.md), was das heute bedeutet.

**Ein TheFork-Restaurant verifizieren**

`POST /appointments/thefork-restaurants/verify`

| Feld | Erforderlich | Beschreibung |
| --- | --- | --- |
| `restaurant_id` | Ja | Die TheFork-Restaurant-ID, eine UUID wie z. B. `9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60`. |

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments/thefork-restaurants/verify?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restaurant_id": "9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60" }'
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "data": {
    "verified": true,
    "restaurantDetails": {
      "restaurantId": "9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60",
      "partySizes": [1, 2, 3, 4, 5, 6, 7, 8]
    }
  }
}
```

`partySizes` sind die Gruppengrößen, die das Restaurant in den nächsten 30 Tagen online akzeptiert. Ein `404` oder `403` bedeutet, dass TheFork uns kein Restaurant mit dieser ID zur Verfügung gestellt hat – entweder ist die ID falsch oder der Partner der Plattform ist für dieses Restaurant noch nicht aktiviert; ein `400` bedeutet, dass die ID keine UUID ist. Ratenbegrenzung auf 10 Versuche pro 5 Minuten pro Konto.

**Ein TheFork-Restaurant hinzufügen**

`POST /appointments/thefork-restaurants`

| Feld | Erforderlich | Beschreibung |
| --- | --- | --- |
| `restaurant_id` | Ja | Die verifizierte Restaurant-ID (UUID). |
| `restaurant_name` | Ja | Anzeigename (eine Bezeichnung; auch der Name, unter dem die KI das Restaurant nennt). |
| `website_url` | Nein | Eine http(s)-URL, die Gästen angezeigt wird, wenn die KI sie an das Restaurant weiterleitet. |

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments/thefork-restaurants?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restaurant_id": "9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60", "restaurant_name": "The Blue Door", "website_url": "https://thebluedoor.example" }'
```

**Antwort** (`201 Created`): `{ "success": true, "data": { "restaurantId": "9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60" } }`

**Ein gespeichertes TheFork-Restaurant aktualisieren**

`PUT /appointments/thefork-restaurants/{restaurantId}`

Senden Sie eines der Felder `restaurant_name`, `is_active` (pausieren mit `false`) oder `website_url` (ein leerer String löscht es); ausgelassene Felder bleiben unverändert.

```bash
curl -X PUT "https://api.dmchamp.com/v1/appointments/thefork-restaurants/9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "is_active": false }'
```

**Antwort** (`200 OK`): `{ "success": true, "data": { "restaurantId": "9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60" } }`

**Ein TheFork-Restaurant entfernen**

`DELETE /appointments/thefork-restaurants/{restaurantId}`

```bash
curl -X DELETE "https://api.dmchamp.com/v1/appointments/thefork-restaurants/9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Antwort** (`200 OK`): `{ "success": true, "data": { "restaurantId": "9f2a1c34-5b6d-4e7f-8a90-1b2c3d4e5f60" } }`

Ein `restaurantId`, das sich derzeit nicht im Konto befindet, gibt bei einer Aktualisierung oder Löschung `404` zurück.

### Trafft

Trafft wird einmal für das gesamte Konto verbunden, nicht pro Standort: eine Firmenadresse plus die API-Anmeldedaten aus dem Trafft-Admin-Panel (**Features & Integrations → API & Connectors**, Teil des Business-Plans von Trafft). Der Connect-Aufruf prüft diese Anmeldedaten bei Trafft, bevor etwas gespeichert wird, sodass eine falsche Adresse, falsche Anmeldedaten oder ein Plan ohne API-Zugriff hier fehlschlagen, anstatt in einem Kundengespräch. Das Client Secret wird verschlüsselt gespeichert und von keinem Endpunkt jemals zurückgegeben.

Alle drei Schreibaufrufe (`POST`, `PUT`, `DELETE`) erfordern die Berechtigung **edit** für Integrationen; der Status `GET` benötigt **view** für Integrationen.

**Trafft verbinden**

`POST /appointments/trafft/connect`

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `subdomain` | Ja | Die Firmenadresse – der Teil vor `.admin.trafft.com` in der URL, unter der Sie sich anmelden, z. B. `acme`. Eine vollständige Adresse wird akzeptiert und auf denselben Wert reduziert. |
| `client_id` | Ja | Client-ID von der Seite „API & Connectors“ in Trafft. |
| `client_secret` | Ja | Client Secret von derselben Seite. Wird verschlüsselt gespeichert und niemals zurückgegeben. |
| `company_name` | Nein | Eine Bezeichnung für Ihre eigene Liste. |

```bash
curl -X POST "https://api.dmchamp.com/v1/appointments/trafft/connect?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "subdomain": "acme",
    "client_id": "YOUR_TRAFFT_CLIENT_ID",
    "client_secret": "YOUR_TRAFFT_CLIENT_SECRET",
    "company_name": "Acme Salon"
  }'
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "data": {
    "subdomain": "acme",
    "service_count": 12,
    "employee_count": 4,
    "location_count": 2
  }
}
```

Die Zählungen werden während der Überprüfung von Trafft zurückgegeben – sie sind der schnellste Weg, um zu bestätigen, dass die Anmeldedaten auf das von Ihnen beabsichtigte Konto verweisen.

**Verbindungsstatus abrufen**

`GET /appointments/trafft`

```bash
curl "https://api.dmchamp.com/v1/appointments/trafft" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Antwort** (`200 OK`):

```json
{
  "success": true,
  "data": {
    "connected": true,
    "subdomain": "acme",
    "hostname": "acme.admin.trafft.com",
    "client_id": "YOUR_TRAFFT_CLIENT_ID",
    "company_name": "Acme Salon",
    "is_active": true,
    "service_count": 12,
    "employee_count": 4,
    "location_count": 2
  }
}
```

`connected: false` bedeutet, dass noch nichts eingerichtet ist. Das Client Secret ist in dieser Antwort niemals enthalten.

**Verbindung aktualisieren**

`PUT /appointments/trafft`

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `is_active` | Nein | Setzen Sie `false` auf „pause“ – die KI stoppt die Buchungen in Trafft, die Verbindung bleibt bestehen. `true` setzt sie fort. |
| `company_name` | Nein | Neues Label. |

```bash
curl -X PUT "https://api.dmchamp.com/v1/appointments/trafft" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "is_active": false }'
```

**Antwort** (`200 OK`): dasselbe Verbindungsobjekt wie das `GET` oben.

**Trafft trennen**

`DELETE /appointments/trafft`

```bash
curl -X DELETE "https://api.dmchamp.com/v1/appointments/trafft" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Antwort** (`200 OK`): `{ "success": true }`

Durch das Trennen werden nur die gespeicherten Anmeldedaten entfernt. Termine, die bereits in Trafft vorhanden sind, bleiben unberührt.

> **Fehlerstruktur bei allen Zenchef-/Formitable-/OpenTable-/TheFork-Endpunkten:** Im Gegensatz zum Rest dieser Seite wird der Status hier zweimal angegeben – einmal als HTTP-Status und einmal als `error_code` im Body –, zum Beispiel `{ "success": false, "error": "Restaurant not found", "error_code": 404 }`. Behandeln Sie dies wie jeden anderen Fehler: Überprüfen Sie `success` und lesen Sie `error` für die Nachricht.

---

## Fehler der Appointments-API

Appointment-Endpunkte geben den Standard-Fehlerumschlag zurück:

```json
{
  "success": false,
  "error": "Appointment not found"
}
```

| Status | Wann dies bei einem Appointment-Endpunkt auftritt |
|---|---|
| `400` | Ein erforderliches Feld fehlt oder ist ungültig – zum Beispiel ein fehlerhaftes `start_time`, ein `end_time`, das nicht nach `start_time` liegt, eine ungültige Filterkombination, keine zu aktualisierenden Felder oder ein bereits stornierter Termin. |
| `404` | Der Termin, der Kontakt oder der Ereignistyp wurde nicht gefunden. |
| `409` | Das angeforderte Zeitfenster ist bereits belegt (Buchungskonflikt). |

Die gemeinsamen Codes, die jeder Endpunkt zurückgeben kann — `401`, `403` (Ihr Plan beinhaltet keinen API-Zugriff), `429` (Ratenbegrenzung) und `500` — sind zusammen mit Hinweisen zur Wiederholung unter [Fehler & Paginierung](errors-and-pagination.md) aufgeführt.

---

::: master-only
## Verwenden Sie Ihren eigenen Google OAuth-Client (Zustimmungsbildschirm für Kalender)

Wenn ein Konto Google Kalender verbindet, nennt das Anmeldefenster von Google das Projekt des OAuth-Clients – standardmäßig das der Plattform. Eine Agentur kann ihren eigenen Google OAuth 2.0-Client auf dem Agenturkonto registrieren; von da an läuft die Kalenderverbindung für dieses Konto und jedes untergeordnete Konto über diesen Client, sodass der Zustimmungsbildschirm den Namen und das Logo der Agentur anzeigt. Nichts anderes ändert sich: Der Verbindungsprozess, die bidirektionale Synchronisierung und die oben genannten Termin-Endpunkte funktionieren genau wie zuvor.

> **Nur Google Kalender.** Das Gmail-Postfach-OAuth für den E-Mail-Kanal ist davon nicht betroffen.

### Was Ihr Client zuerst benötigt

1. **Ein OAuth 2.0-Client** vom Typ Webanwendung in Ihrem Google Cloud-Projekt, bei dem die **Google Calendar API** für dieses Projekt aktiviert ist.
2. **Jeder `redirect_uris`-Eintrag** (zurückgegeben von den unten stehenden Endpunkten), der unter den autorisierten Weiterleitungs-URIs des Clients hinzugefügt wurde. Der erste Eintrag ist Ihre verifizierte `api.`-Domain, sofern Sie eine haben – Google verifiziert nur eine Marke, deren Weiterleitung auf einer Domain liegt, die Sie besitzen – gefolgt vom neutralen Host der Plattform als Fallback, der bis dahin verwendet wird.
3. **Der Zustimmungsbildschirm** mit Ihrer Marke, Ihrer Domain unter autorisierten Domains und den beiden deklarierten Kalender-Scopes (`scopes` in der Antwort). Bis die App veröffentlicht und von Google verifiziert wurde, sehen Benutzer eine Warnung vor nicht verifizierten Apps und der Client ist auf 100 Benutzer begrenzt.

### Speichern Sie Ihren Client

`PUT /account-config/google-oauth-client`

| Feld | Erforderlich | Beschreibung |
|---|---|---|
| `client_id` | Ja | Die OAuth 2.0-Client-ID, die auf `.apps.googleusercontent.com` endet. |
| `client_secret` | Ja | Das Client-Secret. Wird vor der Speicherung bei Google verifiziert und anschließend verschlüsselt. Wird von keinem Endpunkt zurückgegeben. |

```bash
curl -X PUT "https://api.dmchamp.com/v1/account-config/google-oauth-client?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "123456789012-abcdefghijklmnop.apps.googleusercontent.com",
    "client_secret": "GOCSPX-your-client-secret"
  }'
```

**Antwort**

```json
{
  "success": true,
  "configured": true,
  "client_id": "123456789012-abcdefghijklmnop.apps.googleusercontent.com",
  "redirect_uris": [
    "https://api.youragency.com/v1/auth-google-callback",
    "https://api.dmchamp.com/v1/auth-google-callback"
  ],
  "scopes": [
    "https://www.googleapis.com/auth/calendar.events",
    "https://www.googleapis.com/auth/calendar.readonly"
  ],
  "setup": ["…"]
}
```

Ein falsches Secret oder eine unbekannte Client-ID wird mit `400` und dem eigenen Grund von Google in `error` abgelehnt, und es wird nichts gespeichert.

### Lesen oder entfernen

`GET /account-config/google-oauth-client` gibt jederzeit dieselbe Zusammenfassung zurück — `configured: false` plus `redirect_uris` und `scopes`, bevor etwas gespeichert wird, sodass Sie zuerst die Google-Seite einrichten können. `DELETE /account-config/google-oauth-client` entfernt den Client: Neue Verbindungen greifen wieder auf den Plattform-Client zurück, und Kalender, die über den entfernten Client verbunden waren, müssen neu verbunden werden, da nur der Client, der eine Verbindung hergestellt hat, diese auch aktualisieren kann.

Teammitglieder benötigen **Integrations: view** für `GET` und **Integrations: edit** für `PUT` / `DELETE`.
:::

---

## Nächste Schritte

- [Kontakte](contacts.md) — Erstellen und suchen Sie die Kontakte, für die Sie buchen.
- [Nachrichten & Unterhaltungen](messages.md) — Senden Sie einem Kontakt eine Bestätigung oder Erinnerung.
- [Webhooks](webhooks.md) — Lassen Sie sich benachrichtigen, wenn sich Termine ändern.
