
# Afspraken

Met de Appointments API kun je afspraken boeken voor je contacten op basis van je eventtypes, en deze vervolgens ophalen, weergeven, bijwerken, annuleren of verwijderen. Het beantwoordt ook de vraag die in de meeste boekingsstromen als eerste komt — welke tijden zijn daadwerkelijk vrij — en dekt de agendakant: het weergeven van de Google-agenda's die je hebt gekoppeld en het importeren van afspraken die daar al in staan. Wanneer een Google Agenda-koppeling actief is, wordt de bijbehorende agenda-afspraak automatisch op de achtergrond aangemaakt en gesynchroniseerd. Restaurants die Zenchef, Formitable, OpenTable of TheFork gebruiken voor hun eigen reserveringssysteem kunnen hier ook worden geverifieerd en gekoppeld, zodat de AI Agent echte tafels boekt in plaats van interne afspraken — en bedrijven die hun planning in Trafft beheren, kunnen op dezelfde manier koppelen.

Alle paden op deze pagina zijn relatief ten opzichte van de basis-URL `https://api.dmchamp.com/v1`. Voor elk verzoek is je API-sleutel vereist — zie [Authenticatie](authentication.md) voor de volledige lijst met manieren om deze te verzenden. De onderstaande voorbeelden gebruiken de `X-API-Key`-header, waarbij één cURL-voorbeeld ook de `?apiKey=`-queryvorm laat zien.

> **Gebeurtenissen versus afspraken:** Een *gebeurtenistype* is een definitie van een boekbaar tijdslot (het soort vergadering, de duur, de ruimtes). Een *afspraak* is één geboekte instantie van een gebeurtenistype voor een specifiek contact. Je boekt een afspraak door te verwijzen naar het contact en het gebeurtenistype.

---

## Het afspraakobject

Elk eindpunt dat een afspraak retourneert, gebruikt dezelfde structuur:

| Veld | Beschrijving |
|---|---|
| `id` | Unieke ID van de afspraak. |
| `contact_id` | ID van het contact waarmee de afspraak is geboekt. |
| `event_id` | ID van het gebeurtenistype waarop de afspraak is geboekt. |
| `status` | `Confirmed` of `Canceled`. |
| `start_time` | Starttijd van de afspraak, ISO 8601 in UTC. |
| `end_time` | Eindtijd van de afspraak, ISO 8601 in UTC. |
| `created_at` | Wanneer de afspraak is aangemaakt. |
| `last_modified_at` | Wanneer de afspraak voor het laatst is gewijzigd. |
| `room_name` | Ruimte of bron waarin de afspraak is geboekt, wanneer het gebeurtenistype gebruikmaakt van ruimtes. |
| `description` | Vrije beschrijving van de afspraak. |
| `summary` | Korte samenvatting of titel. |
| `cancelation_reason` | Reden opgegeven bij het annuleren van de afspraak, indien van toepassing. |
| `google_calendar_event_id` | ID van de gekoppelde Google Agenda-gebeurtenis. Wordt ingesteld zodra de agendasynchronisatie is voltooid; `null` wanneer er geen agenda is gekoppeld of terwijl de synchronisatie nog bezig is. |
| `calendar_synced` | `true` zodra de afspraak is gekoppeld aan een agenda-gebeurtenis. |
| `imported` | `true` wanneer de afspraak is geïmporteerd vanuit een externe agenda in plaats van direct geboekt. |
| `is_recurring` | `true` wanneer de afspraak deel uitmaakt van een terugkerende reeks. |
| `recurrence_frequency` | Hoe vaak de afspraak zich herhaalt, indien terugkerend. |
| `recurring_event_id` | ID van de terugkerende reeks waartoe deze afspraak behoort. |
| `recurring_interval` | Interval tussen herhalingen, indien terugkerend. |
| `recurring_sequence` | Positie van deze afspraak binnen de terugkerende reeks. |
| `end_after_x_occurrences` | Aantal voorkomens waarna de terugkerende reeks eindigt. |
| `booking_provider` | Bronsysteem waar de boeking vandaan komt, indien geboekt via een gekoppelde reserveringsaanbieder. |

