
# Cuộc hẹn

Appointments API cho phép bạn đặt lịch hẹn cho các liên hệ của mình dựa trên các loại sự kiện, sau đó lấy, liệt kê, cập nhật, hủy hoặc xóa chúng. API này cũng giải đáp câu hỏi xuất hiện đầu tiên trong hầu hết các quy trình đặt lịch — thời gian nào thực sự trống — và bao gồm cả khía cạnh lịch: liệt kê các Lịch Google bạn đã kết nối và nhập các sự kiện đã có sẵn trong đó. Khi kết nối Lịch Google đang hoạt động, sự kiện lịch tương ứng sẽ được tạo và tự động đồng bộ hóa ở chế độ nền. Các nhà hàng sử dụng Zenchef hoặc Formitable cho hệ thống đặt chỗ riêng của họ cũng có thể được xác minh và kết nối tại đây, để AI Agent đặt bàn thực tế thay vì các cuộc hẹn nội bộ.

Tất cả các đường dẫn trên trang này đều tương đối so với URL cơ sở `https://api.youraiconnector.com/v1`. Mọi yêu cầu đều cần khóa API của bạn — xem [Xác thực](authentication.md) để biết danh sách đầy đủ các cách gửi khóa. Các ví dụ bên dưới sử dụng tiêu đề `X-API-Key`, với một ví dụ cURL cũng hiển thị biểu mẫu truy vấn `?apiKey=`.

> **Sự kiện so với cuộc hẹn:** *Loại sự kiện* là định nghĩa về khung giờ có thể đặt lịch (loại cuộc họp, thời lượng, phòng họp). *Cuộc hẹn* là một phiên bản đã đặt của một loại sự kiện dành cho một liên hệ cụ thể. Bạn đặt một cuộc hẹn bằng cách tham chiếu đến liên hệ và loại sự kiện đó.

---

## Đối tượng cuộc hẹn

Mọi endpoint trả về một cuộc hẹn đều sử dụng cùng một cấu trúc:

| Trường | Mô tả |
|---|---|
| `id` | ID duy nhất của cuộc hẹn. |
| `contact_id` | ID của liên hệ đã đặt cuộc hẹn. |
| `event_id` | ID của loại sự kiện mà cuộc hẹn được đặt. |
| `status` | `Confirmed` hoặc `Canceled`. |
| `start_time` | Thời gian bắt đầu cuộc hẹn, theo định dạng ISO 8601 ở UTC. |
| `end_time` | Thời gian kết thúc cuộc hẹn, theo định dạng ISO 8601 ở UTC. |
| `created_at` | Thời điểm cuộc hẹn được tạo. |
| `last_modified_at` | Thời điểm cuộc hẹn được thay đổi lần cuối. |
| `room_name` | Phòng hoặc tài nguyên mà cuộc hẹn được đặt, khi loại sự kiện sử dụng phòng. |
| `description` | Mô tả tự do về cuộc hẹn. |
| `summary` | Tóm tắt hoặc tiêu đề ngắn gọn. |
| `cancelation_reason` | Lý do được cung cấp khi cuộc hẹn bị hủy, nếu có. |
| `google_calendar_event_id` | ID của sự kiện Google Calendar được liên kết. Được thiết lập sau khi đồng bộ hóa lịch hoàn tất; `null` khi không có lịch nào được kết nối hoặc trong khi quá trình đồng bộ hóa vẫn đang diễn ra. |
| `calendar_synced` | `true` khi cuộc hẹn được liên kết với một sự kiện lịch. |
| `imported` | `true` khi cuộc hẹn được nhập từ một lịch bên ngoài thay vì đặt trực tiếp. |
| `is_recurring` | `true` khi cuộc hẹn là một phần của chuỗi lặp lại. |
| `recurrence_frequency` | Tần suất lặp lại của cuộc hẹn, khi là chuỗi lặp lại. |
| `recurring_event_id` | ID của chuỗi lặp lại mà cuộc hẹn này thuộc về. |
| `recurring_interval` | Khoảng thời gian giữa các lần lặp lại, khi là chuỗi lặp lại. |
| `recurring_sequence` | Vị trí của cuộc hẹn này trong chuỗi lặp lại của nó. |
| `end_after_x_occurrences` | Số lần xuất hiện sau đó chuỗi lặp lại kết thúc. |
| `booking_provider` | Hệ thống nguồn mà đặt chỗ đến từ đó, khi được đặt thông qua nhà cung cấp dịch vụ đặt chỗ được kết nối. |

