
# Aftaler

Appointments API'en lader dig booke aftaler for dine kontakter på dine begivenhedstyper, og derefter hente, liste, opdatere, annullere eller slette dem. Den besvarer også det spørgsmål, der kommer først i de fleste booking-flows — hvilke tidspunkter er faktisk ledige — og dækker kalendersiden: visning af de Google-kalendere, du har forbundet, og import af begivenheder, der allerede findes i dem. Når en Google Kalender-forbindelse er aktiv, oprettes den matchende kalenderbegivenhed og holdes automatisk synkroniseret i baggrunden. Restauranter, der bruger Zenchef, Formitable, OpenTable eller TheFork til deres egne reservationssystemer, kan også verificeres og forbindes her, så AI-agenten booker rigtige borde i stedet for interne aftaler — og virksomheder, der kører deres tidsplan i Trafft, kan forbinde på samme måde.

Alle stier på denne side er relative til basis-URL'en `https://api.dmchamp.com/v1`. Hver anmodning kræver din API-nøgle — se [Godkendelse](authentication.md) for den fulde liste over måder at sende den på. Eksemplerne nedenfor bruger `X-API-Key`-headeren, hvor et cURL-eksempel også viser `?apiKey=`-forespørgselsformen.

> **Begivenheder vs. aftaler:** En *begivenhedstype* er en definition af en bookbar plads (typen af møde, dets varighed, dets lokaler). En *aftale* er én booket forekomst af en begivenhedstype for en specifik kontakt. Du booker en aftale ved at referere til kontakten og begivenhedstypen.

---

## Aftaleobjektet

Hvert slutpunkt, der returnerer en aftale, bruger samme form:

| Felt | Beskrivelse |
|---|---|
| `id` | Unikt ID for aftalen. |
| `contact_id` | ID for den kontakt, aftalen er booket med. |
| `event_id` | ID for den begivenhedstype, aftalen blev booket på. |
| `status` | `Confirmed` eller `Canceled`. |
| `start_time` | Starttidspunkt for aftalen, ISO 8601 i UTC. |
| `end_time` | Sluttidspunkt for aftalen, ISO 8601 i UTC. |
| `created_at` | Hvornår aftalen blev oprettet. |
| `last_modified_at` | Hvornår aftalen sidst blev ændret. |
| `room_name` | Lokale eller ressource, aftalen er booket i, når begivenhedstypen bruger lokaler. |
| `description` | Fritekstbeskrivelse af aftalen. |
| `summary` | Kort resumé eller titel. |
| `cancelation_reason` | Årsag angivet ved annullering af aftalen, hvis nogen. |
| `google_calendar_event_id` | ID for den linkede Google Kalender-begivenhed. Sættes når kalendersynkroniseringen er fuldført; `null` når ingen kalender er forbundet, eller mens synkroniseringen stadig er i gang. |
| `calendar_synced` | `true` når aftalen er linket til en kalenderbegivenhed. |
| `imported` | `true` når aftalen er importeret fra en ekstern kalender i stedet for at være booket direkte. |
| `is_recurring` | `true` når aftalen er en del af en tilbagevendende serie. |
| `recurrence_frequency` | Hvor ofte aftalen gentages, når den er tilbagevendende. |
| `recurring_event_id` | ID for den tilbagevendende serie, som denne aftale tilhører. |
| `recurring_interval` | Interval mellem gentagelser, når den er tilbagevendende. |
| `recurring_sequence` | Placering af denne aftale i dens tilbagevendende serie. |
| `end_after_x_occurrences` | Antal forekomster hvorefter den tilbagevendende serie slutter. |
| `booking_provider` | Kildesystem som bookingen kom fra, når den er booket gennem en forbundet reservationsudbyder. |

> **Om kalendersynkronisering:** Lige efter du booker eller ændrer en aftale, kan `google_calendar_event_id` stadig være `null` og `calendar_synced` kan være `false`, fordi synkroniseringen kører i baggrunden et øjeblik senere. Hent aftalen igen kort efter for at se de udfyldte kalenderfelter.

---

## Find ledige tider

`GET /appointments/available-slots`

