
# Spotkania

Interfejs API Appointments umożliwia rezerwowanie wizyt dla Twoich kontaktów w ramach typów wydarzeń, a następnie ich pobieranie, wyświetlanie, aktualizowanie, anulowanie lub usuwanie. Odpowiada również na pytanie, które pojawia się jako pierwsze w większości procesów rezerwacji — które terminy są faktycznie wolne — oraz obsługuje stronę kalendarza: wyświetlanie połączonych kalendarzy Google i importowanie wydarzeń, które już się w nich znajdują. Gdy połączenie z Kalendarzem Google jest aktywne, pasujące wydarzenie w kalendarzu jest tworzone i automatycznie synchronizowane w tle. Restauracje korzystające z systemów Zenchef, Formitable, OpenTable lub TheFork do własnych rezerwacji mogą być również zweryfikowane i połączone w tym miejscu, dzięki czemu Agent AI rezerwuje rzeczywiste stoliki zamiast wewnętrznych wizyt — firmy usługowe prowadzące harmonogram w Trafft mogą połączyć się w ten sam sposób.

Wszystkie ścieżki na tej stronie są względne względem podstawowego adresu URL `https://api.dmchamp.com/v1`. Każde żądanie wymaga Twojego klucza API — zobacz [Uwierzytelnianie](authentication.md), aby uzyskać pełną listę sposobów jego przesyłania. Poniższe przykłady wykorzystują nagłówek `X-API-Key`, a jeden z przykładów cURL pokazuje również formularz zapytania `?apiKey=`.

> **Wydarzenia a spotkania:** *Typ wydarzenia* to definicja dostępnego terminu (rodzaj spotkania, jego długość, sale). *Spotkanie* to jedna zarezerwowana instancja typu wydarzenia dla konkretnego kontaktu. Spotkanie rezerwuje się poprzez odwołanie do kontaktu oraz typu wydarzenia.

---

## Obiekt spotkania

Każdy punkt końcowy, który zwraca spotkanie, używa tego samego formatu:

| Pole | Opis |
|---|---|
| `id` | Unikalny identyfikator spotkania. |
| `contact_id` | Identyfikator kontaktu, dla którego zarezerwowano spotkanie. |
| `event_id` | Identyfikator typu wydarzenia, w ramach którego zarezerwowano spotkanie. |
| `status` | `Confirmed` lub `Canceled`. |
| `start_time` | Początek spotkania, format ISO 8601 w UTC. |
| `end_time` | Koniec spotkania, format ISO 8601 w UTC. |
| `created_at` | Czas utworzenia spotkania. |
| `last_modified_at` | Czas ostatniej zmiany spotkania. |
| `room_name` | Sala lub zasób, w którym zarezerwowano spotkanie, jeśli typ wydarzenia korzysta z sal. |
| `description` | Dowolny opis spotkania. |
| `summary` | Krótkie podsumowanie lub tytuł. |
| `cancelation_reason` | Powód podany podczas anulowania spotkania, jeśli istnieje. |
| `google_calendar_event_id` | Identyfikator powiązanego wydarzenia w Kalendarzu Google. Ustawiany po zakończeniu synchronizacji z kalendarzem; `null`, gdy żaden kalendarz nie jest połączony lub gdy synchronizacja jest w toku. |
| `calendar_synced` | `true` po powiązaniu spotkania z wydarzeniem w kalendarzu. |
| `imported` | `true`, gdy spotkanie zostało zaimportowane z zewnętrznego kalendarza zamiast bezpośredniej rezerwacji. |
| `is_recurring` | `true`, gdy spotkanie jest częścią serii cyklicznej. |
| `recurrence_frequency` | Częstotliwość powtarzania spotkania w przypadku serii cyklicznej. |
| `recurring_event_id` | Identyfikator serii cyklicznej, do której należy to spotkanie. |
| `recurring_interval` | Interwał między powtórzeniami w przypadku serii cyklicznej. |
| `recurring_sequence` | Pozycja tego spotkania w serii cyklicznej. |
| `end_after_x_occurrences` | Liczba wystąpień, po których kończy się seria cykliczna. |
| `booking_provider` | System źródłowy, z którego pochodzi rezerwacja, w przypadku rezerwacji przez połączonego dostawcę usług rezerwacyjnych. |

> **O synchronizacji kalendarza:** Bezpośrednio po zarezerwowaniu lub zmianie spotkania pole `google_calendar_event_id` może nadal mieć wartość `null`, a `calendar_synced` może być `false`, ponieważ synchronizacja odbywa się w tle chwilę później. Pobierz spotkanie ponownie po krótkim czasie, aby zobaczyć wypełnione pola kalendarza.