> **Về đồng bộ hóa lịch:** Ngay sau khi bạn đặt hoặc thay đổi một cuộc hẹn, `google_calendar_event_id` có thể vẫn là `null` và `calendar_synced` có thể là `false` vì quá trình đồng bộ hóa chạy ở chế độ nền ngay sau đó. Hãy lấy lại cuộc hẹn một lúc sau để xem các trường lịch đã được điền.

---

## Tìm các khung giờ khả dụng

`GET /appointments/available-slots`

Trả về các thời điểm thực sự trống trên một loại sự kiện giữa hai thời điểm. Đây thường là lệnh gọi **đầu tiên** trong quy trình đặt lịch: hiển thị các khung giờ này, để người dùng chọn một khung giờ, sau đó gửi thời gian đã chọn tới [Đặt lịch hẹn](#book-an-appointment).

Câu trả lời đã tính đến giờ mở cửa và thời lượng khung giờ của chính loại sự kiện đó, các phòng, các cuộc hẹn bạn đã đặt trên đó và mọi thứ bị chặn trên các Lịch Google được kết nối — vì vậy một khung giờ được trả về ở đây là khung giờ bạn có thể đặt.

| Tham số truy vấn | Bắt buộc | Mô tả |
|---|---|---|
| `event_id` | Có | Loại sự kiện cần kiểm tra. Phải thuộc về tài khoản của bạn. |
| `start_time` | Có | Thời điểm bắt đầu của khoảng thời gian bạn muốn tìm khung giờ, định dạng ngày-giờ ISO 8601. |
| `end_time` | Có | Thời điểm kết thúc của khoảng thời gian, định dạng ngày-giờ ISO 8601. Toàn bộ ngày kết thúc đều được bao gồm. |

Kết quả trả về được nhóm theo ngày — và khi loại sự kiện sử dụng phòng, mỗi nhóm sẽ tương ứng với một phòng mỗi ngày:

| Trường | Mô tả |
|---|---|
| `date` | Ngày mà nhóm bao gồm, được viết dưới dạng `DD/MM/YYYY`. |
| `day` | Tên ngày trong tuần ở dạng chữ thường, ví dụ `monday`. |
| `room_name` | Phòng hoặc tài nguyên mà nhóm này thuộc về, khi loại sự kiện sử dụng phòng. |
| `available_slots` | Các khối thời gian có thể đặt vào ngày đó, sắp xếp theo thứ tự sớm nhất trước. |

Mỗi mục trong `available_slots` có:

| Trường | Mô tả |
|---|---|
| `start_time` | Thời gian bắt đầu khối dưới dạng `HH:mm`. |
| `end_time` | Thời gian kết thúc khối dưới dạng `HH:mm`. |
| `available` | `true` — chỉ thời gian trống mới được trả về. |
| `spots_left` | Số lượng đặt chỗ còn có thể thực hiện trong khối này. Chỉ xuất hiện trên các loại sự kiện cho phép nhiều hơn một đặt chỗ mỗi khung giờ. |

> **Thời gian là thời gian địa phương của loại sự kiện, không phải UTC.** `date`, `start_time` và `end_time` là các giá trị đồng hồ thực tế theo múi giờ của chính loại sự kiện đó (múi giờ ghi đè của nó, hoặc múi giờ tài khoản của bạn nếu không có ghi đè). [Đặt lịch hẹn](#book-an-appointment) yêu cầu một thời điểm UTC theo chuẩn ISO 8601, vì vậy hãy chuyển đổi khung giờ bạn đã chọn trước khi gửi đi.

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/appointments/available-slots?event_id=event_xyz789&start_time=2026-06-15T00:00:00.000Z&end_time=2026-06-19T00:00:00.000Z" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const params = new URLSearchParams({
  event_id: "event_xyz789",
  start_time: "2026-06-15T00:00:00.000Z",
  end_time: "2026-06-19T00:00:00.000Z",
});
const res = await fetch(
  `https://api.youraiconnector.com/v1/appointments/available-slots?${params}`,
  { headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.data);
```

**Python**

```python
import requests

res = requests.get(
    "https://api.youraiconnector.com/v1/appointments/available-slots",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={
        "event_id": "event_xyz789",
        "start_time": "2026-06-15T00:00:00.000Z",
        "end_time": "2026-06-19T00:00:00.000Z",
    },
)
print(res.json()["data"])
```

**Phản hồi** (`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 }
      ]
    }
  ]
}
```

Một ngày không có thời gian trống sẽ đơn giản là không xuất hiện. Thiếu `event_id`, `start_time` hoặc `end_time` sẽ trả về `400`; một loại sự kiện không thuộc tài khoản của bạn sẽ trả về `404`.

---

## Đặt một cuộc hẹn

`POST /appointments`

Đặt một cuộc hẹn mới cho một liên hệ trên một trong các loại sự kiện của bạn. Thời gian kết thúc được tính toán tự động từ thời lượng khung giờ của loại sự kiện.

Việc đặt chỗ được kiểm tra xung đột: nếu khung giờ được yêu cầu trùng lặp với một cuộc hẹn đã xác nhận hiện có trên cùng một loại sự kiện, yêu cầu sẽ thất bại với mã `409` và không có gì được tạo.

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `contact_id` | Có | ID của liên hệ cần đặt lịch. Phải thuộc về tài khoản của bạn. |
| `event_id` | Có | ID của loại sự kiện cần đặt lịch. Phải thuộc về tài khoản của bạn. |
| `start_time` | Có | Thời gian bắt đầu mong muốn dưới dạng ngày-giờ ISO 8601. |
| `room_name` | Không | Tên phòng hoặc tài nguyên, khi loại sự kiện sử dụng phòng. |

**cURL** (sử dụng biểu mẫu truy vấn `?apiKey=`)

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

**JavaScript**

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

**Python**

```python
import requests

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

