
# Programări

API-ul de Programări vă permite să rezervați programări pentru contactele dvs. în funcție de tipurile de evenimente, apoi să le preluați, să le listați, să le actualizați, să le anulați sau să le ștergeți. De asemenea, răspunde la întrebarea care apare prima în majoritatea fluxurilor de rezervare — ce intervale orare sunt libere — și acoperă partea de calendar: listarea calendarelor Google pe care le-ați conectat și importarea evenimentelor care există deja în acestea. Când o conexiune Google Calendar este activă, evenimentul corespondent din calendar este creat și menținut sincronizat automat în fundal. Restaurantele care utilizează Zenchef, Formitable, OpenTable sau TheFork pentru propriul sistem de rezervări pot fi, de asemenea, verificate și conectate aici, astfel încât Agentul AI să rezerve mese reale în loc de programări interne — iar companiile care își gestionează programul în Trafft se pot conecta în același mod.

Toate căile de pe această pagină sunt relative la URL-ul de bază `https://api.dmchamp.com/v1`. Fiecare cerere necesită cheia dvs. API — consultați [Autentificare](authentication.md) pentru lista completă a modalităților de trimitere a acesteia. Exemplele de mai jos utilizează antetul `X-API-Key`, un exemplu cURL arătând și forma de interogare `?apiKey=`.

> **Evenimente vs. programări:** Un *tip de eveniment* este o definiție a unui interval rezervabil (tipul de întâlnire, durata sa, sălile sale). O *programare* este o instanță rezervată a unui tip de eveniment pentru un anumit contact. Rezervați o programare făcând referire la contact și la tipul de eveniment.

---

## Obiectul programare

Fiecare endpoint care returnează o programare utilizează aceeași structură:

| Câmp | Descriere |
|---|---|
| `id` | ID unic al programării. |
| `contact_id` | ID-ul contactului cu care este făcută programarea. |
| `event_id` | ID-ul tipului de eveniment pentru care a fost făcută programarea. |
| `status` | `Confirmed` sau `Canceled`. |
| `start_time` | Începutul programării, ISO 8601 în UTC. |
| `end_time` | Sfârșitul programării, ISO 8601 în UTC. |
| `created_at` | Când a fost creată programarea. |
| `last_modified_at` | Când a fost modificată ultima dată programarea. |
| `room_name` | Sala sau resursa în care este rezervată programarea, atunci când tipul de eveniment utilizează săli. |
| `description` | Descrierea liberă a programării. |
| `summary` | Rezumat scurt sau titlu. |
| `cancelation_reason` | Motivul furnizat la anularea programării, dacă există. |
| `google_calendar_event_id` | ID-ul evenimentului Google Calendar asociat. Setat odată ce sincronizarea calendarului este finalizată; `null` când niciun calendar nu este conectat sau în timp ce sincronizarea este încă în curs. |
| `calendar_synced` | `true` odată ce programarea este legată de un eveniment din calendar. |
| `imported` | `true` când programarea a fost importată dintr-un calendar extern în loc să fie rezervată direct. |
| `is_recurring` | `true` când programarea face parte dintr-o serie recurentă. |
| `recurrence_frequency` | Cât de des se repetă programarea, în cazul recurenței. |
| `recurring_event_id` | ID-ul seriei recurente din care face parte această programare. |
| `recurring_interval` | Intervalul dintre repetiții, în cazul recurenței. |
| `recurring_sequence` | Poziția acestei programări în cadrul seriei sale recurente. |
| `end_after_x_occurrences` | Numărul de apariții după care se încheie seria recurentă. |
| `booking_provider` | Sistemul sursă din care provine rezervarea, atunci când este rezervată printr-un furnizor de rezervări conectat. |

> **Despre sincronizarea calendarului:** Imediat după ce rezervați sau modificați o programare, `google_calendar_event_id` poate fi încă `null` și `calendar_synced` poate fi `false` deoarece sincronizarea rulează în fundal puțin mai târziu. Preluarea programării din nou la scurt timp după aceea va afișa câmpurile de calendar completate.

