
# Termine

Die Appointments API ermöglicht es Ihnen, Termine für Ihre Kontakte für Ihre Ereignistypen zu buchen sowie diese abzurufen, aufzulisten, zu aktualisieren, zu stornieren oder zu 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 synchron gehalten. Restaurants, die Zenchef oder Formitable 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.

Alle Pfade auf dieser Seite beziehen sich auf die Basis-URL `https://api.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.com/v1/appointments/aBcD1234eFgH5678" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.com/v1/appointments/aBcD1234eFgH5678" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.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.youraiconnector.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.youraiconnector.com/v1/appointments/google-calendars" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.

---

## Restaurant-Buchungsintegrationen (Zenchef / Formitable)

Zenchef und Formitable sind Restaurant-Reservierungssysteme, über die Ihr KI-Agent echte Tische buchen kann. Jedes verfügt über ein **öffentliches, nicht authentifiziertes Buchungs-Widget** (`https://api.youraiconnector.com/v1/zenchef-widget/...` und `https://api.youraiconnector.com/v1/formitable-widget/...`), das innerhalb des Chats 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. Im Folgenden finden Sie 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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.youraiconnector.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.

> **Fehlerstruktur bei allen Zenchef/Formitable-Endpunkten:** Im Gegensatz zum Rest dieser Seite enthalten Fehler hier ihren Status zweimal – 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: Prüfen Sie `success`, 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.

---

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