**Phản hồi** (`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
  }
}
```

---

## Lấy thông tin cuộc hẹn

`GET /appointments/{appointmentId}`

Trả về một cuộc hẹn duy nhất theo ID của nó, bao gồm trạng thái đồng bộ hóa lịch của cuộc hẹn đó.

**cURL**

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

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678",
  { headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.appointment);
```

**Python**

```python
import requests

res = requests.get(
    "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["appointment"])
```

**Phản hồi** (`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
  }
}
```

---

## Liệt kê các cuộc hẹn

`GET /appointments`

Liệt kê các cuộc hẹn cho tài khoản của bạn, hiển thị cuộc hẹn mới nhất trước, với tính năng phân trang dựa trên con trỏ.

| Tham số truy vấn | Bắt buộc | Mô tả |
|---|---|---|
| `contact_id` | Không | Chỉ trả về các cuộc hẹn cho liên hệ này. Danh sách được lọc theo liên hệ chỉ bao gồm **các cuộc hẹn đã xác nhận** |
| `date` | Không | Chỉ trả về các cuộc hẹn trong ngày dương lịch này (`YYYY-MM-DD`). **Yêu cầu `contact_id`.** |
| `status` | Không | Lọc theo `Confirmed` hoặc `Canceled`. Chỉ khả dụng **khi không có** `contact_id`. |
| `limit` | Không | Kích thước trang, một số nguyên từ 1 đến 100. Mặc định là `50`. |
| `cursor` | Không | Giá trị `next_cursor` từ phản hồi trước đó. |

Một vài quy tắc cần ghi nhớ:

- **Không có bộ lọc**, bạn sẽ nhận được mọi cuộc hẹn trên tài khoản, theo từng trang.
- **Theo liên hệ** — đặt `contact_id` để xem các cuộc hẹn đã xác nhận của một liên hệ. Bạn có thể thu hẹp phạm vi này xuống một ngày cụ thể bằng cách truyền thêm `date`.
- **Theo trạng thái** — đặt `status` (không có `contact_id`) để chỉ liệt kê các cuộc hẹn `Confirmed` hoặc chỉ `Canceled` trên toàn bộ tài khoản.
- Bộ lọc `date` khi không có `contact_id`, hoặc `status=Canceled` cùng với `contact_id`, sẽ trả về `400`.

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/appointments?contact_id=contact_abc123&date=2026-06-15" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const params = new URLSearchParams({
  contact_id: "contact_abc123",
  date: "2026-06-15",
});
const res = await fetch(
  `https://api.youraiconnector.com/v1/appointments?${params}`,
  { headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.appointments, data.next_cursor);