---

## Găsiți intervale disponibile

`GET /appointments/available-slots`

Returnează momentele care sunt cu adevărat libere pentru un tip de eveniment între două puncte în timp. Acesta este, de obicei, **primul** apel într-un flux de rezervare: afișați aceste intervale, lăsați persoana să aleagă unul, apoi postați ora aleasă către [Rezervați o programare](#book-an-appointment).

Răspunsul ia deja în considerare programul de funcționare și durata intervalului specifice tipului de eveniment, sălile acestuia, programările pe care le-ați făcut deja și tot ceea ce este blocat în calendarele Google conectate — astfel încât un interval returnat aici este unul pe care îl puteți rezerva.

| Parametru interogare | Obligatoriu | Descriere |
|---|---|---|
| `event_id` | Da | Tipul de eveniment de verificat. Trebuie să aparțină contului dvs. |
| `start_time` | Da | Începutul ferestrei pentru care doriți intervale, dată-oră ISO 8601. |
| `end_time` | Da | Sfârșitul ferestrei, dată-oră ISO 8601. Întreaga zi de sfârșit este inclusă. |

Rezultatele sunt returnate grupate pe zi — și, atunci când tipul de eveniment utilizează săli, un grup per sală per zi:

| Câmp | Descriere |
|---|---|
| `date` | Ziua pe care o acoperă grupul, scrisă `DD/MM/YYYY`. |
| `day` | Numele zilei săptămânii cu litere mici, de exemplu `monday`. |
| `room_name` | Sala sau resursa căreia îi aparține acest grup, atunci când tipul de eveniment utilizează săli. |
| `available_slots` | Blocurile rezervabile din acea zi, începând cu cel mai devreme. |

Fiecare intrare din `available_slots` are:

| Câmp | Descriere |
|---|---|
| `start_time` | Începutul blocului ca `HH:mm`. |
| `end_time` | Sfârșitul blocului ca `HH:mm`. |
| `available` | `true` — este returnat doar timpul liber. |
| `spots_left` | Câte rezervări mai încap în acest bloc. Prezent doar pentru tipurile de evenimente care acceptă mai mult de o rezervare per interval. |

> **Orele sunt locale pentru tipul de eveniment, nu UTC.** `date`, `start_time` și `end_time` sunt valori de ceas de perete în fusul orar propriu al tipului de eveniment (suprascrierea acestuia sau fusul orar al contului dvs. când nu are unul). [Rezervați o programare](#book-an-appointment) așteaptă un moment UTC ISO 8601, deci convertiți intervalul ales înainte de a-l posta.

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

**Răspuns** (`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 }
      ]
    }
  ]
}
```

O zi în care nu există nimic liber pur și simplu nu apare. Lipsa `event_id`, `start_time` sau `end_time` returnează `400`; un tip de eveniment care nu se află în contul dvs. returnează `404`.

---

## Rezervarea unei programări

`POST /appointments`

Rezervă o nouă programare pentru un contact pe unul dintre tipurile dvs. de evenimente. Ora de sfârșit este calculată automat din durata intervalului tipului de eveniment.

Rezervarea este verificată pentru conflicte: dacă intervalul solicitat se suprapune cu o programare confirmată existentă pe același tip de eveniment, cererea eșuează cu un `409` și nu se creează nimic.

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `contact_id` | Da | ID-ul contactului pentru care se face rezervarea. Trebuie să aparțină contului dvs. |
| `event_id` | Da | ID-ul tipului de eveniment pe care se face rezervarea. Trebuie să aparțină contului dvs. |
| `start_time` | Da | Începutul dorit ca dată-oră ISO 8601. |
| `room_name` | Nu | Numele sălii sau resursei, atunci când tipul de eveniment utilizează săli. |

**cURL** (folosind forma de interogare `?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"])
```

**Răspuns** (`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
  }
}
```

---

## Obțineți o programare

`GET /appointments/{appointmentId}`

Returnează o singură programare după ID-ul acesteia, incluzând starea de sincronizare a calendarului.

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

**Răspuns** (`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
  }
}
```

---

## Listează programările

`GET /appointments`

Listează programările pentru contul dvs., începând cu cele mai recente, folosind paginarea bazată pe cursor.

| Parametru de interogare | Obligatoriu | Descriere |
|---|---|---|
| `contact_id` | Nu | Returnează doar programările pentru acest contact. Listele filtrate după contact includ **doar programările confirmate** |
| `date` | Nu | Returnează doar programările din această zi calendaristică (`YYYY-MM-DD`). **Necesită `contact_id`.** |
| `status` | Nu | Filtrați după `Confirmed` sau `Canceled`. Disponibil doar **fără** `contact_id`. |
| `limit` | Nu | Dimensiunea paginii, un număr întreg între 1 și 100. Valoarea implicită este `50`. |
| `cursor` | Nu | Valoarea `next_cursor` dintr-un răspuns anterior. |

Câteva reguli de reținut:

- **Fără filtre**, obțineți fiecare programare din cont, pagină cu pagină.
- **După contact** — setați `contact_id` pentru a vedea programările confirmate ale unui contact. Puteți restrânge acest lucru la o singură zi transmițând și `date`.
- **După stare** — setați `status` (fără `contact_id`) pentru a lista doar programările `Confirmed` sau doar pe cele `Canceled` din întregul cont.
- Filtrul `date` fără `contact_id`, sau `status=Canceled` împreună cu `contact_id`, returnează o `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"])
```

**Răspuns** (`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
}
```

Pentru a naviga prin rezultate, transmiteți `next_cursor` dintr-un răspuns ca `cursor` al următoarei cereri. Continuați până când `next_cursor` este `null`. Consultați [Erori și paginare](errors-and-pagination.md) pentru modelul de paginare partajat.

---

## Actualizați o programare

`PUT /appointments/{appointmentId}`

Reprogramați o întâlnire sau modificați detaliile acesteia. Trimiteți doar câmpurile pe care doriți să le modificați — este necesar cel puțin unul. Combinația de început și sfârșit trebuie să rămână în ordine cronologică (`end_time` trebuie să fie după `start_time`). Modificările sunt sincronizate automat cu evenimentul din calendarul asociat.

| Câmp | Descriere |
|---|---|
| `start_time` | Dată-oră de început nouă, format ISO 8601. |
| `end_time` | Dată-oră de sfârșit nouă, format ISO 8601. Trebuie să fie ulterioară orei de început. |
| `room_name` | Nume nou pentru cameră sau resursă. |
| `description` | Descriere nouă sau `null` pentru a o șterge. |
| `summary` | Rezumat nou sau `null` pentru a-l șterge. |

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

**Răspuns** (`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
  }
}
```

---

## Anularea unei programări

`POST /appointments/{appointmentId}/cancel`

Anulează o programare confirmată, înregistrând opțional un motiv. Programarea rămâne în contul tău cu starea `Canceled`, iar evenimentul din calendarul asociat este eliminat automat în fundal. Anularea unei programări deja anulate returnează un `400`.

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `cancellation_reason` | Nu | Motivul anulării, stocat în programare. |

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

**Răspuns** (`200 OK`):

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

---

## Ștergerea unei programări

`DELETE /appointments/{appointmentId}`

Șterge definitiv o programare și referințele acesteia. Dacă dorești doar să anulezi rezervarea păstrând în același timp înregistrarea, folosește [anulare](#cancel-an-appointment) în schimb.

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

**Răspuns** (`200 OK`):

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

---

## Listați calendarele Google conectate

`GET /appointments/google-calendars`

Returnează calendarele Google disponibile în acest cont, direct de la Google — util pentru a arăta deținătorului contului un selector din care calendar să importe mai jos, sau pur și simplu pentru a confirma că conexiunea este activă.

Acest lucru funcționează doar după ce contul a conectat Google Calendar (Setări → Integrări) cu cel puțin acces de citire. Dacă nu a făcut-o, sau dacă accesul acordat nu mai include permisiunea de citire a calendarului, vei primi un `400` care îți solicită să îl (re)conectezi.

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

**Răspuns** (`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"
    }
  ]
}
```

Fiecare intrare are forma [`CalendarListEntry`](https://developers.google.com/calendar/api/v3/reference/calendarList) proprie Google, deci numele câmpurilor urmează `camelCase` de la Google, nu `snake_case` obișnuit al acestui API — acestea sunt datele Google transmise ca atare, nu ale noastre. O conexiune lipsă sau revocată returnează `400` cu o eroare care explică faptul că Google Calendar trebuie (re)conectat.

---

## Importă evenimente dintr-un Google Calendar

`POST /appointments/import-calendar-events`

Extrage evenimentele deja existente în Google Calendarul/Calendarele conectate ale unei campanii sau ale unui Agent AI și le transformă în programări — util prima dată când conectezi un calendar care are deja rezervări. Acest proces poate dura (fiecare eveniment trece prin extracție pentru a determina cui îi aparține), așa că nu rulează niciodată inline: cererea pune în coadă un job de fundal și îți returnează un `job_id` pentru interogare (polling).

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `campaign_id` | Unul dintre cele două | Campania din al cărei calendar/calendare conectate se face importul. |
| `agent_id` | Unul dintre cele două | Agentul AI din al cărui calendar/calendare conectate se face importul. |
| `identifier` | Da | `"EMAIL"` sau `"PHONE_NUMBER"` — ce informație de contact să fie extrasă din fiecare eveniment din calendar pentru a potrivi sau crea contactul căruia îi aparține. |

Trimite exact unul dintre `campaign_id` / `agent_id`, niciodată pe ambele și niciodată pe niciunul — orice altă combinație returnează un `400`. Oricare dintre ele trimiți, trebuie să aparțină contului tău, altfel vei primi un `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"])
```

**Răspuns** (`202 Accepted`):

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

`campaign_id` și `agent_id` reflectă exact ceea ce ai trimis; celălalt este întotdeauna `null`.

### Interoghează jobul de import

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

**Răspuns** (`200 OK`):

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

| `status` | Semnificație |
|---|---|
| `queued` | Încă nu a fost preluat. Continuă interogarea. |
| `processing` | Importul este în curs de desfășurare. Continuă interogarea. |
| `completed` | Finalizat — `message` conține un scurt rezumat ușor de citit. |
| `failed` | Ceva nu a mers bine — `error` conține motivul. |

`GET` pe un `jobId` care nu există (sau aparține unui alt cont) returnează `404`.

---

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

## Integrări externe de rezervare (Zenchef / Formitable / OpenTable / TheFork / Trafft)

Zenchef și Formitable sunt sisteme de rezervări pentru restaurante prin care Agentul dvs. AI poate rezerva mese reale; [Trafft](#trafft) este o platformă de programări pentru afaceri, conectată o singură dată per cont, nu per restaurant. Cele două platforme pentru restaurante au fiecare un **widget de rezervare public, neautentificat** (`https://api.dmchamp.com/v1/zenchef-widget/...` și `https://api.dmchamp.com/v1/formitable-widget/...`) care se afișează în chat pentru client — acele rute ale widget-ului sunt pagini HTML simple menite să fie deschise într-un browser, nu endpoint-uri API JSON, deci nu sunt documentate aici. Ceea ce urmează sunt endpoint-urile de gestionare a contului: verificarea faptului că un ID de restaurant aparține titularului contului, apoi adăugarea, actualizarea sau eliminarea acestuia.

### Zenchef

Conectarea unui restaurant Zenchef este un proces de verificare în doi pași, astfel încât titularul contului să demonstreze că administrează efectiv restaurantul înainte ca acesta să fie conectat la bot: mai întâi se verifică dacă ID-ul există (fără a dezvălui numele), apoi i se cere acestuia să introducă singur numele restaurantului pentru a verifica dacă acesta corespunde.

**Pasul 1 — Verificarea existenței unui ID de restaurant**

`POST /appointments/zenchef-restaurants/check`

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `restaurant_id` | Da | ID-ul restaurantului Zenchef care trebuie verificat. |

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

**Răspuns** (`200 OK`):

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

`exists: false` înseamnă că niciun restaurant Zenchef nu are acel ID — nu mai este nimic de făcut. Limitat la 10 verificări la fiecare 5 minute per cont; depășirea acestei limite returnează `429`.

**Pasul 2 — Verificarea numelui restaurantului**

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

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `restaurant_id` | Da | ID-ul restaurantului Zenchef de la pasul 1. |
| `user_input_name` | Da | Numele introdus de titularul contului — comparat cu numele real al restaurantului din Zenchef (fără a ține cont de majuscule/spații). |

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

**Răspuns** (`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` înseamnă că numele nu a corespuns — `restaurantDetails` este omis, cereți titularului contului să încerce din nou. Limitat la 3 încercări la fiecare 5 minute (mai strict decât verificarea existenței, deoarece acesta este pasul de verificare propriu-zisă). Un `restaurant_id` care nu mai este valid în Zenchef returnează `404`.

**Pasul 3 — Salvarea restaurantului**

`POST /appointments/zenchef-restaurants`

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `restaurant_id` | Da | 1–64 caractere, litere/cifre/underscore/cratimă. |
| `restaurant_name` | Da | Numele verificat al restaurantului de la pasul 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" }'
```

**Răspuns** (`201 Created`):

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

**Actualizarea unui restaurant Zenchef salvat**

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

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `restaurant_name` | Nu | Numele de afișare nou. |
| `is_active` | Nu | Setați `false` pentru a împiedica botul să efectueze rezervări la acest restaurant fără a-l elimina. |

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

**Răspuns** (`200 OK`): aceeași formă ca răspunsul de salvare de mai sus.

**Eliminarea unui restaurant 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"
```

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

Un `restaurantId` care nu se află în prezent în cont returnează `404` la actualizare sau ștergere.

### Formitable

Formitable nu are nevoie de verificarea numelui în doi pași ca Zenchef — ID-urile sale de restaurant sunt deja delimitate per afacere, deci un singur apel de verificare este suficient. De asemenea, are o funcție de căutare a detaliilor utilizată pentru a stoca în cache URL-ul site-ului web al restaurantului în timpul configurării.

**Verificarea unui ID de restaurant**

`POST /appointments/formitable-restaurants/verify`

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `restaurant_id` | Da | ID-ul restaurantului Formitable. |
| `language` | Nu | Etichetă de limbă pentru cererea de sondare. Valoarea implicită este `"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" }'
```

**Răspuns** (`200 OK`):

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

Un `restaurant_id` pe care Formitable nu îl recunoaște returnează `404`. Limitat la 10 încercări la fiecare 5 minute per cont.

**Obținerea detaliilor restaurantului**

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

Preluarea profilului public al restaurantului de pe Formitable, inclusiv site-ul web — utilizat pentru a stoca în cache URL-ul site-ului web în timpul configurării restaurantului. `language` este un parametru de interogare opțional, cu valoarea implicită `"en"`.

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

**Răspuns** (`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"
  }
}
```

**Salvează restaurantul**

`POST /appointments/formitable-restaurants`

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `restaurant_id` | Da | 1–64 caractere, litere/cifre/underscore/cratimă. |
| `restaurant_name` | Da | Nume afișat. |
| `language` | Da | Etichetă de limbă ISO, de ex. `"en"` sau `"en-GB"`. |
| `website_url` | Nu | Site-ul web al restaurantului, din căutarea detaliilor de mai sus. Trebuie să fie `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"
  }'
```

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

**Actualizează un restaurant Formitable salvat**

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

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `restaurant_name` | Nu | Nume afișat nou. |
| `language` | Nu | Etichetă de limbă ISO nouă. |
| `is_active` | Nu | Setează `false` pentru a opri botul din a efectua rezervări la acest restaurant fără a-l elimina. |
| `website_url` | Nu | URL site web nou. |

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

**Răspuns** (`200 OK`): aceeași formă ca răspunsul de salvare de mai sus.

**Elimină un restaurant 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"
```

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

Un `restaurantId` care nu se află în prezent în cont returnează `404` la actualizare sau ștergere.

### OpenTable

Restaurantele OpenTable sunt accesate prin intermediul acreditărilor de partener OpenTable ale platformei, iar OpenTable permite acestor acreditări să vadă doar restaurantele care au conectat listarea platformei în cadrul Marketplace-ului de Integrări OpenTable. Așadar, la fel ca în cazul Formitable, un singur apel de verificare este suficient: un ID de restaurant accesibil (numărul "RID") dovedește atât faptul că restaurantul există, cât și că a conectat integrarea. Până când listarea de partener OpenTable este activată pe platformă, apelul de verificare va răspunde `503`.

**Verificarea unui restaurant OpenTable**

`POST /appointments/opentable-restaurants/verify`

| Câmp | Obligatoriu | Descriere |
| --- | --- | --- |
| `restaurant_id` | Da | ID-ul restaurantului OpenTable (RID), un număr precum `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" }'
```

**Răspuns** (`200 OK`):

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

`verified: false` înseamnă că niciun restaurant OpenTable nu are acel ID. Un `403` înseamnă că restaurantul există, dar nu a conectat încă integrarea platformei în OpenTable. Limitat la 10 încercări la fiecare 5 minute per cont.

**Adăugarea unui restaurant OpenTable**

`POST /appointments/opentable-restaurants`

| Câmp | Obligatoriu | Descriere |
| --- | --- | --- |
| `restaurant_id` | Da | ID-ul restaurantului verificat. |
| `restaurant_name` | Da | Numele afișat (o etichetă; de asemenea, modul în care AI-ul numește restaurantul). |
| `website_url` | Nu | Un URL http(s) afișat oaspeților atunci când AI-ul îi direcționează către restaurant. |

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

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

**Actualizarea unui restaurant OpenTable salvat**

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

Trimiteți oricare dintre `restaurant_name`, `is_active` (întrerupeți cu `false`) sau `website_url` (un șir gol îl șterge); câmpurile omise rămân neschimbate.

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

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

**Eliminarea unui restaurant 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"
```

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

Un `restaurantId` care nu se află în prezent în cont returnează `404` la actualizare.

### TheFork

Restaurantele TheFork sunt accesate prin intermediul acreditărilor de partener TheFork ale platformei, iar TheFork permite acestor acreditări să vadă doar restaurantele care au activat partenerul platformei în contul lor TheFork. Așadar, la fel ca în cazul Formitable și OpenTable, un singur apel de verificare este suficient: un ID de restaurant accesibil dovedește atât faptul că restaurantul există, cât și faptul că partenerul este activat pentru acesta. ID-ul este UUID-ul pe care TheFork îl atribuie restaurantului în TheFork Manager, trimis ca șir de caractere. Până când TheFork aprobă platforma ca partener și emite acreditările, apelul de verificare răspunde cu `503` — consultați [TheFork](../integrations/thefork.md) pentru a vedea ce înseamnă acest lucru în prezent.

**Verificarea unui restaurant TheFork**

`POST /appointments/thefork-restaurants/verify`

| Câmp | Obligatoriu | Descriere |
| --- | --- | --- |
| `restaurant_id` | Da | ID-ul restaurantului TheFork, un UUID precum `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" }'
```

**Răspuns** (`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` reprezintă dimensiunile grupului pe care restaurantul le acceptă online în următoarele 30 de zile. Un `404` sau `403` înseamnă că TheFork nu ne-a oferit un restaurant cu acel ID — fie ID-ul este greșit, fie partenerul platformei nu este încă activat pentru acel restaurant; un `400` înseamnă că ID-ul nu este un UUID. Limitat la 10 încercări la fiecare 5 minute per cont.

**Adăugarea unui restaurant TheFork**

`POST /appointments/thefork-restaurants`

| Câmp | Obligatoriu | Descriere |
| --- | --- | --- |
| `restaurant_id` | Da | ID-ul restaurantului verificat (UUID). |
| `restaurant_name` | Da | Numele afișat (o etichetă; este și modul în care AI-ul numește restaurantul). |
| `website_url` | Nu | Un URL http(s) afișat oaspeților atunci când AI-ul îi direcționează către restaurant. |

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

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

**Actualizarea unui restaurant TheFork salvat**

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

Trimiteți oricare dintre `restaurant_name`, `is_active` (întrerupeți cu `false`) sau `website_url` (un șir gol îl șterge); câmpurile omise rămân neschimbate.

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

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

**Elimină un restaurant 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"
```

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

Un `restaurantId` care nu se află în prezent în cont returnează `404` la actualizare sau ștergere.

### Trafft

Trafft este conectat o singură dată pentru întregul cont, nu per locație: o adresă de companie plus credențialele API din panoul de administrare Trafft (**Features & Integrations → API & Connectors**, parte din planul Business al Trafft). Apelul de conectare verifică acele credențiale în Trafft înainte de a stoca orice, astfel încât o adresă greșită, credențiale incorecte sau un plan fără acces API vor eșua aici, nu în timpul unei conversații cu un client. Secretul clientului (client secret) este stocat criptat și nu este returnat niciodată de niciun endpoint.

Toate cele trei apeluri de scriere (`POST`, `PUT`, `DELETE`) necesită permisiunea **edit** pentru Integrări; starea `GET` necesită permisiunea **view** pentru Integrări.

**Conectare Trafft**

`POST /appointments/trafft/connect`

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `subdomain` | Da | Adresa companiei — partea dinaintea `.admin.trafft.com` în URL-ul la care vă conectați, de ex. `acme`. O adresă completă este acceptată și redusă la aceeași valoare. |
| `client_id` | Da | ID-ul clientului din pagina API & Connectors a Trafft. |
| `client_secret` | Da | Secretul clientului (Client Secret) din aceeași pagină. Stocat criptat, nu este returnat niciodată. |
| `company_name` | Nu | O etichetă pentru propria listă. |

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

**Răspuns** (`200 OK`):

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

Numărătorile vin înapoi de la Trafft în timpul verificării — acestea sunt cea mai rapidă metodă de a confirma că acele credențiale indică spre contul dorit.

**Obținerea stării conexiunii**

`GET /appointments/trafft`

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

**Răspuns** (`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` înseamnă că nu este nimic configurat încă. Secretul clientului nu este inclus niciodată în acest răspuns.

**Actualizarea conexiunii**

`PUT /appointments/trafft`

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `is_active` | Nu | Setați `false` pentru a întrerupe — AI-ul nu mai efectuează rezervări în Trafft, dar conexiunea rămâne activă. `true` o reia. |
| `company_name` | Nu | Etichetă nouă. |

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

**Răspuns** (`200 OK`): același obiect de conexiune ca `GET` de mai sus.

**Deconectare Trafft**

`DELETE /appointments/trafft`

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

**Răspuns** (`200 OK`): `{ "success": true }`

Deconectarea elimină doar credențialele stocate. Programările deja existente în Trafft rămân intacte.

> **Formatul erorii pentru toate endpoint-urile Zenchef/Formitable/OpenTable/TheFork:** spre deosebire de restul acestei pagini, erorile de aici conțin statusul de două ori — o dată ca status HTTP și o dată ca `error_code` în corp — de exemplu `{ "success": false, "error": "Restaurant not found", "error_code": 404 }`. Gestionează-l la fel ca pe orice altă eroare: verifică `success`, citește `error` pentru mesaj.

---

## Erori API pentru programări

Endpoint-urile pentru programări returnează plicul standard de eroare:

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

| Status | Când apare pe un endpoint de programări |
|---|---|
| `400` | Un câmp obligatoriu lipsește sau este invalid — de exemplu, un `start_time` incorect, un `end_time` care nu este după `start_time`, o combinație de filtre invalidă, lipsa câmpurilor de actualizat sau o programare deja anulată. |
| `404` | Programarea, contactul sau tipul de eveniment nu a fost găsit. |
| `409` | Intervalul orar solicitat este deja ocupat (conflict de rezervare). |

Codurile partajate pe care orice endpoint le poate returna — `401`, `403` (planul dvs. nu include acces API), `429` (limită de rată) și `500` — sunt listate cu îndrumări pentru reîncercare în [Erori și Paginare](errors-and-pagination.md).

---

::: master-only
## Utilizați propriul client Google OAuth (ecran de consimțământ pentru Calendar)

Atunci când un cont se conectează la Google Calendar, fereastra de autentificare Google afișează numele proiectului clientului OAuth — în mod implicit, cel al platformei. O agenție își poate înregistra propriul client Google OAuth 2.0 în contul de agenție; din acel moment, conectarea calendarului pentru acel cont și pentru toate sub-conturile aferente se va face prin acel client, astfel încât ecranul de consimțământ va afișa numele și logo-ul agenției. Nimic altceva nu se schimbă: fluxul de conectare, sincronizarea bidirecțională și endpoint-urile pentru programări menționate mai sus funcționează exact ca înainte.

> **Doar pentru Google Calendar.** OAuth-ul pentru căsuța poștală Gmail pentru canalul de e-mail nu este afectat.

### De ce are nevoie clientul dumneavoastră mai întâi

1. **Un client OAuth 2.0** de tip Aplicație web în proiectul dumneavoastră Google Cloud, cu **Google Calendar API** activat pentru acel proiect.
2. **Fiecare intrare `redirect_uris`** (returnată de endpoint-urile de mai jos) adăugată în secțiunea URI-uri de redirecționare autorizate a clientului. Prima intrare este domeniul dumneavoastră `api.` verificat, dacă aveți unul — Google verifică doar un brand a cărui redirecționare se află pe un domeniu pe care îl dețineți — urmată de host-ul neutru al platformei ca rezervă utilizată până atunci.
3. **Ecranul de consimțământ** cu brandul dumneavoastră, domeniul dumneavoastră în secțiunea Domenii autorizate și cele două scopuri (scopes) pentru Calendar declarate (`scopes` în răspuns). Până când aplicația este publicată și verificată de Google, utilizatorii vor vedea un avertisment de aplicație neverificată, iar clientul este limitat la 100 de utilizatori.

### Salvarea clientului dumneavoastră

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

| Câmp | Obligatoriu | Descriere |
|---|---|---|
| `client_id` | Da | ID-ul clientului OAuth 2.0, care se termină în `.apps.googleusercontent.com`. |
| `client_secret` | Da | Secretul clientului. Verificat cu Google înainte de a fi stocat, apoi criptat. Nu este returnat niciodată de niciun endpoint. |

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

**Răspuns**

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

Un secret greșit sau un ID de client necunoscut este refuzat cu `400`, iar motivul oferit de Google apare în `error`; nimic nu este stocat.

### Citește sau elimină

`GET /account-config/google-oauth-client` returnează același rezumat în orice moment — `configured: false` plus `redirect_uris` și `scopes` înainte ca orice să fie salvat, astfel încât să poți configura mai întâi partea Google. `DELETE /account-config/google-oauth-client` elimină clientul: noile conexiuni revin la clientul platformei, iar calendarele care au fost conectate prin clientul eliminat trebuie reconectate, deoarece doar clientul care a emis o conexiune o poate reîmprospăta.

Membrii echipei au nevoie de **Integrări: vizualizare** pentru `GET` și **Integrări: editare** pentru `PUT` / `DELETE`.
:::

---

## Pașii următori

- [Contacte](contacts.md) — creează și caută contactele pentru care faci rezervări.
- [Mesaje și conversații](messages.md) — trimite unui contact o confirmare sau un memento.
- [Webhook-uri](webhooks.md) — primește notificări când programările se modifică.