Returnerer de tider, der er reelt ledige på en begivenhedstype mellem to tidspunkter. Dette er normalt det **første** kald i et booking-flow: vis disse tider, lad personen vælge en, og post derefter det valgte tidspunkt til [Book en aftale](#book-an-appointment).

Svaret tager allerede højde for begivenhedstypens egne åbningstider og varighed, dens lokaler, aftaler du allerede har booket på den, og alt, hvad der er blokeret i de forbundne Google-kalendere — så en tid, der returneres her, er en, du kan booke.

| Forespørgselsparameter | Påkrævet | Beskrivelse |
|---|---|---|
| `event_id` | Ja | Begivenhedstypen, der skal tjekkes. Skal tilhøre din konto. |
| `start_time` | Ja | Start på vinduet, du ønsker tider for, ISO 8601 dato-tid. |
| `end_time` | Ja | Slut på vinduet, ISO 8601 dato-tid. Hele slutdagen er inkluderet. |

Resultaterne kommer tilbage grupperet efter dag — og når begivenhedstypen bruger lokaler, én gruppe pr. lokale pr. dag:

| Felt | Beskrivelse |
|---|---|
| `date` | Dagen gruppen dækker, skrevet `DD/MM/YYYY`. |
| `day` | Ugedagsnavn med små bogstaver, for eksempel `monday`. |
| `room_name` | Lokalet eller ressourcen, denne gruppe tilhører, når begivenhedstypen bruger lokaler. |
| `available_slots` | De bookbare blokke på den dag, tidligste først. |

Hver post i `available_slots` har:

| Felt | Beskrivelse |
|---|---|
| `start_time` | Blokstart som `HH:mm`. |
| `end_time` | Blokslut som `HH:mm`. |
| `available` | `true` — kun ledig tid returneres. |
| `spots_left` | Hvor mange bookinger der stadig er plads til i denne blok. Kun til stede på begivenhedstyper, der tager mere end én booking pr. tid. |

> **Tider er lokale for begivenhedstypen, ikke UTC.** `date`, `start_time` og `end_time` er vægursværdier i begivenhedstypens egen tidszone (dens overstyring, eller din kontos tidszone, når den ikke har nogen). [Book en aftale](#book-an-appointment) forventer et ISO 8601 UTC-øjeblik, så konverter den tid, du valgte, før du poster den.

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

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

En dag uden ledige tider vises slet ikke. Manglende `event_id`, `start_time` eller `end_time` returnerer `400`; en begivenhedstype, der ikke er på din konto, returnerer `404`.

---

## Book en aftale

`POST /appointments`

Booker en ny aftale for en kontakt på en af dine begivenhedstyper. Sluttidspunktet beregnes automatisk ud fra begivenhedstypens varighed.

Bookingen bliver tjekket for konflikter: Hvis det ønskede tidspunkt overlapper en eksisterende bekræftet aftale på samme begivenhedstype, fejler anmodningen med en `409`, og intet oprettes.

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `contact_id` | Ja | ID for kontakten, der skal bookes til. Skal tilhøre din konto. |
| `event_id` | Ja | ID for begivenhedstypen, der skal bookes på. Skal tilhøre din konto. |
| `start_time` | Ja | Ønsket starttidspunkt som en ISO 8601 dato-tid. |
| `room_name` | Nej | Lokale- eller ressourcenavn, når begivenhedstypen bruger lokaler. |

**cURL** (ved brug af `?apiKey=` forespørgselsformen)

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

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

---

## Hent en aftale

`GET /appointments/{appointmentId}`

Returnerer en enkelt aftale via dens ID, inklusive dens kalendersynkroniseringstilstand.

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

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

---

## List aftaler

`GET /appointments`

Lister aftaler for din konto, nyeste først, med markør-baseret paginering.

| Forespørgselsparameter | Påkrævet | Beskrivelse |
|---|---|---|
| `contact_id` | Nej | Returner kun aftaler for denne kontakt. Kontakt-filtrerede lister inkluderer **kun bekræftede aftaler**. |
| `date` | Nej | Returner kun aftaler på denne kalenderdag (`YYYY-MM-DD`). **Kræver `contact_id`.** |
| `status` | Nej | Filtrer efter `Confirmed` eller `Canceled`. Kun tilgængelig **uden** `contact_id`. |
| `limit` | Nej | Sidestørrelse, et heltal mellem 1 og 100. Standard `50`. |
| `cursor` | Nej | `next_cursor`-værdien fra et tidligere svar. |

Et par regler at huske på:

- **Uden filtre** får du hver aftale på kontoen, side for side.
- **Efter kontakt** — sæt `contact_id` for at se én kontakts bekræftede aftaler. Du kan indsnævre dette til en enkelt dag ved også at sende `date`.
- **Efter status** — sæt `status` (uden `contact_id`) for kun at liste `Confirmed` eller kun `Canceled` aftaler på tværs af kontoen.
- `date`-filteret uden `contact_id`, eller `status=Canceled` sammen med `contact_id`, returnerer en `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"])
```

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

For at bladre gennem resultaterne skal du sende `next_cursor` fra ét svar som `cursor` i den næste anmodning. Fortsæt indtil `next_cursor` er `null`. Se [Fejl & Paginering](errors-and-pagination.md) for det fælles pagineringsmønster.

---

## Opdater en aftale

`PUT /appointments/{appointmentId}`

Omplanlæg en aftale eller skift dens detaljer. Send kun de felter, du ønsker at ændre — mindst ét er påkrævet. Den kombinerede start og slut skal forblive i kronologisk rækkefølge (`end_time` skal være efter `start_time`). Ændringer synkroniseres automatisk til den linkede kalenderbegivenhed.

| Felt | Beskrivelse |
|---|---|
| `start_time` | Ny start, ISO 8601 dato-tid. |
| `end_time` | Ny slutning, ISO 8601 dato-tid. Skal være efter starttidspunktet. |
| `room_name` | Nyt lokale- eller ressourcenavn. |
| `description` | Ny beskrivelse, eller `null` for at rydde den. |
| `summary` | Nyt resumé, eller `null` for at rydde det. |

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

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

---

## Annuller en aftale

`POST /appointments/{appointmentId}/cancel`

Annullerer en bekræftet aftale, eventuelt med angivelse af en årsag. Aftalen forbliver på din konto med status `Canceled`, og den tilknyttede kalenderbegivenhed fjernes automatisk i baggrunden. Annullering af en allerede annulleret aftale returnerer en `400`.

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `cancellation_reason` | Nej | Årsag til annulleringen, gemmes på aftalen. |

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

**Svar** (`200 OK`):

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

---

## Slet en aftale

`DELETE /appointments/{appointmentId}`

Sletter permanent en aftale og dens referencer. Hvis du kun ønsker at aflyse bookingen, men beholde posten, skal du i stedet bruge [annuller](#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"])
```

**Svar** (`200 OK`):

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

---

## List dine forbundne Google-kalendere

`GET /appointments/google-calendars`

Returnerer de Google-kalendere, der er tilgængelige på denne konto, direkte fra Google — nyttigt til at vise kontohaveren en vælger af, hvilken kalender der skal importeres fra nedenfor, eller blot for at bekræfte, at forbindelsen er aktiv.

Dette virker kun, når kontoen har forbundet Google Kalender (Indstillinger → Integrationer) med mindst læseadgang. Hvis den ikke har, eller hvis den givne adgang ikke længere inkluderer scope for kalenderlæsning, får du en `400`, der beder dig om at (gen)forbinde den.

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

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

Hver post er Googles egen [`CalendarListEntry`](https://developers.google.com/calendar/api/v3/reference/calendarList)-form, så feltnavne følger Googles `camelCase`, ikke denne API's sædvanlige `snake_case` — det er Googles data, der sendes igennem som de er, ikke vores. En manglende eller tilbagekaldt forbindelse returnerer `400` med en fejl, der forklarer, at Google Kalender skal (gen)forbindes.

---

## Importér begivenheder fra en Google Kalender

`POST /appointments/import-calendar-events`

Henter de begivenheder, der allerede ligger i en kampagnes eller AI-agents forbundne Google Kalender(e), og omdanner dem til aftaler — nyttigt første gang du forbinder en kalender, der allerede har bookinger. Dette kan tage et stykke tid (hver begivenhed gennemgår ekstraktion for at finde ud af, hvem den er til), så den kører aldrig inline: anmodningen sætter et baggrundsjob i kø og giver dig en `job_id` tilbage, som du kan polle.

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `campaign_id` | En af disse to | Kampagnen, hvis forbundne kalender(e) der skal importeres fra. |
| `agent_id` | En af disse to | AI-agenten, hvis forbundne kalender(e) der skal importeres fra. |
| `identifier` | Ja | `"EMAIL"` eller `"PHONE_NUMBER"` — hvilken kontaktinformation der skal udtrækkes fra hver kalenderbegivenhed for at matche eller oprette den kontakt, den tilhører. |

Send præcis én af `campaign_id` / `agent_id`, aldrig begge og aldrig ingen af dem — enhver anden kombination returnerer en `400`. Den, du sender, skal tilhøre din konto, ellers får du en `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"])
```

**Svar** (`202 Accepted`):

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

`campaign_id` og `agent_id` ekkoer den, du sendte; den anden er altid `null`.

### Polling af importjobbet

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

**Svar** (`200 OK`):

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

| `status` | Betydning |
|---|---|
| `queued` | Ikke hentet endnu. Fortsæt med at polle. |
| `processing` | Importen kører. Fortsæt med at polle. |
| `completed` | Færdig — `message` indeholder et kort, menneskeligt læsbart resumé. |
| `failed` | Noget gik galt — `error` indeholder årsagen. |

`GET` på en `jobId`, der ikke eksisterer (eller tilhører en anden konto), returnerer `404`.

---

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

## Eksterne booking-integrationer (Zenchef / Formitable / OpenTable / TheFork / Trafft)

Zenchef og Formitable er restaurant-reservationssystemer, som din AI-agent kan booke rigtige borde igennem; [Trafft](#trafft) er en planlægningsplatform for virksomheder, der arbejder med aftaler, og som forbindes én gang pr. konto i stedet for pr. restaurant. De to restaurantplatforme har hver en **offentlig, uautentificeret booking-widget** (`https://api.dmchamp.com/v1/zenchef-widget/...` og `https://api.dmchamp.com/v1/formitable-widget/...`), der vises i chatten for gæsten — disse widget-ruter er almindelige HTML-sider beregnet til at blive åbnet i en browser, ikke JSON API-slutpunkter, så de er ikke dokumenteret her. Det, der følger, er slutpunkterne for kontostyring: verificering af, at et restaurant-ID tilhører kontohaveren, samt tilføjelse, opdatering eller fjernelse af det.

### Zenchef

Forbindelse af en Zenchef-restaurant er en to-trins bekræftelse, så kontohaveren beviser, at de rent faktisk driver restauranten, før den bliver forbundet til botten: tjek først, at ID'et eksisterer (uden at afsløre navnet), og få dem derefter til selv at indtaste restaurantens navn og bekræft, at det stemmer overens.

**Trin 1 — Tjek om et restaurant-ID eksisterer**

`POST /appointments/zenchef-restaurants/check`

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `restaurant_id` | Ja | Det Zenchef restaurant-ID, der skal tjekkes. |

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

**Svar** (`200 OK`):

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

`exists: false` betyder, at ingen Zenchef-restaurant har det ID — der er ikke mere at gøre. Hastighedsbegrænset til 10 tjek pr. 5 minutter pr. konto; overskridelse returnerer `429`.

**Trin 2 — Bekræft restaurantens navn**

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

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `restaurant_id` | Ja | Zenchef restaurant-ID'et fra trin 1. |
| `user_input_name` | Ja | Navnet, som kontohaveren indtastede — sammenlignes med restaurantens rigtige navn på Zenchef (uafhængigt af store/små bogstaver og mellemrum). |

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

**Svar** (`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` betyder, at navnet ikke stemte overens — `restaurantDetails` udelades, bed kontohaveren om at prøve igen. Hastighedsbegrænset til 3 forsøg pr. 5 minutter (strammere end eksistenstjekket, da dette er selve bevisførelsen). Et `restaurant_id`, der ikke længere kan findes på Zenchef, returnerer `404`.

**Trin 3 — Gem restauranten**

`POST /appointments/zenchef-restaurants`

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `restaurant_id` | Ja | 1–64 tegn, bogstaver/tal/understregning/bindestreg. |
| `restaurant_name` | Ja | Det bekræftede restaurantnavn fra trin 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" }'
```

**Svar** (`201 Created`):

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

**Opdater en gemt Zenchef-restaurant**

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

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `restaurant_name` | Nej | Nyt visningsnavn. |
| `is_active` | Nej | Indstil `false` for at forhindre botten i at booke hos denne restaurant uden at fjerne den. |

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

**Svar** (`200 OK`): samme format som gem-svaret ovenfor.

**Fjern en Zenchef-restaurant**

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

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

En `restaurantId`, der ikke i øjeblikket er på kontoen, returnerer `404` ved opdatering eller sletning.

### Formitable

Formitable behøver ikke den to-trins navnebekræftelse, som Zenchef gør — dens restaurant-id'er er allerede afgrænset pr. virksomhed, så et enkelt bekræftelsesopkald er nok. Den har også et opslag af detaljer, der bruges til at cache restaurantens websteds-URL under opsætningen.

**Bekræft et restaurant-id**

`POST /appointments/formitable-restaurants/verify`

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `restaurant_id` | Ja | Formitable restaurant-id'et. |
| `language` | Nej | Sprogkode for probe-anmodningen. Standard er `"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" }'
```

**Svar** (`200 OK`):

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

Et `restaurant_id`, som Formitable ikke genkender, returnerer `404`. Hastighedsbegrænset til 10 forsøg pr. 5 minutter pr. konto.

**Hent restaurantdetaljer**

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

Henter restaurantens offentlige profil fra Formitable, inklusive dens websted — bruges til at cache websteds-URL'en, mens restauranten opsættes. `language` er en valgfri forespørgselsparameter, der som standard er `"en"`.

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

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

**Gem restauranten**

`POST /appointments/formitable-restaurants`

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `restaurant_id` | Ja | 1–64 tegn, bogstaver/tal/understregning/bindestreg. |
| `restaurant_name` | Ja | Visningsnavn. |
| `language` | Ja | ISO-sprogkode, f.eks. `"en"` eller `"en-GB"`. |
| `website_url` | Nej | Restaurantens hjemmeside fra opslaget af detaljer ovenfor. Skal være `http(s)://`. |

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

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

**Opdater en gemt Formitable-restaurant**

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

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `restaurant_name` | Nej | Nyt visningsnavn. |
| `language` | Nej | Ny ISO-sprogkode. |
| `is_active` | Nej | Sæt `false` for at forhindre botten i at booke hos denne restaurant uden at fjerne den. |
| `website_url` | Nej | Ny URL til hjemmeside. |

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

**Svar** (`200 OK`): samme format som gem-svaret ovenfor.

**Fjern en 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"
```

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

En `restaurantId`, der ikke i øjeblikket er på kontoen, returnerer `404` ved opdatering eller sletning.

### OpenTable

OpenTable-restauranter nås gennem platformens OpenTable-partnerlegitimationsoplysninger, og OpenTable lader kun disse legitimationsoplysninger se restauranter, der har forbundet platformens registrering inde i OpenTable's Integrations Marketplace. Så ligesom med Formitable er ét bekræftelsesopkald nok: et tilgængeligt Restaurant-ID (det numeriske "RID") beviser både, at restauranten eksisterer, og at den har forbundet integrationen. Indtil OpenTable-partnerregistreringen er aktiveret på platformen, svarer bekræftelsesopkaldet med `503`.

**Verificer en OpenTable-restaurant**

`POST /appointments/opentable-restaurants/verify`

| Felt | Påkrævet | Beskrivelse |
| --- | --- | --- |
| `restaurant_id` | Ja | OpenTable Restaurant-ID (RID), et tal såsom `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" }'
```

**Svar** (`200 OK`):

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

`verified: false` betyder, at ingen OpenTable-restaurant har det ID. En `403` betyder, at restauranten eksisterer, men endnu ikke har forbundet platformens integration inde i OpenTable. Hastighedsbegrænset til 10 forsøg pr. 5 minutter pr. konto.

**Tilføj en OpenTable-restaurant**

`POST /appointments/opentable-restaurants`

| Felt | Påkrævet | Beskrivelse |
| --- | --- | --- |
| `restaurant_id` | Ja | Det verificerede Restaurant-ID. |
| `restaurant_name` | Ja | Visningsnavn (en etiket; også det AI'en kalder restauranten). |
| `website_url` | Nej | En http(s) URL, der vises til gæster, når AI'en sender dem videre til restauranten. |

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

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

**Opdater en gemt OpenTable-restaurant**

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

Send enhver af `restaurant_name`, `is_active` (pause med `false`) eller `website_url` (tom streng rydder den); udeladte felter forbliver uændrede.

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

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

**Fjern en OpenTable-restaurant**

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

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

En `restaurantId`, der ikke i øjeblikket er på kontoen, returnerer `404` ved opdatering.

### TheFork

TheFork-restauranter nås gennem platformens TheFork-partnerlegitimationsoplysninger, og TheFork lader kun disse legitimationsoplysninger se restauranter, der har platformens partner aktiveret på deres TheFork-konto. Så ligesom med Formitable og OpenTable er ét bekræftelsesopkald nok: et tilgængeligt Restaurant-ID beviser både, at restauranten eksisterer, og at partneren er aktiveret på den. ID'et er den UUID, som TheFork giver restauranten i TheFork Manager, sendt som en streng. Indtil TheFork har godkendt platformen som partner og udstedt legitimationsoplysningerne, svarer bekræftelsesopkaldet `503` — se [TheFork](../integrations/thefork.md) for hvad det betyder i dag.

**Bekræft en TheFork-restaurant**

`POST /appointments/thefork-restaurants/verify`

| Felt | Påkrævet | Beskrivelse |
| --- | --- | --- |
| `restaurant_id` | Ja | TheFork Restaurant-ID'et, en UUID såsom `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" }'
```

**Svar** (`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` er de gruppestørrelser, som restauranten tager imod online over de næste 30 dage. En `404` eller `403` betyder, at TheFork ikke ville give os en restaurant med det ID — enten er ID'et forkert, eller også er platformens partner endnu ikke aktiveret på den restaurant; en `400` betyder, at ID'et ikke er en UUID. Hastighedsbegrænset til 10 forsøg pr. 5 minutter pr. konto.

**Tilføj en TheFork-restaurant**

`POST /appointments/thefork-restaurants`

| Felt | Påkrævet | Beskrivelse |
| --- | --- | --- |
| `restaurant_id` | Ja | Det verificerede Restaurant-ID (UUID). |
| `restaurant_name` | Ja | Visningsnavn (en etiket; også det AI'en kalder restauranten). |
| `website_url` | Nej | En http(s) URL, der vises for gæster, når AI'en sender dem videre til restauranten. |

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

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

**Opdater en gemt TheFork-restaurant**

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

Send enhver af `restaurant_name`, `is_active` (pause med `false`) eller `website_url` (tom streng rydder den); udeladte felter forbliver uændrede.

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

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

**Fjern en TheFork-restaurant**

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

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

En `restaurantId`, der ikke i øjeblikket er på kontoen, returnerer `404` ved opdatering eller sletning.

### Trafft

Trafft forbindes én gang for hele kontoen, ikke pr. lokation: én virksomhedsadresse plus API-legitimationsoplysninger fra Trafft-administrationspanelet (**Features & Integrations → API & Connectors**, en del af Traffts Business-plan). Forbindelsesopkaldet tjekker disse legitimationsoplysninger mod Trafft, før noget gemmes, så en forkert adresse, forkerte legitimationsoplysninger eller en plan uden API-adgang fejler her i stedet for i en kundesamtale. Klienthemmeligheden gemmes krypteret og returneres aldrig af noget slutpunkt.

Alle tre skriveopkald (`POST`, `PUT`, `DELETE`) kræver **redigerings**-tilladelse til integrationer; status `GET` kræver **visnings**-tilladelse til integrationer.

**Forbind Trafft**

`POST /appointments/trafft/connect`

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `subdomain` | Ja | Virksomhedsadressen — den del før `.admin.trafft.com` i den URL, du logger ind på, f.eks. `acme`. En fuld adresse accepteres og reduceres til den samme værdi. |
| `client_id` | Ja | Klient-ID fra Traffts API & Connectors-side. |
| `client_secret` | Ja | Klienthemmelighed fra samme side. Gemmes krypteret, returneres aldrig. |
| `company_name` | Nej | En etiket til din egen 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"
  }'
```

**Svar** (`200 OK`):

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

Antallene kommer tilbage fra Trafft under tjekket — de er den hurtigste måde at bekræfte, at legitimationsoplysningerne peger på den konto, du mente.

**Hent forbindelsesstatus**

`GET /appointments/trafft`

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

**Svar** (`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` betyder, at intet er sat op endnu. Klienthemmeligheden er aldrig inkluderet i dette svar.

**Opdater forbindelsen**

`PUT /appointments/trafft`

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `is_active` | Nej | Indstil `false` til at sætte på pause — AI'en stopper med at booke i Trafft, forbindelsen forbliver aktiv. `true` genoptager den. |
| `company_name` | Nej | Nyt mærkat. |

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

**Svar** (`200 OK`): det samme forbindelsesobjekt som `GET` ovenfor.

**Afbryd Trafft**

`DELETE /appointments/trafft`

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

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

Afbrydelse fjerner kun de gemte legitimationsoplysninger. Aftaler, der allerede findes i Trafft, forbliver uberørte.

> **Fejlformat på alle Zenchef/Formitable/OpenTable/TheFork-slutpunkter:** i modsætning til resten af denne side indeholder fejl her deres status to gange — én gang som HTTP-status og én gang som `error_code` i brødteksten — for eksempel `{ "success": false, "error": "Restaurant not found", "error_code": 404 }`. Håndter det på samme måde som enhver anden fejl: tjek `success`, læs `error` for meddelelsen.

---

## Fejl i Appointments API

Appointment-slutpunkter returnerer standard-fejlkuverten:

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

| Status | Hvornår det sker på et appointment-slutpunkt |
|---|---|
| `400` | Et påkrævet felt mangler eller er ugyldigt — for eksempel et dårligt `start_time`, en `end_time` der ikke er efter `start_time`, en ugyldig filterkombination, ingen felter at opdatere, eller en aftale der allerede er annulleret. |
| `404` | Aftalen, kontakten eller begivenhedstypen blev ikke fundet. |
| `409` | Den ønskede tidslomme er allerede optaget (bookingkonflikt). |

De delte koder, som ethvert endpoint kan returnere — `401`, `403` (din plan inkluderer ikke API-adgang), `429` (rate limit) og `500` — er angivet med vejledning om genforsøg i [Errors & Pagination](errors-and-pagination.md).

---

::: master-only
## Brug din egen Google OAuth-klient (skærm til kalendersamtykke)

Når en konto forbinder Google Kalender, navngiver Googles logindialog OAuth-klientens projekt — som standard platformens. Et bureau kan registrere sin egen Google OAuth 2.0-klient på bureaukontoen; fra da af kører kalenderforbindelsen for den konto og alle underkonti gennem den klient, så samtykkeskærmen viser bureauets navn og logo. Intet andet ændrer sig: forbindelsesflowet, tovejs-synkroniseringen og aftaleslutpunkterne ovenfor fungerer præcis som før.

> **Kun Google Kalender.** Gmail-postkassens OAuth til e-mailkanalen påvirkes ikke.

### Hvad din klient har brug for først

1. **En OAuth 2.0-klient** af typen Webapplikation i dit Google Cloud-projekt, med **Google Calendar API** aktiveret på det projekt.
2. **Hver `redirect_uris`-post** (returneret af slutpunkterne nedenfor) tilføjet under klientens godkendte omdirigerings-URI'er. Den første post er dit verificerede `api.`-domæne, når du har et — Google verificerer kun et brand, hvis omdirigering ligger på et domæne, du ejer — efterfulgt af platformens neutrale vært som fallback, der bruges indtil da.
3. **Samtykkeskærmen** med dit brand, dit domæne under godkendte domæner og de to kalender-scopes deklareret (`scopes` i svaret). Indtil appen er udgivet og verificeret af Google, ser brugere en advarsel om ikke-verificeret app, og klienten er begrænset til 100 brugere.

### Gem din klient

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

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `client_id` | Ja | OAuth 2.0-klient-id'et, der slutter på `.apps.googleusercontent.com`. |
| `client_secret` | Ja | Klienthemmeligheden. Verificeres mod Google, før den gemmes, og derefter krypteres. Returneres aldrig af noget slutpunkt. |

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

**Svar**

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

En forkert hemmelighed eller et ukendt klient-id afvises med `400` og Googles egen årsag i `error`, og intet gemmes.

### Læs eller fjern den

`GET /account-config/google-oauth-client` returnerer det samme resumé til enhver tid — `configured: false` plus `redirect_uris` og `scopes` før noget gemmes, så du kan konfigurere Google-siden først. `DELETE /account-config/google-oauth-client` fjerner klienten: nye forbindelser vender tilbage til platformsklienten, og kalendere, der var forbundet via den fjernede klient, skal forbindes igen, da kun den klient, der oprettede en forbindelse, kan opdatere den.

Teammedlemmer har brug for **Integrations: view** til `GET` og **Integrations: edit** til `PUT` / `DELETE`.
:::

---

## Næste skridt

- [Kontakter](contacts.md) — opret og find de kontakter, du booker for.
- [Beskeder og samtaler](messages.md) — send en bekræftelse eller påmindelse til en kontakt.
- [Webhooks](webhooks.md) — få besked, når aftaler ændres.