> **Over agendasynchronisatie:** Direct nadat je een afspraak hebt geboekt of gewijzigd, kan `google_calendar_event_id` nog `null` zijn en kan `calendar_synced` `false` zijn, omdat de synchronisatie een moment later op de achtergrond wordt uitgevoerd. Haal de afspraak kort daarna opnieuw op om de ingevulde agendavelden te zien.

---

## Beschikbare tijdsloten vinden

`GET /appointments/available-slots`

Geeft de tijden terug die daadwerkelijk vrij zijn voor een eventtype tussen twee momenten. Dit is normaal gesproken de **eerste** aanroep in een boekingsstroom: toon deze tijdsloten, laat de persoon er een kiezen en verstuur vervolgens de gekozen tijd naar [Een afspraak boeken](#book-an-appointment).

Het antwoord houdt al rekening met de openingstijden en de lengte van het tijdslot van het eventtype zelf, de ruimtes, afspraken die je er al op hebt geboekt en alles wat geblokkeerd is in de gekoppelde Google-agenda's — dus een tijdslot dat hier wordt teruggegeven, is een tijdslot dat je kunt boeken.

| Query-parameter | Vereist | Beschrijving |
|---|---|---|
| `event_id` | Ja | Het eventtype om te controleren. Moet bij jouw account horen. |
| `start_time` | Ja | Begin van het venster waarvoor je tijdsloten wilt, ISO 8601 datum-tijd. |
| `end_time` | Ja | Einde van het venster, ISO 8601 datum-tijd. De gehele einddag is inbegrepen. |

De resultaten worden gegroepeerd per dag teruggegeven — en, wanneer het eventtype gebruikmaakt van ruimtes, één groep per ruimte per dag:

| Veld | Beschrijving |
|---|---|
| `date` | De dag die de groep beslaat, geschreven als `DD/MM/YYYY`. |
| `day` | Naam van de weekdag in kleine letters, bijvoorbeeld `monday`. |
| `room_name` | De ruimte of bron waar deze groep bij hoort, wanneer het eventtype gebruikmaakt van ruimtes. |
| `available_slots` | De boekbare blokken op die dag, vroegste eerst. |

Elk item in `available_slots` bevat:

| Veld | Beschrijving |
|---|---|
| `start_time` | Begin van het blok als `HH:mm`. |
| `end_time` | Einde van het blok als `HH:mm`. |
| `available` | `true` — alleen vrije tijd wordt geretourneerd. |
| `spots_left` | Hoeveel boekingen er nog in dit blok passen. Alleen aanwezig bij eventtypes die meer dan één boeking per tijdslot toestaan. |

> **Tijden zijn lokaal voor het eventtype, niet UTC.** `date`, `start_time` en `end_time` zijn kloktijden in de eigen tijdzone van het eventtype (de overschrijving ervan, of de tijdzone van je account als er geen is). [Een afspraak boeken](#book-an-appointment) verwacht een ISO 8601 UTC-moment, dus converteer het gekozen tijdslot voordat je het verstuurt.

**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"])
```

**Antwoord** (`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 }
      ]
    }
  ]
}
```

Een dag waarop niets vrij is, verschijnt simpelweg niet. Het ontbreken van `event_id`, `start_time` of `end_time` resulteert in `400`; een eventtype dat niet bij jouw account hoort, resulteert in `404`.

---

## Een afspraak boeken

`POST /appointments`

Boekt een nieuwe afspraak voor een contact op een van je gebeurtenistypen. De eindtijd wordt automatisch berekend op basis van de duur van het tijdslot van het gebeurtenistype.

De boeking wordt gecontroleerd op conflicten: als het aangevraagde tijdslot overlapt met een bestaande bevestigde afspraak voor hetzelfde gebeurtenistype, mislukt het verzoek met een `409` en wordt er niets aangemaakt.

| Veld | Vereist | Beschrijving |
|---|---|---|
| `contact_id` | Ja | ID van het contact waarvoor geboekt moet worden. Moet tot je account behoren. |
| `event_id` | Ja | ID van het gebeurtenistype waarop geboekt moet worden. Moet tot je account behoren. |
| `start_time` | Ja | Gewenste starttijd als ISO 8601 datum-tijd. |
| `room_name` | Nee | Naam van de ruimte of bron, wanneer het gebeurtenistype gebruikmaakt van ruimtes. |

**cURL** (met gebruik van de `?apiKey=`-queryvorm)

```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"])
```

**Antwoord** (`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
  }
}
```

---

## Een afspraak ophalen

`GET /appointments/{appointmentId}`

Geeft één afspraak terug op basis van het ID, inclusief de synchronisatiestatus met de agenda.

**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"])
```