```

**Python**

```python
import requests

res = requests.get(
    "https://api.youraiconnector.com/v1/appointments",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"contact_id": "contact_abc123", "date": "2026-06-15"},
)
data = res.json()
print(data["appointments"], data["next_cursor"])
```

**Phản hồi** (`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
}
```

Để chuyển trang kết quả, hãy truyền `next_cursor` từ một phản hồi làm `cursor` cho yêu cầu tiếp theo. Tiếp tục cho đến khi `next_cursor` là `null`. Xem [Lỗi & Phân trang](errors-and-pagination.md) để biết mẫu phân trang dùng chung.

---

## Cập nhật cuộc hẹn

`PUT /appointments/{appointmentId}`

Lên lịch lại cuộc hẹn hoặc thay đổi chi tiết cuộc hẹn. Chỉ gửi các trường bạn muốn thay đổi — bắt buộc phải có ít nhất một trường. Thời gian bắt đầu và kết thúc kết hợp phải theo đúng thứ tự thời gian (`end_time` phải sau `start_time`). Các thay đổi sẽ được tự động đồng bộ hóa với sự kiện lịch được liên kết.

| Trường | Mô tả |
|---|---|
| `start_time` | Thời gian bắt đầu mới, định dạng ISO 8601. |
| `end_time` | Thời gian kết thúc mới, định dạng ISO 8601. Phải sau thời gian bắt đầu. |
| `room_name` | Tên phòng hoặc tài nguyên mới. |
| `description` | Mô tả mới, hoặc `null` để xóa nội dung này. |
| `summary` | Tóm tắt mới, hoặc `null` để xóa nội dung này. |

**cURL**

```bash
curl -X PUT "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "start_time": "2026-06-16T10:00:00.000Z",
    "end_time": "2026-06-16T10:30:00.000Z"
  }'
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678",
  {
    method: "PUT",
    headers: {
      "X-API-Key": "YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      start_time: "2026-06-16T10:00:00.000Z",
      end_time: "2026-06-16T10:30:00.000Z",
    }),
  }
);
const data = await res.json();
console.log(data.appointment);
```

**Python**

```python
import requests