---

## Znajdź dostępne terminy

`GET /appointments/available-slots`

Zwraca terminy, które są faktycznie wolne dla danego typu wydarzenia pomiędzy dwoma punktami w czasie. Jest to zazwyczaj **pierwsze** wywołanie w procesie rezerwacji: wyświetl te terminy, pozwól użytkownikowi wybrać jeden z nich, a następnie wyślij wybrany czas do [Zarezerwuj spotkanie](#book-an-appointment).

Odpowiedź uwzględnia już godziny otwarcia i długość slotu danego typu wydarzenia, jego sale, spotkania, które już zostały zarezerwowane, oraz wszystko, co jest zablokowane w połączonych Kalendarzach Google — więc każdy zwrócony termin jest terminem, który możesz zarezerwować.

| Parametr zapytania | Wymagany | Opis |
|---|---|---|
| `event_id` | Tak | Typ wydarzenia do sprawdzenia. Musi należeć do Twojego konta. |
| `start_time` | Tak | Początek okna czasowego, dla którego chcesz sprawdzić dostępność, w formacie daty i godziny ISO 8601. |
| `end_time` | Tak | Koniec okna czasowego w formacie daty i godziny ISO 8601. Cały dzień końcowy jest uwzględniony. |

Wyniki są pogrupowane według dni — a w przypadku, gdy typ wydarzenia korzysta z sal, jedna grupa na salę na dzień:

| Pole | Opis |
|---|---|
| `date` | Dzień, którego dotyczy grupa, zapisany jako `DD/MM/YYYY`. |
| `day` | Nazwa dnia tygodnia małymi literami, na przykład `monday`. |
| `room_name` | Sala lub zasób, do którego należy ta grupa, jeśli typ wydarzenia korzysta z sal. |
| `available_slots` | Dostępne bloki rezerwacyjne w tym dniu, od najwcześniejszego. |

Każdy wpis w `available_slots` zawiera:

| Pole | Opis |
|---|---|
| `start_time` | Początek bloku jako `HH:mm`. |
| `end_time` | Koniec bloku jako `HH:mm`. |
| `available` | `true` — zwracany jest tylko wolny czas. |
| `spots_left` | Ile rezerwacji jeszcze mieści się w tym bloku. Obecne tylko w typach wydarzeń, które przyjmują więcej niż jedną rezerwację na slot. |

> **Czasy są lokalne dla typu wydarzenia, a nie w UTC.** `date`, `start_time` oraz `end_time` to wartości zegarowe w strefie czasowej typu wydarzenia (jego nadpisaniu lub strefie czasowej Twojego konta, jeśli nie określono inaczej). [Zarezerwuj spotkanie](#book-an-appointment) oczekuje momentu w formacie ISO 8601 UTC, więc przekonwertuj wybrany termin przed jego wysłaniem.

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

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

Dzień, w którym nie ma wolnych terminów, po prostu się nie pojawia. Brak `event_id`, `start_time` lub `end_time` zwraca `400`; typ wydarzenia, którego nie ma na Twoim koncie, zwraca `404`.

---

## Zarezerwuj spotkanie

`POST /appointments`

Rezerwuje nowe spotkanie dla kontaktu w ramach jednego z Twoich typów wydarzeń. Czas zakończenia jest obliczany automatycznie na podstawie czasu trwania terminu typu wydarzenia.

Rezerwacja jest sprawdzana pod kątem konfliktów: jeśli żądany termin pokrywa się z istniejącym potwierdzonym spotkaniem w ramach tego samego typu wydarzenia, żądanie kończy się niepowodzeniem z błędem `409` i nic nie zostaje utworzone.

| Pole | Wymagane | Opis |
|---|---|---|
| `contact_id` | Tak | Identyfikator kontaktu, dla którego dokonujemy rezerwacji. Musi należeć do Twojego konta. |
| `event_id` | Tak | Identyfikator typu wydarzenia, w ramach którego dokonujemy rezerwacji. Musi należeć do Twojego konta. |
| `start_time` | Tak | Żądany czas rozpoczęcia jako data i godzina w formacie ISO 8601. |
| `room_name` | Nie | Nazwa sali lub zasobu, jeśli typ wydarzenia korzysta z sal. |

**cURL** (używając formularza zapytania `?apiKey=`)

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

**JavaScript**

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

**Python**

```python
import requests

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

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

---

## Uzyskaj spotkanie

`GET /appointments/{appointmentId}`

Zwraca pojedyncze spotkanie na podstawie jego identyfikatora, w tym jego stan synchronizacji z kalendarzem.

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

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

---

## Wyświetl listę spotkań

`GET /appointments`

Wyświetla listę spotkań dla Twojego konta, od najnowszego, z wykorzystaniem stronicowania opartego na kursorze.

| Parametr zapytania | Wymagany | Opis |
|---|---|---|
| `contact_id` | Nie | Zwraca tylko spotkania dla tego kontaktu. Listy filtrowane według kontaktu zawierają **tylko potwierdzone spotkania** |
| `date` | Nie | Zwraca tylko spotkania z tego dnia kalendarzowego (`YYYY-MM-DD`). **Wymaga `contact_id`.** |
| `status` | Nie | Filtruj według `Confirmed` lub `Canceled`. Dostępne tylko **bez** `contact_id`. |
| `limit` | Nie | Rozmiar strony, liczba całkowita od 1 do 100. Domyślnie `50`. |
| `cursor` | Nie | Wartość `next_cursor` z poprzedniej odpowiedzi. |

Kilka zasad, o których warto pamiętać:

- **Bez filtrów** otrzymasz każde spotkanie na koncie, strona po stronie.
- **Według kontaktu** — ustaw `contact_id`, aby zobaczyć potwierdzone spotkania danego kontaktu. Możesz zawęzić to do jednego dnia, przekazując również `date`.
- **Według statusu** — ustaw `status` (bez `contact_id`), aby wyświetlić tylko spotkania `Confirmed` lub tylko `Canceled` na całym koncie.
- Filtr `date` bez `contact_id` lub `status=Canceled` wraz z `contact_id` zwraca `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"])
```

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

Aby przeglądać wyniki, przekaż `next_cursor` z jednej odpowiedzi jako `cursor` w następnym żądaniu. Kontynuuj, aż `next_cursor` będzie `null`. Zobacz [Błędy i stronicowanie](errors-and-pagination.md), aby poznać wspólny wzorzec stronicowania.

---

## Zaktualizuj spotkanie

`PUT /appointments/{appointmentId}`

Zmień termin spotkania lub edytuj jego szczegóły. Wyślij tylko te pola, które chcesz zmienić — wymagane jest co najmniej jedno. Połączony czas rozpoczęcia i zakończenia musi zachowywać porządek chronologiczny (`end_time` musi być po `start_time`). Zmiany są automatycznie synchronizowane z powiązanym wydarzeniem w kalendarzu.

| Pole | Opis |
|---|---|
| `start_time` | Nowy początek, data i godzina w formacie ISO 8601. |
| `end_time` | Nowy koniec, data i godzina w formacie ISO 8601. Musi przypadać po czasie rozpoczęcia. |
| `room_name` | Nowa nazwa pokoju lub zasobu. |
| `description` | Nowy opis lub `null`, aby go wyczyścić. |
| `summary` | Nowe podsumowanie lub `null`, aby je wyczyścić. |

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

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

---

## Anulowanie spotkania

`POST /appointments/{appointmentId}/cancel`

Anuluje potwierdzone spotkanie, opcjonalnie rejestrując powód. Spotkanie pozostaje na Twoim koncie ze statusem `Canceled`, a powiązane wydarzenie w kalendarzu jest automatycznie usuwane w tle. Anulowanie już anulowanego spotkania zwraca `400`.

| Pole | Wymagane | Opis |
|---|---|---|
| `cancellation_reason` | Nie | Powód anulowania, zapisywany w spotkaniu. |

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

**Odpowiedź** (`200 OK`):

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

---

## Usuwanie spotkania

`DELETE /appointments/{appointmentId}`

Trwale usuwa spotkanie i jego odniesienia. Jeśli chcesz tylko odwołać rezerwację, zachowując rekord, użyj zamiast tego [anuluj](#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"])
```

**Odpowiedź** (`200 OK`):

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

---

## Wyświetl swoje połączone Kalendarze Google

`GET /appointments/google-calendars`

Zwraca kalendarze Google dostępne na tym koncie, bezpośrednio z Google — przydatne do pokazania właścicielowi konta selektora kalendarza, z którego ma importować dane, lub po prostu do potwierdzenia, że połączenie jest aktywne.

Działa to tylko wtedy, gdy konto ma połączony Kalendarz Google (Ustawienia → Integracje) z co najmniej dostępem do odczytu. Jeśli tak nie jest lub przyznany dostęp nie obejmuje już zakresu odczytu kalendarza, otrzymasz `400` z informacją o konieczności (ponownego) połączenia go.

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

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

Każdy wpis ma własny format [`CalendarListEntry`](https://developers.google.com/calendar/api/v3/reference/calendarList) Google, więc nazwy pól są zgodne z `camelCase` Google, a nie ze standardowym `snake_case` tego API — są to dane Google przekazane w niezmienionej formie, a nie nasze. Brakujące lub cofnięte połączenie zwraca `400` z błędem wyjaśniającym, że Kalendarz Google wymaga (ponownego) połączenia.

---

## Importuj wydarzenia z Kalendarza Google

`POST /appointments/import-calendar-events`

Pobiera wydarzenia znajdujące się już w połączonym(-ych) Kalendarzu(-ach) Google kampanii lub Agenta AI i zamienia je w spotkania — przydatne przy pierwszym łączeniu kalendarza, który ma już istniejące rezerwacje. Może to chwilę potrwać (każde wydarzenie przechodzi przez proces ekstrakcji, aby ustalić, dla kogo jest przeznaczone), więc nigdy nie działa w trybie inline: żądanie dodaje zadanie do kolejki w tle i zwraca `job_id` do odpytywania.

| Pole | Wymagane | Opis |
|---|---|---|
| `campaign_id` | Jedno z tych dwóch | Kampania, z której połączonych kalendarzy importować dane. |
| `agent_id` | Jedno z tych dwóch | Agent AI, z którego połączonych kalendarzy importować dane. |
| `identifier` | Tak | `"EMAIL"` lub `"PHONE_NUMBER"` — który element danych kontaktowych wyodrębnić z każdego wydarzenia w kalendarzu, aby dopasować lub utworzyć kontakt, do którego ono należy. |

Wyślij dokładnie jedno z `campaign_id` / `agent_id`, nigdy oba i nigdy żadnego — każda inna kombinacja zwraca `400`. To, które wyślesz, musi należeć do Twojego konta, w przeciwnym razie otrzymasz `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"])
```

**Odpowiedź** (`202 Accepted`):

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

`campaign_id` i `agent_id` zwracają to, które wysłałeś; drugie jest zawsze `null`.

### Odpytywanie zadania importu

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

**Odpowiedź** (`200 OK`):

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

| `status` | Znaczenie |
|---|---|
| `queued` | Jeszcze nieodebrane. Kontynuuj odpytywanie. |
| `processing` | Import jest w toku. Kontynuuj odpytywanie. |
| `completed` | Gotowe — `message` zawiera krótkie, czytelne podsumowanie. |
| `failed` | Coś poszło nie tak — `error` zawiera przyczynę. |

`GET` dla `jobId`, który nie istnieje (lub należy do innego konta), zwraca `404`.

---

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

## Zewnętrzne integracje rezerwacyjne (Zenchef / Formitable / OpenTable / TheFork / Trafft)

Zenchef i Formitable to systemy rezerwacji restauracji, za pośrednictwem których Twój Agent AI może rezerwować prawdziwe stoliki; [Trafft](#trafft) to platforma do planowania dla firm usługowych, łączona raz na konto, a nie dla każdej restauracji z osobna. Obie platformy restauracyjne posiadają **publiczny, nieuwierzytelniony widżet rezerwacyjny** (`https://api.dmchamp.com/v1/zenchef-widget/...` i `https://api.dmchamp.com/v1/formitable-widget/...`), który wyświetla się w czacie dla klienta — te ścieżki widżetów to zwykłe strony HTML przeznaczone do otwierania w przeglądarce, a nie punkty końcowe JSON API, dlatego nie zostały tutaj opisane. Poniżej znajdują się punkty końcowe zarządzania kontem: weryfikacja, czy identyfikator restauracji należy do właściciela konta, a następnie dodawanie, aktualizowanie lub usuwanie go.

### Zenchef

Podłączenie restauracji Zenchef to dwuetapowa weryfikacja, dzięki której właściciel konta potwierdza, że faktycznie prowadzi restaurację, zanim zostanie ona połączona z botem: najpierw sprawdź, czy identyfikator istnieje (bez ujawniania nazwy), a następnie poproś o samodzielne wpisanie nazwy restauracji i sprawdź, czy jest ona zgodna.

**Krok 1 — Sprawdź, czy identyfikator restauracji istnieje**

`POST /appointments/zenchef-restaurants/check`

| Pole | Wymagane | Opis |
|---|---|---|
| `restaurant_id` | Tak | Identyfikator restauracji Zenchef do sprawdzenia. |

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

**Odpowiedź** (`200 OK`):

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

`exists: false` oznacza, że żadna restauracja Zenchef nie posiada tego identyfikatora — nie ma nic więcej do zrobienia. Limit wynosi 10 sprawdzeń na 5 minut na konto; przekroczenie tego limitu zwraca `429`.

**Krok 2 — Zweryfikuj nazwę restauracji**

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

| Pole | Wymagane | Opis |
|---|---|---|
| `restaurant_id` | Tak | Identyfikator restauracji Zenchef z kroku 1. |
| `user_input_name` | Tak | Nazwa wpisana przez właściciela konta — porównywana z rzeczywistą nazwą restauracji w Zenchef (wielkość liter i białe znaki są ignorowane). |

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

**Odpowiedź** (`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` oznacza, że nazwa nie pasuje — `restaurantDetails` jest pomijane, poproś właściciela konta o ponowną próbę. Limit wynosi 3 próby na 5 minut (bardziej rygorystyczny niż sprawdzenie istnienia, ponieważ jest to właściwy krok weryfikacyjny). `restaurant_id`, który nie jest już rozpoznawany w Zenchef, zwraca `404`.

**Krok 3 — Zapisz restaurację**

`POST /appointments/zenchef-restaurants`

| Pole | Wymagane | Opis |
|---|---|---|
| `restaurant_id` | Tak | 1–64 znaki, litery/cyfry/podkreślnik/myślnik. |
| `restaurant_name` | Tak | Zweryfikowana nazwa restauracji z kroku 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" }'
```

**Odpowiedź** (`201 Created`):

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

**Aktualizacja zapisanej restauracji Zenchef**

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

| Pole | Wymagane | Opis |
|---|---|---|
| `restaurant_name` | Nie | Nowa nazwa wyświetlana. |
| `is_active` | Nie | Ustaw `false`, aby powstrzymać bota przed dokonywaniem rezerwacji w tej restauracji bez jej usuwania. |

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

**Odpowiedź** (`200 OK`): ten sam format co odpowiedź zapisu powyżej.

**Usuń restaurację Zenchef**

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

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

`restaurantId`, którego obecnie nie ma na koncie, zwraca `404` przy aktualizacji lub usunięciu.

### Formitable

Formitable nie wymaga dwuetapowego potwierdzenia nazwy, jak Zenchef — jego identyfikatory restauracji są już przypisane do konkretnej firmy, więc wystarczy jedno wywołanie weryfikacyjne. Posiada również funkcję wyszukiwania szczegółów, używaną do buforowania adresu URL strony internetowej restauracji podczas konfiguracji.

**Zweryfikuj identyfikator restauracji**

`POST /appointments/formitable-restaurants/verify`

| Pole | Wymagane | Opis |
|---|---|---|
| `restaurant_id` | Tak | Identyfikator restauracji Formitable. |
| `language` | Nie | Znacznik języka dla żądania sondowania. Domyślnie `"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" }'
```

**Odpowiedź** (`200 OK`):

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

`restaurant_id`, którego Formitable nie rozpoznaje, zwraca `404`. Limit prędkości wynosi 10 prób na 5 minut na konto.

**Pobierz szczegóły restauracji**

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

Pobiera publiczny profil restauracji z Formitable, w tym jej stronę internetową — używane do buforowania adresu URL strony podczas konfigurowania restauracji. `language` jest opcjonalnym parametrem zapytania, domyślnie ustawionym na `"en"`.

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

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

**Zapisz restaurację**

`POST /appointments/formitable-restaurants`

| Pole | Wymagane | Opis |
|---|---|---|
| `restaurant_id` | Tak | 1–64 znaki, litery/cyfry/podkreślnik/myślnik. |
| `restaurant_name` | Tak | Nazwa wyświetlana. |
| `language` | Tak | Znacznik języka ISO, np. `"en"` lub `"en-GB"`. |
| `website_url` | Nie | Strona internetowa restauracji, z powyższego wyszukiwania szczegółów. Musi być `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"
  }'
```

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

**Zaktualizuj zapisaną restaurację Formitable**

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

| Pole | Wymagane | Opis |
|---|---|---|
| `restaurant_name` | Nie | Nowa nazwa wyświetlana. |
| `language` | Nie | Nowy znacznik języka ISO. |
| `is_active` | Nie | Ustaw `false`, aby powstrzymać bota przed dokonywaniem rezerwacji w tej restauracji bez jej usuwania. |
| `website_url` | Nie | Nowy adres URL strony internetowej. |

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

**Odpowiedź** (`200 OK`): ten sam format co odpowiedź zapisu powyżej.

**Usuń restaurację Formitable**

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

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

`restaurantId`, którego obecnie nie ma na koncie, zwraca `404` przy aktualizacji lub usunięciu.

### OpenTable

Restauracje OpenTable są dostępne za pośrednictwem danych uwierzytelniających partnera OpenTable platformy, a OpenTable pozwala tym danym widzieć tylko te restauracje, które połączyły ofertę platformy wewnątrz Marketplace Integracji OpenTable. Podobnie jak w przypadku Formitable, wystarczy jedno wywołanie weryfikacyjne: osiągalny identyfikator restauracji (numeryczny "RID") potwierdza zarówno istnienie restauracji, jak i fakt, że połączyła ona integrację. Dopóki oferta partnera OpenTable nie zostanie włączona na platformie, wywołanie weryfikacyjne zwróci `503`.

**Weryfikacja restauracji OpenTable**

`POST /appointments/opentable-restaurants/verify`

| Pole | Wymagane | Opis |
| --- | --- | --- |
| `restaurant_id` | Tak | Identyfikator restauracji OpenTable (RID), liczba taka jak `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" }'
```

**Odpowiedź** (`200 OK`):

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

`verified: false` oznacza, że żadna restauracja OpenTable nie posiada tego identyfikatora. `403` oznacza, że restauracja istnieje, ale nie połączyła jeszcze integracji platformy wewnątrz OpenTable. Limit wynosi 10 prób na 5 minut na konto.

**Dodawanie restauracji OpenTable**

`POST /appointments/opentable-restaurants`

| Pole | Wymagane | Opis |
| --- | --- | --- |
| `restaurant_id` | Tak | Zweryfikowany identyfikator restauracji. |
| `restaurant_name` | Tak | Nazwa wyświetlana (etykieta; używana również przez AI do nazywania restauracji). |
| `website_url` | Nie | Adres URL http(s) pokazywany gościom, gdy AI przekierowuje ich do restauracji. |

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

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

**Aktualizacja zapisanej restauracji OpenTable**

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

Wyślij dowolne z `restaurant_name`, `is_active` (wstrzymaj za pomocą `false`) lub `website_url` (pusty ciąg znaków czyści pole); pominięte pola pozostają bez zmian.

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

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

**Usuwanie restauracji z OpenTable**

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

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

Aktualizacja `restaurantId`, którego nie ma obecnie na koncie, zwraca `404`.

### TheFork

Restauracje TheFork są obsługiwane za pośrednictwem danych uwierzytelniających partnera TheFork, a platforma TheFork pozwala tym danym widzieć tylko te restauracje, które mają włączonego partnera platformy na swoim koncie TheFork. Podobnie jak w przypadku Formitable i OpenTable, wystarczy jedno wywołanie weryfikacyjne: osiągalny identyfikator restauracji (Restaurant ID) potwierdza zarówno istnienie restauracji, jak i to, że partner jest na niej włączony. Identyfikator to UUID nadawany restauracji przez TheFork w menedżerze TheFork Manager, przesyłany jako ciąg znaków. Dopóki TheFork nie zatwierdzi platformy jako partnera i nie wyda danych uwierzytelniających, wywołanie weryfikacyjne zwróci `503` — zobacz [TheFork](../integrations/thefork.md), aby dowiedzieć się, co to oznacza obecnie.

**Weryfikacja restauracji TheFork**

`POST /appointments/thefork-restaurants/verify`

| Pole | Wymagane | Opis |
| --- | --- | --- |
| `restaurant_id` | Tak | Identyfikator restauracji TheFork, UUID, np. `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" }'
```

**Odpowiedź** (`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` to wielkości grup, które restauracja przyjmuje online w ciągu najbliższych 30 dni. `404` lub `403` oznacza, że TheFork nie udostępnił nam restauracji o tym identyfikatorze — albo identyfikator jest błędny, albo partner platformy nie jest jeszcze włączony dla tej restauracji; `400` oznacza, że identyfikator nie jest w formacie UUID. Limit wynosi 10 prób na 5 minut na konto.

**Dodawanie restauracji TheFork**

`POST /appointments/thefork-restaurants`

| Pole | Wymagane | Opis |
| --- | --- | --- |
| `restaurant_id` | Tak | Zweryfikowany identyfikator restauracji (UUID). |
| `restaurant_name` | Tak | Nazwa wyświetlana (etykieta; używana również przez AI do nazywania restauracji). |
| `website_url` | Nie | Adres URL http(s) pokazywany gościom, gdy AI przekierowuje ich do restauracji. |

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

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

**Aktualizacja zapisanej restauracji TheFork**

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

Wyślij dowolne z `restaurant_name`, `is_active` (wstrzymaj za pomocą `false`) lub `website_url` (pusty ciąg znaków czyści pole); pominięte pola pozostają bez zmian.

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

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

**Usuwanie restauracji TheFork**

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

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

`restaurantId`, którego obecnie nie ma na koncie, zwraca `404` przy aktualizacji lub usunięciu.

### Trafft

System Trafft łączy się raz dla całego konta, a nie dla każdej lokalizacji: jeden adres firmy oraz dane uwierzytelniające API z panelu administratora Trafft (**Features & Integrations → API & Connectors**, część planu Business w Trafft). Wywołanie połączenia sprawdza te dane uwierzytelniające w systemie Trafft przed zapisaniem czegokolwiek, więc błędny adres, błędne dane lub plan bez dostępu do API spowodują błąd na tym etapie, a nie podczas rozmowy z klientem. Klient secret jest przechowywany w formie zaszyfrowanej i nigdy nie jest zwracany przez żaden punkt końcowy.

Wszystkie trzy wywołania zapisu (`POST`, `PUT`, `DELETE`) wymagają uprawnienia **edit** dla integracji; status `GET` wymaga uprawnienia **view** dla integracji.

**Połącz Trafft**

`POST /appointments/trafft/connect`

| Pole | Wymagane | Opis |
|---|---|---|
| `subdomain` | Tak | Adres firmy — część przed `.admin.trafft.com` w adresie URL, pod którym się logujesz, np. `acme`. Pełny adres jest akceptowany i skracany do tej samej wartości. |
| `client_id` | Tak | Client ID ze strony API & Connectors w Trafft. |
| `client_secret` | Tak | Client Secret z tej samej strony. Przechowywany w formie zaszyfrowanej, nigdy nie jest zwracany. |
| `company_name` | Nie | Etykieta dla Twojej własnej listy. |

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

**Odpowiedź** (`200 OK`):

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

Liczby są zwracane z systemu Trafft podczas sprawdzania — to najszybszy sposób, aby potwierdzić, że dane uwierzytelniające wskazują na konto, o które Ci chodziło.

**Pobierz status połączenia**

`GET /appointments/trafft`

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

**Odpowiedź** (`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` oznacza, że nic nie zostało jeszcze skonfigurowane. Klient secret nigdy nie jest uwzględniany w tej odpowiedzi.

**Aktualizuj połączenie**

`PUT /appointments/trafft`

| Pole | Wymagane | Opis |
|---|---|---|
| `is_active` | Nie | Ustaw `false`, aby wstrzymać — AI przestaje dodawać rezerwacje do Trafft, połączenie pozostaje aktywne. `true` wznawia działanie. |
| `company_name` | Nie | Nowa etykieta. |

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

**Odpowiedź** (`200 OK`): ten sam obiekt połączenia co `GET` powyżej.

**Rozłącz Trafft**

`DELETE /appointments/trafft`

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

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

Rozłączenie usuwa tylko zapisane dane uwierzytelniające. Rezerwacje znajdujące się już w Trafft pozostają nienaruszone.

> **Format błędu we wszystkich punktach końcowych Zenchef/Formitable/OpenTable/TheFork:** w przeciwieństwie do reszty tej strony, błędy w tym miejscu zawierają status dwukrotnie — raz jako status HTTP, a raz jako `error_code` w treści — na przykład `{ "success": false, "error": "Restaurant not found", "error_code": 404 }`. Obsługuj je w taki sam sposób jak każdy inny błąd: sprawdź `success`, przeczytaj `error`, aby uzyskać komunikat.

---

## Błędy API wizyt

Punkty końcowe wizyt zwracają standardową kopertę błędu:

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

| Status | Kiedy występuje w punkcie końcowym wizyt |
|---|---|
| `400` | Brakuje wymaganego pola lub jest ono nieprawidłowe — na przykład błędny `start_time`, `end_time` niebędący po `start_time`, nieprawidłowa kombinacja filtrów, brak pól do aktualizacji lub wizyta, która została już anulowana. |
| `404` | Nie znaleziono wizyty, kontaktu lub typu wydarzenia. |
| `409` | Żądany przedział czasowy jest już zajęty (konflikt rezerwacji). |

Wspólne kody, które może zwrócić każdy punkt końcowy — `401`, `403` (Twój plan nie obejmuje dostępu do API), `429` (limit szybkości) oraz `500` — zostały wymienione wraz ze wskazówkami dotyczącymi ponawiania prób w sekcji [Błędy i stronicowanie](errors-and-pagination.md).

---

::: master-only
## Użyj własnego klienta Google OAuth (ekran zgody Kalendarza)

Gdy konto łączy się z Kalendarzem Google, okno logowania Google wyświetla nazwę projektu klienta OAuth — domyślnie jest to projekt platformy. Agencja może zarejestrować własnego klienta Google OAuth 2.0 na koncie agencji; od tego momentu łączenie kalendarza dla tego konta i wszystkich podrzędnych kont odbywa się za pośrednictwem tego klienta, dzięki czemu na ekranie zgody wyświetlana jest nazwa i logo agencji. Nic innego się nie zmienia: proces łączenia, synchronizacja dwukierunkowa i powyższe punkty końcowe dotyczące spotkań działają dokładnie tak samo jak wcześniej.

> **Tylko Kalendarz Google.** OAuth skrzynki pocztowej Gmail dla kanału Email pozostaje bez zmian.

### Czego najpierw potrzebuje Twój klient

1. **Klient OAuth 2.0** typu Aplikacja internetowa w Twoim projekcie Google Cloud, z włączonym interfejsem **Google Calendar API** w tym projekcie.
2. **Każdy wpis `redirect_uris`** (zwracany przez poniższe punkty końcowe) dodany w sekcji Autoryzowane identyfikatory URI przekierowania klienta. Pierwszym wpisem jest Twoja zweryfikowana domena `api.`, jeśli ją posiadasz — Google weryfikuje tylko markę, której przekierowanie znajduje się w domenie, której jesteś właścicielem — a następnie neutralny host platformy jako rozwiązanie zastępcze używane do tego czasu.
3. **Ekran zgody** z Twoją marką, Twoją domeną w sekcji Autoryzowane domeny oraz zadeklarowanymi dwoma zakresami Kalendarza (`scopes` w odpowiedzi). Dopóki aplikacja nie zostanie opublikowana i zweryfikowana przez Google, użytkownicy będą widzieć ostrzeżenie o niezweryfikowanej aplikacji, a liczba użytkowników klienta będzie ograniczona do 100.

### Zapisz swojego klienta

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

| Pole | Wymagane | Opis |
|---|---|---|
| `client_id` | Tak | Identyfikator klienta OAuth 2.0, kończący się na `.apps.googleusercontent.com`. |
| `client_secret` | Tak | Klucz tajny klienta. Weryfikowany w Google przed zapisaniem, a następnie szyfrowany. Nigdy nie jest zwracany przez żaden punkt końcowy. |

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

**Odpowiedź**

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

Błędny klucz tajny lub nieznany identyfikator klienta są odrzucane z `400` oraz własnym powodem Google w `error`, a żadne dane nie są zapisywane.

### Odczytaj lub usuń

`GET /account-config/google-oauth-client` zwraca to samo podsumowanie w dowolnym momencie — `configured: false` oraz `redirect_uris` i `scopes` przed zapisaniem czegokolwiek, dzięki czemu możesz najpierw skonfigurować stronę Google. `DELETE /account-config/google-oauth-client` usuwa klienta: nowe połączenia powracają do klienta platformy, a kalendarze, które zostały połączone za pośrednictwem usuniętego klienta, muszą zostać połączone ponownie, ponieważ tylko klient, który ustanowił połączenie, może je odświeżyć.

Członkowie zespołu potrzebują uprawnień **Integracje: wyświetlanie** dla `GET` oraz **Integracje: edycja** dla `PUT` / `DELETE`.
:::

---

## Następne kroki

- [Kontakty](contacts.md) — twórz i wyszukuj kontakty, dla których dokonujesz rezerwacji.
- [Wiadomości i konwersacje](messages.md) — wysyłaj do kontaktu potwierdzenia lub przypomnienia.
- [Webhooki](webhooks.md) — otrzymuj powiadomienia o zmianach w spotkaniach.