**Antwoord** (`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
  }
}
```

---

## Afspraken weergeven

`GET /appointments`

Geeft een lijst met afspraken voor uw account, beginnend bij de nieuwste, met cursor-gebaseerde paginering.

| Query-parameter | Verplicht | Beschrijving |
|---|---|---|
| `contact_id` | Nee | Retourneer alleen afspraken voor dit contact. Met contact gefilterde lijsten bevatten **alleen bevestigde afspraken** |
| `date` | Nee | Retourneer alleen afspraken op deze kalenderdag (`YYYY-MM-DD`). **Vereist `contact_id`.** |
| `status` | Nee | Filter op `Confirmed` of `Canceled`. Alleen beschikbaar **zonder** `contact_id`. |
| `limit` | Nee | Paginagrootte, een geheel getal tussen 1 en 100. Standaard `50`. |
| `cursor` | Nee | De `next_cursor`-waarde van een vorig antwoord. |

Een paar regels om rekening mee te houden:

- **Zonder filters** krijgt u elke afspraak in het account, pagina voor pagina.
- **Op contact** — stel `contact_id` in om de bevestigde afspraken van één contact te zien. U kunt dit beperken tot één dag door ook `date` mee te geven.
- **Op status** — stel `status` in (zonder `contact_id`) om alleen `Confirmed` of alleen `Canceled` afspraken in het hele account weer te geven.
- Het `date`-filter zonder `contact_id`, of `status=Canceled` samen met `contact_id`, retourneert een `400`.

**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"])
```

**Antwoord** (`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
}
```

Om door de resultaten te bladeren, geeft u de `next_cursor` van het ene antwoord door als de `cursor` van het volgende verzoek. Ga door totdat `next_cursor` gelijk is aan `null`. Zie [Fouten & Paginering](errors-and-pagination.md) voor het gedeelde pagineringspatroon.

---

## Een afspraak bijwerken

`PUT /appointments/{appointmentId}`

Plan een afspraak opnieuw in of wijzig de details. Stuur alleen de velden die u wilt wijzigen — er is er minimaal één vereist. De gecombineerde start- en eindtijd moeten in chronologische volgorde blijven (`end_time` moet na `start_time` vallen). Wijzigingen worden automatisch gesynchroniseerd met het gekoppelde agendagebeurtenis.

| Veld | Beschrijving |
|---|---|
| `start_time` | Nieuwe starttijd, ISO 8601 datum-tijd. |
| `end_time` | Nieuwe eindtijd, ISO 8601 datum-tijd. Moet na de starttijd liggen. |
| `room_name` | Nieuwe naam voor de ruimte of resource. |
| `description` | Nieuwe beschrijving, of `null` om deze te wissen. |
| `summary` | Nieuwe samenvatting, of `null` om deze te wissen. |

**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"])
```