res = requests.put(
    "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={
        "start_time": "2026-06-16T10:00:00.000Z",
        "end_time": "2026-06-16T10:30:00.000Z",
    },
)
print(res.json()["appointment"])
```

**Phản hồi** (`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
  }
}
```

---

## Hủy cuộc hẹn

`POST /appointments/{appointmentId}/cancel`

Hủy một cuộc hẹn đã xác nhận, tùy chọn ghi lại lý do. Cuộc hẹn vẫn nằm trong tài khoản của bạn với trạng thái `Canceled`, và sự kiện lịch được liên kết sẽ tự động bị xóa trong nền. Việc hủy một cuộc hẹn đã bị hủy sẽ trả về mã `400`.

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `cancellation_reason` | Không | Lý do hủy, được lưu trữ trên cuộc hẹn. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678/cancel" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cancellation_reason": "Client asked to reschedule next month"
  }'
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678/cancel",
  {
    method: "POST",
    headers: {
      "X-API-Key": "YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      cancellation_reason: "Client asked to reschedule next month",
    }),
  }
);
const data = await res.json();
console.log(data.success);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678/cancel",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={"cancellation_reason": "Client asked to reschedule next month"},
)
print(res.json()["success"])
```

**Phản hồi** (`200 OK`):

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

---

## Xóa cuộc hẹn

`DELETE /appointments/{appointmentId}`

Xóa vĩnh viễn một cuộc hẹn và các tham chiếu của nó. Nếu bạn chỉ muốn hủy đặt chỗ trong khi vẫn giữ lại hồ sơ, hãy sử dụng [hủy](#cancel-an-appointment) thay thế.

**cURL**

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

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678",
  { method: "DELETE", headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.success);
```

**Python**

```python
import requests

res = requests.delete(
    "https://api.youraiconnector.com/v1/appointments/aBcD1234eFgH5678",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["success"])
```

**Phản hồi** (`200 OK`):

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

---

## Liệt kê các Lịch Google đã kết nối của bạn

`GET /appointments/google-calendars`

Trả về các Lịch Google khả dụng trên tài khoản này, trực tiếp từ Google — hữu ích để hiển thị cho chủ tài khoản bộ chọn lịch để nhập từ bên dưới, hoặc chỉ để xác nhận kết nối đang hoạt động.

Điều này chỉ hoạt động sau khi tài khoản đã kết nối Google Calendar (Cài đặt → Tích hợp) với ít nhất quyền truy cập đọc. Nếu chưa, hoặc quyền truy cập được cấp không còn bao gồm phạm vi đọc lịch, bạn sẽ nhận được một `400` yêu cầu bạn (kết nối lại) nó.

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/appointments/google-calendars" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/appointments/google-calendars", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.data);
```

**Python**

```python
import requests

res = requests.get(
    "https://api.youraiconnector.com/v1/appointments/google-calendars",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["data"])
```

**Phản hồi** (`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"
    }
  ]
}
```

Mỗi mục nhập là hình dạng [`CalendarListEntry`](https://developers.google.com/calendar/api/v3/reference/calendarList) của riêng Google, vì vậy tên trường tuân theo `camelCase` của Google, không phải `snake_case` thông thường của API này — đó là dữ liệu của Google được chuyển qua nguyên trạng, không phải của chúng tôi. Kết nối bị thiếu hoặc bị thu hồi sẽ trả về `400` kèm theo lỗi giải thích rằng Google Calendar cần được (kết nối lại).

---

## Nhập sự kiện từ Google Calendar

`POST /appointments/import-calendar-events`

Kéo các sự kiện đã có sẵn trong Google Calendar được kết nối của chiến dịch hoặc AI Agent và biến chúng thành các cuộc hẹn — hữu ích cho lần đầu tiên bạn kết nối một lịch đã có sẵn các đặt chỗ. Quá trình này có thể mất một chút thời gian (mỗi sự kiện đều trải qua quá trình trích xuất để xác định xem nó dành cho ai), vì vậy nó không bao giờ chạy trực tiếp: yêu cầu sẽ xếp hàng một tác vụ nền và trả lại cho bạn một `job_id` để thăm dò.

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `campaign_id` | Một trong hai | Chiến dịch có lịch được kết nối để nhập từ đó. |
| `agent_id` | Một trong hai | AI Agent có lịch được kết nối để nhập từ đó. |
| `identifier` | Có | `"EMAIL"` hoặc `"PHONE_NUMBER"` — thông tin liên hệ nào cần trích xuất từ mỗi sự kiện lịch để khớp hoặc tạo liên hệ mà nó thuộc về. |

Gửi chính xác một trong `campaign_id` / `agent_id`, không bao giờ gửi cả hai và không bao giờ không gửi cái nào — bất kỳ sự kết hợp nào khác đều trả về `400`. Bất kỳ cái nào bạn gửi đều phải thuộc về tài khoản của bạn, nếu không bạn sẽ nhận được `404`.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/appointments/import-calendar-events?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agent_abc123",
    "identifier": "EMAIL"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/appointments/import-calendar-events", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    agent_id: "agent_abc123",
    identifier: "EMAIL",
  }),
});
const data = await res.json();
console.log(data.job_id);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/appointments/import-calendar-events",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={"agent_id": "agent_abc123", "identifier": "EMAIL"},
)
print(res.json()["job_id"])
```

**Phản hồi** (`202 Accepted`):

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

`campaign_id` và `agent_id` phản hồi lại bất kỳ cái nào bạn đã gửi; cái còn lại luôn là `null`.

### Thăm dò tác vụ nhập

`GET /appointments/import-calendar-events/{jobId}`

```bash
curl "https://api.youraiconnector.com/v1/appointments/import-calendar-events/jK9mQ2xR7pL4wN1t" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Phản hồi** (`200 OK`):

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