**Antwoord** (`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
  }
}
```

---

## Een afspraak annuleren

`POST /appointments/{appointmentId}/cancel`

Annuleert een bevestigde afspraak, waarbij optioneel een reden kan worden opgegeven. De afspraak blijft in uw account staan met de status `Canceled` en de gekoppelde agendagebeurtenis wordt automatisch op de achtergrond verwijderd. Het annuleren van een reeds geannuleerde afspraak resulteert in een `400`.

| Veld | Verplicht | Beschrijving |
|---|---|---|
| `cancellation_reason` | Nee | Reden voor de annulering, opgeslagen bij de afspraak. |

**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"])
```

**Antwoord** (`200 OK`):

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

---

## Een afspraak verwijderen

`DELETE /appointments/{appointmentId}`

Verwijdert een afspraak en de bijbehorende verwijzingen definitief. Als u de boeking alleen wilt afzeggen maar het record wilt behouden, gebruik dan in plaats daarvan [annuleren](#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"])
```

**Antwoord** (`200 OK`):

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

---

## Je gekoppelde Google-agenda's weergeven

`GET /appointments/google-calendars`

Geeft de Google-agenda's terug die beschikbaar zijn voor dit account, rechtstreeks vanuit Google — handig om de accounthouder een keuze te laten maken uit welke agenda hieronder geïmporteerd moet worden, of gewoon om te bevestigen dat de koppeling actief is.

Dit werkt pas zodra het account Google Calendar heeft gekoppeld (Instellingen → Integraties) met ten minste leestoegang. Als dit niet het geval is, of als de verleende toegang niet langer de calendar-read scope bevat, ontvang je een `400` waarin wordt gevraagd deze te (her)koppelen.

**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"])
```

**Antwoord** (`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"
    }
  ]
}
```

Elk item heeft de vorm van Google's eigen [`CalendarListEntry`](https://developers.google.com/calendar/api/v3/reference/calendarList), dus veldnamen volgen Google's `camelCase`, niet de gebruikelijke `snake_case` van deze API — dat zijn Google's gegevens die ongewijzigd worden doorgegeven, niet de onze. Een ontbrekende of ingetrokken koppeling retourneert `400` met een foutmelding die uitlegt dat Google Calendar moet worden (her)gekoppeld.

---

## Evenementen importeren uit een Google Calendar

`POST /appointments/import-calendar-events`

Haalt de evenementen op die al in de gekoppelde Google Calendar(s) van een campagne of AI-agent staan en zet deze om in afspraken — handig de eerste keer dat je een agenda koppelt waar al boekingen in staan. Dit kan even duren (elk evenement wordt geëxtraheerd om te achterhalen voor wie het is), dus het wordt nooit inline uitgevoerd: het verzoek plaatst een achtergrondtaak in de wachtrij en geeft je een `job_id` terug om te pollen.

| Veld | Verplicht | Beschrijving |
|---|---|---|
| `campaign_id` | Eén van deze twee | De campagne waarvan de gekoppelde agenda('s) moeten worden geïmporteerd. |
| `agent_id` | Eén van deze twee | De AI-agent waarvan de gekoppelde agenda('s) moeten worden geïmporteerd. |
| `identifier` | Ja | `"EMAIL"` of `"PHONE_NUMBER"` — welk contactgegeven uit elk agenda-evenement moet worden geëxtraheerd om de bijbehorende contactpersoon te matchen of aan te maken. |

Stuur precies één van `campaign_id` / `agent_id`, nooit beide en nooit geen van beide — elke andere combinatie retourneert een `400`. Degene die je verstuurt, moet bij je account horen, anders krijg je een `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"])
```

**Antwoord** (`202 Accepted`):

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

`campaign_id` en `agent_id` echoën terug welke je hebt verstuurd; de andere is altijd `null`.

### De importtaak pollen

`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"
```

**Antwoord** (`200 OK`):

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

| `status` | Betekenis |
|---|---|
| `queued` | Nog niet opgepakt. Blijf pollen. |
| `processing` | De import is bezig. Blijf pollen. |
| `completed` | Klaar — `message` bevat een korte, leesbare samenvatting. |
| `failed` | Er is iets misgegaan — `error` bevat de reden. |

`GET` op een `jobId` die niet bestaat (of bij een ander account hoort) retourneert `404`.

---

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

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

Zenchef en Formitable zijn restaurantreserveringssystemen waar je AI Agent echte tafels via kan boeken; [Trafft](#trafft) is een planningsplatform voor bedrijven die met afspraken werken, en wordt één keer per account gekoppeld in plaats van per restaurant. De twee restaurantplatforms hebben elk een **openbare, niet-geauthenticeerde boekingswidget** (`https://api.dmchamp.com/v1/zenchef-widget/...` en `https://api.dmchamp.com/v1/formitable-widget/...`) die in de chat voor de gast wordt weergegeven — die widget-routes zijn gewone HTML-pagina's die bedoeld zijn om in een browser te worden geopend, geen JSON API-endpoints, dus ze worden hier niet gedocumenteerd. Wat volgt zijn de accountbeheer-endpoints: het verifiëren of een restaurant-ID bij de accounthouder hoort, en het vervolgens toevoegen, bijwerken of verwijderen ervan.

### Zenchef

Het koppelen van een Zenchef-restaurant is een verificatie in twee stappen, zodat de accounthouder bewijst dat hij het restaurant daadwerkelijk beheert voordat het aan de bot wordt gekoppeld: controleer eerst of het ID bestaat (zonder de naam te onthullen), en laat ze vervolgens zelf de naam van het restaurant typen en verifieer of deze overeenkomt.

**Stap 1 — Controleren of een restaurant-ID bestaat**

`POST /appointments/zenchef-restaurants/check`

| Veld | Verplicht | Beschrijving |
|---|---|---|
| `restaurant_id` | Ja | Het Zenchef-restaurant-ID om te controleren. |

```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" }'
```

**Antwoord** (`200 OK`):

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

`exists: false` betekent dat geen enkel Zenchef-restaurant dat ID heeft — er is niets meer te doen. Snelheidslimiet van 10 controles per 5 minuten per account; overschrijding resulteert in `429`.

**Stap 2 — De naam van het restaurant verifiëren**

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

| Veld | Verplicht | Beschrijving |
|---|---|---|
| `restaurant_id` | Ja | Het Zenchef-restaurant-ID uit stap 1. |
| `user_input_name` | Ja | De naam die de accounthouder heeft ingetypt — vergeleken met de echte naam van het restaurant op Zenchef (ongevoelig voor hoofdletters/spaties). |

```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" }'
```

**Antwoord** (`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` betekent dat de naam niet overeenkwam — `restaurantDetails` wordt weggelaten, vraag de accounthouder om het opnieuw te proberen. Snelheidslimiet van 3 pogingen per 5 minuten (strenger dan de bestaancontrole, aangezien dit de daadwerkelijke verificatiestap is). Een `restaurant_id` die niet langer wordt opgelost op Zenchef resulteert in `404`.

**Stap 3 — Het restaurant opslaan**

`POST /appointments/zenchef-restaurants`

| Veld | Verplicht | Beschrijving |
|---|---|---|
| `restaurant_id` | Ja | 1–64 tekens, letters/cijfers/underscore/koppelteken. |
| `restaurant_name` | Ja | De geverifieerde restaurantnaam uit stap 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" }'
```

**Antwoord** (`201 Created`):

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

**Een opgeslagen Zenchef-restaurant bijwerken**

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

| Veld | Verplicht | Beschrijving |
|---|---|---|
| `restaurant_name` | Nee | Nieuwe weergavenaam. |
| `is_active` | Nee | Stel `false` in om te voorkomen dat de bot bij dit restaurant boekt zonder het te verwijderen. |

```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 }'
```

**Antwoord** (`200 OK`): dezelfde vorm als het antwoord bij opslaan hierboven.

**Een Zenchef-restaurant verwijderen**

`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"
```

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

Een `restaurantId` die momenteel niet aan het account is gekoppeld, geeft `404` terug bij een update of verwijdering.

### Formitable

Formitable heeft niet de tweestaps-naamcontrole nodig die Zenchef wel vereist — de restaurant-ID's zijn al per bedrijf gescopeerd, dus één verificatie-aanroep is voldoende. Het heeft ook een details-opzoekfunctie die wordt gebruikt om de website-URL van het restaurant tijdens de configuratie in de cache op te slaan.

**Een restaurant-ID verifiëren**

`POST /appointments/formitable-restaurants/verify`

| Veld | Verplicht | Beschrijving |
|---|---|---|
| `restaurant_id` | Ja | Het Formitable restaurant-ID. |
| `language` | Nee | Taaltag voor het probe-verzoek. Standaard ingesteld op `"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" }'
```

**Antwoord** (`200 OK`):

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

Een `restaurant_id` die Formitable niet herkent, retourneert `404`. Snelheidslimiet van 10 pogingen per 5 minuten per account.

**Restaurantdetails ophalen**

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

Haalt het openbare profiel van het restaurant op uit Formitable, inclusief de website — wordt gebruikt om de website-URL in de cache op te slaan tijdens het instellen van het restaurant. `language` is een optionele queryparameter, die standaard op `"en"` staat.

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

**Antwoord** (`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"
  }
}
```

**Sla het restaurant op**

`POST /appointments/formitable-restaurants`

| Veld | Verplicht | Beschrijving |
|---|---|---|
| `restaurant_id` | Ja | 1–64 tekens, letters/cijfers/underscore/koppelteken. |
| `restaurant_name` | Ja | Weergavenaam. |
| `language` | Ja | ISO-taallabel, bijv. `"en"` of `"en-GB"`. |
| `website_url` | Nee | De website van het restaurant, uit de bovenstaande details-opzoeking. Moet `http(s)://` zijn. |