| `status` | Ý nghĩa |
|---|---|
| `queued` | Chưa được nhận. Hãy tiếp tục thăm dò. |
| `processing` | Quá trình nhập đang chạy. Hãy tiếp tục thăm dò. |
| `completed` | Hoàn tất — `message` có một bản tóm tắt ngắn gọn dễ đọc. |
| `failed` | Đã xảy ra lỗi — `error` có lý do cụ thể. |

`GET` trên một `jobId` không tồn tại (hoặc thuộc về một tài khoản khác) sẽ trả về `404`.

---

## Tích hợp đặt chỗ nhà hàng (Zenchef / Formitable)

Zenchef và Formitable là các hệ thống đặt chỗ nhà hàng mà AI Agent của bạn có thể sử dụng để đặt bàn thực tế. Mỗi hệ thống đều có một **tiện ích đặt chỗ công khai, không cần xác thực** (`https://api.youraiconnector.com/v1/zenchef-widget/...` và `https://api.youraiconnector.com/v1/formitable-widget/...`) hiển thị trong cuộc trò chuyện cho thực khách — các đường dẫn tiện ích đó là các trang HTML thuần túy dành cho trình duyệt, không phải điểm cuối API JSON, vì vậy chúng không được ghi lại ở đây. Những gì theo sau là các điểm cuối quản lý tài khoản: xác minh ID nhà hàng thuộc về chủ tài khoản, sau đó thêm, cập nhật hoặc xóa nó.

### Zenchef

Việc kết nối nhà hàng Zenchef là quy trình xác minh hai bước, để chủ tài khoản chứng minh rằng họ thực sự điều hành nhà hàng đó trước khi nó được liên kết vào bot: trước tiên hãy kiểm tra xem ID có tồn tại hay không (mà không tiết lộ tên), sau đó yêu cầu họ tự nhập tên nhà hàng và xác minh xem nó có khớp hay không.

**Bước 1 — Kiểm tra xem ID nhà hàng có tồn tại không**