```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"
  }'
```

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

**Update een opgeslagen Formitable-restaurant**

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

| Veld | Verplicht | Beschrijving |
|---|---|---|
| `restaurant_name` | Nee | Nieuwe weergavenaam. |
| `language` | Nee | Nieuw ISO-taallabel. |
| `is_active` | Nee | Stel `false` in om te voorkomen dat de bot boekingen maakt voor dit restaurant zonder het te verwijderen. |
| `website_url` | Nee | Nieuwe 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 }'
```

**Antwoord** (`200 OK`): dezelfde vorm als het antwoord bij opslaan hierboven.

**Verwijder een Formitable-restaurant**

`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"
```

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

Een `restaurantId` die momenteel niet aan het account is gekoppeld, geeft `404` terug bij een update of verwijdering.

### OpenTable

OpenTable-restaurants worden bereikt via de OpenTable-partnergegevens van het platform, en OpenTable staat die gegevens alleen toe om restaurants te zien die het platform hebben gekoppeld in de Integrations Marketplace van OpenTable. Dus, net als bij Formitable, is één verificatie-aanroep voldoende: een bereikbaar Restaurant ID (het numerieke "RID") bewijst zowel dat het restaurant bestaat als dat het de integratie heeft gekoppeld. Totdat de OpenTable-partnervermelding is ingeschakeld op het platform, beantwoordt de verify-aanroep met `503`.

**Een OpenTable-restaurant verifiëren**

`POST /appointments/opentable-restaurants/verify`

| Veld | Verplicht | Beschrijving |
| --- | --- | --- |
| `restaurant_id` | Ja | Het OpenTable Restaurant ID (RID), een nummer zoals `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" }'
```

**Antwoord** (`200 OK`):

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

`verified: false` betekent dat geen enkel OpenTable-restaurant dat ID heeft. Een `403` betekent dat het restaurant bestaat, maar de integratie van het platform nog niet heeft gekoppeld in OpenTable. Snelheidslimiet van 10 pogingen per 5 minuten per account.

**Een OpenTable-restaurant toevoegen**

`POST /appointments/opentable-restaurants`

| Veld | Verplicht | Beschrijving |
| --- | --- | --- |
| `restaurant_id` | Ja | Het geverifieerde Restaurant ID. |
| `restaurant_name` | Ja | Weergavenaam (een label; ook hoe de AI het restaurant noemt). |
| `website_url` | Nee | Een http(s) URL die aan gasten wordt getoond wanneer de AI ze doorverwijst naar het restaurant. |

```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" }'
```

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

**Een opgeslagen OpenTable-restaurant bijwerken**

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

Stuur een van `restaurant_name`, `is_active` (pauzeer met `false`) of `website_url` (een lege string wist het); weggelaten velden blijven ongewijzigd.

```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 }'
```

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

**Een OpenTable-restaurant verwijderen**

`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"
```

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

Een `restaurantId` die momenteel niet aan het account is gekoppeld, retourneert `404` bij een update.

### TheFork

TheFork-restaurants worden bereikt via de TheFork-partnergegevens van het platform, en TheFork staat deze gegevens alleen toe om restaurants te zien waarvoor de partner van het platform is ingeschakeld in hun TheFork-account. Dus, net als bij Formitable en OpenTable, is één verificatieaanroep voldoende: een bereikbaar Restaurant-ID bewijst zowel dat het restaurant bestaat als dat de partner erop is ingeschakeld. Het ID is de UUID die TheFork aan het restaurant geeft in TheFork Manager, verzonden als een string. Totdat TheFork het platform als partner heeft goedgekeurd en de inloggegevens heeft verstrekt, beantwoordt de verificatieaanroep met `503` — zie [TheFork](../integrations/thefork.md) voor wat dat vandaag de dag betekent.

**Een TheFork-restaurant verifiëren**

`POST /appointments/thefork-restaurants/verify`

| Veld | Vereist | Beschrijving |
| --- | --- | --- |
| `restaurant_id` | Ja | Het TheFork Restaurant-ID, een UUID zoals `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" }'
```

**Antwoord** (`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` zijn de groepsgroottes die het restaurant de komende 30 dagen online accepteert. Een `404` of `403` betekent dat TheFork ons geen restaurant met dat ID wilde geven — of het ID is onjuist, of de partner van het platform is nog niet ingeschakeld voor dat restaurant; een `400` betekent dat het ID geen UUID is. Snelheidslimiet van 10 pogingen per 5 minuten per account.

**Een TheFork-restaurant toevoegen**

`POST /appointments/thefork-restaurants`

| Veld | Vereist | Beschrijving |
| --- | --- | --- |
| `restaurant_id` | Ja | Het geverifieerde Restaurant-ID (UUID). |
| `restaurant_name` | Ja | Weergavenaam (een label; ook hoe de AI het restaurant noemt). |
| `website_url` | Nee | Een http(s) URL die aan gasten wordt getoond wanneer de AI hen doorverwijst naar het restaurant. |

```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" }'
```

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

**Een opgeslagen TheFork-restaurant bijwerken**

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

Stuur een van `restaurant_name`, `is_active` (pauzeer met `false`) of `website_url` (een lege string wist het); weggelaten velden blijven ongewijzigd.

```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 }'
```

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

**Een TheFork-restaurant verwijderen**

`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"
```

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

Een `restaurantId` die momenteel niet aan het account is gekoppeld, geeft `404` terug bij een update of verwijdering.

### Trafft

Trafft wordt één keer gekoppeld voor het hele account, niet per locatie: één bedrijfsadres plus de API-inloggegevens uit het Trafft-beheerderspaneel (**Features & Integrations → API & Connectors**, onderdeel van het Business-abonnement van Trafft). De 'connect'-aanroep controleert die inloggegevens bij Trafft voordat er iets wordt opgeslagen, dus een verkeerd adres, verkeerde inloggegevens of een abonnement zonder API-toegang faalt hier in plaats van tijdens een klantgesprek. Het client secret wordt versleuteld opgeslagen en wordt nooit door een endpoint geretourneerd.

Alle drie de schrijfaanroepen (`POST`, `PUT`, `DELETE`) vereisen de **edit**-toestemming voor Integraties; de status `GET` vereist **view**-toestemming voor Integraties.

**Trafft koppelen**

`POST /appointments/trafft/connect`

| Veld | Verplicht | Beschrijving |
|---|---|---|
| `subdomain` | Ja | Het bedrijfsadres — het deel vóór `.admin.trafft.com` in de URL waar je inlogt, bijv. `acme`. Een volledig adres wordt geaccepteerd en teruggebracht tot dezelfde waarde. |
| `client_id` | Ja | Client ID van de API & Connectors-pagina van Trafft. |
| `client_secret` | Ja | Client Secret van dezelfde pagina. Wordt versleuteld opgeslagen, nooit geretourneerd. |
| `company_name` | Nee | Een label voor je eigen lijst. |

```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"
  }'
```

**Antwoord** (`200 OK`):

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

De aantallen komen terug van Trafft tijdens de controle — dit is de snelste manier om te bevestigen dat de inloggegevens verwijzen naar het account dat je bedoelde.

**De verbindingsstatus ophalen**

`GET /appointments/trafft`

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

**Antwoord** (`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` betekent dat er nog niets is ingesteld. Het client secret wordt nooit in dit antwoord opgenomen.

**De koppeling bijwerken**

`PUT /appointments/trafft`

| Veld | Vereist | Beschrijving |
|---|---|---|
| `is_active` | Nee | Stel `false` in op pauze — de AI stopt met boeken in Trafft, de verbinding blijft behouden. `true` hervat deze. |
| `company_name` | Nee | Nieuw 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 }'
```

**Antwoord** (`200 OK`): hetzelfde verbindings-object als de `GET` hierboven.

**Trafft verbreken**

`DELETE /appointments/trafft`

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

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

Het verbreken van de verbinding verwijdert alleen de opgeslagen inloggegevens. Afspraken die al in Trafft staan, blijven onaangetast.

> **Foutstructuur op alle Zenchef/Formitable/OpenTable/TheFork-eindpunten:** in tegenstelling tot de rest van deze pagina, bevatten fouten hier de status twee keer — één keer als de HTTP-status en één keer als `error_code` in de body — bijvoorbeeld `{ "success": false, "error": "Restaurant not found", "error_code": 404 }`. Handel dit op dezelfde manier af als elke andere fout: controleer `success`, lees `error` voor het bericht.

---

## Fouten in de Appointments API

Appointment-endpoints retourneren de standaardfouten-envelop:

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