`POST /appointments/zenchef-restaurants/check`

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `restaurant_id` | Có | ID nhà hàng Zenchef cần kiểm tra. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/appointments/zenchef-restaurants/check?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restaurant_id": "12345" }'
```

**Phản hồi** (`200 OK`):

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

`exists: false` nghĩa là không có nhà hàng Zenchef nào có ID đó — không cần làm gì thêm. Giới hạn tốc độ là 10 lần kiểm tra mỗi 5 phút cho mỗi tài khoản; nếu vượt quá sẽ trả về `429`.

**Bước 2 — Xác minh tên nhà hàng**

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

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `restaurant_id` | Có | ID nhà hàng Zenchef từ bước 1. |
| `user_input_name` | Có | Tên mà chủ tài khoản đã nhập — được so sánh với tên thực của nhà hàng trên Zenchef (không phân biệt chữ hoa/thường và khoảng trắng). |

```bash
curl -X POST "https://api.youraiconnector.com/v1/appointments/zenchef-restaurants/verify-name?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restaurant_id": "12345", "user_input_name": "The Blue Door Bistro" }'
```

**Phản hồi** (`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` nghĩa là tên không khớp — `restaurantDetails` bị bỏ qua, hãy yêu cầu chủ tài khoản thử lại. Giới hạn tốc độ là 3 lần thử mỗi 5 phút (chặt chẽ hơn so với kiểm tra sự tồn tại, vì đây là bước chứng minh thực tế). Một `restaurant_id` không còn tồn tại trên Zenchef sẽ trả về `404`.

**Bước 3 — Lưu nhà hàng**

`POST /appointments/zenchef-restaurants`

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `restaurant_id` | Có | 1–64 ký tự, chữ cái/số/dấu gạch dưới/dấu gạch ngang. |
| `restaurant_name` | Có | Tên nhà hàng đã xác minh từ bước 2. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/appointments/zenchef-restaurants?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restaurant_id": "12345", "restaurant_name": "The Blue Door Bistro" }'
```

**Phản hồi** (`201 Created`):

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

**Cập nhật nhà hàng Zenchef đã lưu**

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

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `restaurant_name` | Không | Tên hiển thị mới. |
| `is_active` | Không | Đặt `false` để ngăn bot đặt chỗ tại nhà hàng này mà không cần xóa nó. |

```bash
curl -X PUT "https://api.youraiconnector.com/v1/appointments/zenchef-restaurants/12345" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "is_active": false }'
```

**Phản hồi** (`200 OK`): có cùng cấu trúc với phản hồi lưu ở trên.

**Xóa một nhà hàng Zenchef**

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

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/appointments/zenchef-restaurants/12345" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Phản hồi** (`200 OK`): `{ "success": true, "data": { "restaurantId": "12345" } }`

Một `restaurantId` hiện không có trong tài khoản sẽ trả về `404` khi cập nhật hoặc xóa.

### Formitable

Formitable không cần xác minh tên hai bước như Zenchef — ID nhà hàng của nó đã được phân định theo doanh nghiệp, vì vậy chỉ cần một cuộc gọi xác minh là đủ. Nó cũng có tính năng tra cứu chi tiết được sử dụng để lưu vào bộ nhớ đệm URL trang web của nhà hàng trong quá trình thiết lập.

**Xác minh ID nhà hàng**

`POST /appointments/formitable-restaurants/verify`

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `restaurant_id` | Có | ID nhà hàng Formitable. |
| `language` | Không | Thẻ ngôn ngữ cho yêu cầu thăm dò. Mặc định là `"nl"`. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/appointments/formitable-restaurants/verify?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "restaurant_id": "the-blue-door", "language": "en" }'
```

**Phản hồi** (`200 OK`):

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

Một `restaurant_id` mà Formitable không nhận dạng được sẽ trả về `404`. Giới hạn tốc độ là 10 lần thử mỗi 5 phút cho mỗi tài khoản.

**Lấy chi tiết nhà hàng**

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

Tìm nạp hồ sơ công khai của nhà hàng từ Formitable, bao gồm cả trang web của nó — được sử dụng để lưu vào bộ nhớ đệm URL trang web trong khi thiết lập nhà hàng. `language` là tham số truy vấn tùy chọn, mặc định là `"en"`.

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

**Phản hồi** (`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"
  }
}
```

**Lưu nhà hàng**

`POST /appointments/formitable-restaurants`

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `restaurant_id` | Có | 1–64 ký tự, chữ cái/số/dấu gạch dưới/dấu gạch ngang. |
| `restaurant_name` | Có | Tên hiển thị. |
| `language` | Có | Thẻ ngôn ngữ ISO, ví dụ: `"en"` hoặc `"en-GB"`. |
| `website_url` | Không | Trang web của nhà hàng, từ phần tra cứu chi tiết ở trên. Phải là `http(s)://`. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/appointments/formitable-restaurants?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "restaurant_id": "the-blue-door",
    "restaurant_name": "The Blue Door Bistro",
    "language": "en",
    "website_url": "https://thebluedoorbistro.com"
  }'