| Status | Wanneer dit gebeurt op een appointment-endpoint |
|---|---|
| `400` | Een verplicht veld ontbreekt of is ongeldig — bijvoorbeeld een onjuiste `start_time`, een `end_time` die niet na `start_time` ligt, een ongeldige filtercombinatie, geen velden om bij te werken, of een reeds geannuleerde afspraak. |
| `404` | De afspraak, het contact of het type evenement is niet gevonden. |
| `409` | Het gevraagde tijdslot is al bezet (boekingsconflict). |

De gedeelde codes die elk endpoint kan retourneren — `401`, `403` (uw abonnement bevat geen API-toegang), `429` (snelheidslimiet) en `500` — worden vermeld met richtlijnen voor opnieuw proberen in [Fouten & Paginering](errors-and-pagination.md).

---

::: master-only
## Gebruik je eigen Google OAuth-client (toestemmingsscherm voor Agenda)

Wanneer een account Google Agenda koppelt, noemt het inlogvenster van Google het project van de OAuth-client — standaard dat van het platform. Een bureau kan zijn eigen Google OAuth 2.0-client registreren op het bureau-account; vanaf dat moment verloopt de agendakoppeling voor dat account en elk sub-account daaronder via die client, zodat het toestemmingsscherm de naam en het logo van het bureau toont. Er verandert niets anders: het koppelingsproces, de tweerichtingssynchronisatie en de bovenstaande afspraken-eindpunten werken precies zoals voorheen.

> **Alleen Google Agenda.** De Gmail-mailbox OAuth voor het e-mailkanaal blijft ongewijzigd.

### Wat je klant eerst nodig heeft

1. **Een OAuth 2.0-client** van het type Webapplicatie in je Google Cloud-project, met de **Google Calendar API** ingeschakeld voor dat project.
2. **Elke `redirect_uris`-vermelding** (geretourneerd door de onderstaande eindpunten) toegevoegd onder de geautoriseerde omleidings-URI's van de client. De eerste vermelding is je geverifieerde `api.`-domein wanneer je er een hebt — Google verifieert alleen een merk waarvan de omleiding op een domein staat dat je bezit — gevolgd door de neutrale host van het platform als fallback die tot die tijd wordt gebruikt.
3. **Het toestemmingsscherm** met je merk, je domein onder Geautoriseerde domeinen en de twee gedeclareerde Agenda-scopes (`scopes` in het antwoord). Totdat de app is gepubliceerd en geverifieerd door Google, zien gebruikers een waarschuwing voor een niet-geverifieerde app en is de client beperkt tot 100 gebruikers.

### Je client opslaan

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

| Veld | Verplicht | Beschrijving |
|---|---|---|
| `client_id` | Ja | De OAuth 2.0-client-ID, eindigend op `.apps.googleusercontent.com`. |
| `client_secret` | Ja | Het clientgeheim. Wordt gecontroleerd bij Google voordat het wordt opgeslagen en vervolgens versleuteld. Wordt nooit door een eindpunt geretourneerd. |

```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"
  }'
```

**Antwoord**

```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": ["…"]
}
```

Een onjuist geheim of een onbekende client-ID wordt geweigerd met `400` en de eigen reden van Google in `error`, en er wordt niets opgeslagen.

### Lezen of verwijderen

`GET /account-config/google-oauth-client` retourneert op elk gewenst moment hetzelfde overzicht — `configured: false` plus de `redirect_uris` en `scopes` voordat er iets wordt opgeslagen, zodat u eerst de Google-kant kunt instellen. `DELETE /account-config/google-oauth-client` verwijdert de client: nieuwe verbindingen keren terug naar de platform-client en agenda's die via de verwijderde client waren verbonden, moeten opnieuw worden verbonden, omdat alleen de client die een verbinding heeft uitgegeven deze kan vernieuwen.

Teamleden hebben **Integraties: bekijken** nodig voor `GET` en **Integraties: bewerken** voor `PUT` / `DELETE`.
:::

---

## Volgende stappen

- [Contacten](contacts.md) — maak contacten aan en zoek ze op voor wie u boekt.
- [Berichten & Gesprekken](messages.md) — stuur een contactpersoon een bevestiging of herinnering.
- [Webhooks](webhooks.md) — ontvang een melding wanneer afspraken wijzigen.