```

**Phản hồi** (`201 Created`): `{ "success": true, "data": { "restaurantId": "the-blue-door" } }`

**Cập nhật nhà hàng Formitable đã lưu**

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

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `restaurant_name` | Không | Tên hiển thị mới. |
| `language` | Không | Thẻ ngôn ngữ ISO mới. |
| `is_active` | Không | Đặt `false` để ngăn bot đặt chỗ tại nhà hàng này mà không cần xóa nó. |
| `website_url` | Không | URL trang web mới. |

```bash
curl -X PUT "https://api.youraiconnector.com/v1/appointments/formitable-restaurants/the-blue-door" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "is_active": false }'
```

**Phản hồi** (`200 OK`): có cùng cấu trúc với phản hồi lưu ở trên.

**Xóa nhà hàng Formitable**

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

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/appointments/formitable-restaurants/the-blue-door" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Phản hồi** (`200 OK`): `{ "success": true, "data": { "restaurantId": "the-blue-door" } }`

Một `restaurantId` hiện không có trong tài khoản sẽ trả về `404` khi cập nhật hoặc xóa.

> **Cấu trúc lỗi trên tất cả các endpoint Zenchef/Formitable:** không giống như phần còn lại của trang này, các lỗi ở đây mang trạng thái của chúng hai lần — một lần là trạng thái HTTP và một lần là `error_code` trong phần thân — ví dụ: `{ "success": false, "error": "Restaurant not found", "error_code": 404 }`. Hãy xử lý theo cách tương tự như bất kỳ lỗi nào khác: kiểm tra `success`, đọc `error` để biết thông báo lỗi.

---

## Các lỗi của Appointments API

Các điểm cuối (endpoint) của cuộc hẹn trả về phong bì lỗi tiêu chuẩn:

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

| Trạng thái | Khi nào lỗi xảy ra trên một điểm cuối cuộc hẹn |
|---|---|
| `400` | Một trường bắt buộc bị thiếu hoặc không hợp lệ — ví dụ: `start_time` sai, `end_time` không nằm sau `start_time`, kết hợp bộ lọc không hợp lệ, không có trường nào để cập nhật hoặc cuộc hẹn đã bị hủy. |
| `404` | Không tìm thấy cuộc hẹn, liên hệ hoặc loại sự kiện. |
| `409` | Khung thời gian được yêu cầu đã bị chiếm dụng (xung đột đặt lịch). |

Các mã chung mà mọi điểm cuối có thể trả về — `401`, `403` (gói của bạn không bao gồm quyền truy cập API), `429` (giới hạn tốc độ) và `500` — được liệt kê cùng với hướng dẫn thử lại trong [Lỗi & Phân trang](errors-and-pagination.md).

---

## Các bước tiếp theo

- [Danh bạ](contacts.md) — tạo và tra cứu các liên hệ mà bạn đặt lịch.
- [Tin nhắn & Cuộc hội thoại](messages.md) — gửi xác nhận hoặc lời nhắc cho liên hệ.
- [Webhook](webhooks.md) — nhận thông báo khi các cuộc hẹn thay đổi.
