{
  "openapi": "3.1.0",
  "info": {
    "title": "iClinic Integration API",
    "version": "1.1.0",
    "summary": "Read a clinic's patients, appointments, receipts, therapy plans and encounters (visits) with one fixed API key.",
    "description": "The iClinic Integration API is **read-only**. External software (for example Tele-sale) uses it to read a clinic's data: encounters (visits) by date range, and patients, appointments, receipts and therapy plans by update time, for incremental sync.\n\nAuthenticate with a fixed API key in the format `ick_…`. A clinic admin creates the key in **Admin → External connections (API)** in the iClinic staff app. Each key can read only the data of the clinic that created it.\n\n## Scopes\n\nEach key carries one or more read scopes, which the clinic admin selects when creating the key. A request to a path that needs a scope the key does not have returns `403 INSUFFICIENT_SCOPE`.\n\n| Scope | Paths |\n| --- | --- |\n| `encounters:read` | `/encounters` |\n| `patients:read` | `/patients`, `/patients/{id}` |\n| `appointments:read` | `/appointments`, `/appointments/{id}` |\n| `receipts:read` | `/receipts`, `/receipts/{id}` |\n| `therapy:read` | `/therapy-plans`, `/therapy-plans/{id}` |\n\nKeys created before version 1.1.0 have only `encounters:read`.\n\n## Incremental sync\n\nThe four new resources (patients, appointments, receipts and therapy plans) share one paging method, with these parameters: `updatedSince` (return only records updated since that time), `cursor` (an opaque cursor taken from `nextCursor` of the previous page) and `limit`. Results are sorted by `(updatedAt, id)` ascending. Paging uses a key, not a page number, so data that changes between two calls does not cause unchanged records to be skipped or repeated.\n\nA deleted or merged record is returned as a **tombstone**: `deletedAt` has a value and the record holds no personal data. Delete the matching record on your side.\n\nDelivery is *at least once*: `updatedSince` includes records whose `updatedAt` is exactly equal to that value, so the same record can arrive again. Overwrite the record that has the same `id` instead of inserting a new one.\n\n## Personal data\n\nPatients, appointments, receipts and therapy plans are personal data, and some of it is health data. Create keys only with the scopes you need, and use the data only for the purpose the patient agreed to. Responses always carry `Cache-Control: no-store`.\n\n## Limits\n\nThe rate limit is 60 requests per minute per key. Above it, the API returns `429` with a `Retry-After` header. If you send a wrong key 5 times in 10 minutes from the same IP address, that IP address is blocked for 15 minutes.\n\n## Versioning\n\nChanges that only add fields, paths or enum values do not change the version number in the `/v1` path. Ignore unknown fields in responses.",
    "contact": {
      "name": "iClinic",
      "url": "https://iclinic.phongkhamhieploi.vn/#demo"
    }
  },
  "servers": [
    {
      "url": "https://iclinic.phongkhamhieploi.vn/api/integration/v1",
      "description": "Production"
    }
  ],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Encounters", "description": "A clinic's encounters (visits), looked up by date range." },
    {
      "name": "Patients",
      "description": "Patients (minimal administrative record), synced by update time."
    },
    {
      "name": "Appointments",
      "description": "Appointments, synced by update time."
    },
    {
      "name": "Receipts",
      "description": "Receipts and their line items, synced by update time."
    },
    {
      "name": "Therapy plans",
      "description": "Therapy plans (packages of several sessions) and their progress, synced by update time."
    }
  ],
  "paths": {
    "/encounters": {
      "get": {
        "operationId": "listEncounters",
        "tags": ["Encounters"],
        "summary": "List encounters by date range",
        "description": "Returns the encounters whose `visitDate` is within `[from, to]`, newest first. To read them all, increase `page` until you have received `total` encounters.\n\nBy default, `CANCELLED` encounters are **not** included. To receive cancelled encounters too, call with `status=all` or `status=CANCELLED`.\n\nRequires the `encounters:read` scope.",
        "security": [{ "bearerAuth": ["encounters:read"] }],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "Start date of the visits to return (clinic time), `YYYY-MM-DD`.",
            "schema": { "type": "string", "format": "date" },
            "example": "2026-09-14"
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "End date of the visits to return, inclusive. Defaults to today. The range between `from` and `to` is at most 31 days.",
            "schema": { "type": "string", "format": "date" },
            "example": "2026-09-19"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by status. Omit it to get every status except `CANCELLED`. `all` returns every status, including `CANCELLED`. The value is case-insensitive.",
            "schema": {
              "type": "string",
              "enum": ["NEW", "ONGOING", "FINISHED", "REOPENED", "LOCKED", "CANCELLED", "all"]
            },
            "example": "all"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0.",
            "schema": { "type": "integer", "minimum": 0, "default": 0 },
            "example": 0
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "description": "Number of encounters per page.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 200 },
            "example": 200
          }
        ],
        "responses": {
          "200": {
            "description": "The list of encounters.",
            "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EncounterList" },
                "examples": {
                  "ongoing": {
                    "summary": "An ongoing encounter",
                    "value": {
                      "requestId": "req_7c1f3a",
                      "from": "2026-09-14",
                      "to": "2026-09-19",
                      "page": 0,
                      "pageSize": 200,
                      "total": 1,
                      "encounters": [
                        {
                          "id": "BA-000123",
                          "encounterUuid": "3f0c9a52-6d1b-4e7a-9b1e-2a4c8d5e7f10",
                          "patientId": "5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10",
                          "patientName": "Nguyễn Văn A",
                          "phone": "0900000001",
                          "visitDate": "2026-09-19",
                          "checkedInAt": "2026-09-19T02:14:05.000Z",
                          "completedAt": null,
                          "status": "ONGOING",
                          "patientType": "OLD",
                          "diagnosis": "Tăng huyết áp",
                          "icd10Code": "I10",
                          "followUpDate": "2026-10-19",
                          "updatedAt": "2026-09-19T02:20:11.482Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadEncounterQuery" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/patients": {
      "get": {
        "operationId": "listPatients",
        "tags": ["Patients"],
        "summary": "Sync patients by update time",
        "description": "Returns patients ordered by `(updatedAt, id)` ascending, starting from `updatedSince` (if provided) and after `cursor` (if provided). A patient merged into another patient record is returned as a tombstone with `mergedIntoId`.\n\nIf the clinic has no phone number for the patient, `phone` is `null`. The response does not include the citizen ID card number (CCCD), email, address, allergies or the full date of birth.\n\nRequires the `patients:read` scope.",
        "security": [{ "bearerAuth": ["patients:read"] }],
        "parameters": [
          { "$ref": "#/components/parameters/UpdatedSince" },
          { "$ref": "#/components/parameters/Cursor" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": {
            "description": "One page of patients.",
            "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PatientList" },
                "examples": {
                  "page": {
                    "summary": "A patient and a tombstone",
                    "value": {
                      "requestId": "req_7c1f3a",
                      "patients": [
                        {
                          "id": "5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10",
                          "code": "BN-000412",
                          "name": "Nguyễn Văn A",
                          "phone": "0900000001",
                          "gender": "MALE",
                          "birthYear": 1984,
                          "patientType": "OLD",
                          "status": "ACTIVE",
                          "registeredDate": "2026-03-02",
                          "createdAt": "2026-03-02T01:30:00.000Z",
                          "updatedAt": "2026-10-04T08:12:45.120Z",
                          "deletedAt": null
                        },
                        {
                          "id": "c9a1d7e2-0b34-4c6f-8a51-6e7f3b2d9c08",
                          "deletedAt": "2026-10-05T03:00:00.000Z",
                          "updatedAt": "2026-10-05T03:00:00.000Z",
                          "mergedIntoId": "5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10"
                        }
                      ],
                      "nextCursor": "djF8cGF0aWVudHN8MjAyNi0xMC0wNVQwMzowMDowMC4wMDBafGM5YTFkN2UyLTBiMzQtNGM2Zi04YTUxLTZlN2YzYjJkOWMwOA"
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadListQuery" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/patients/{id}": {
      "get": {
        "operationId": "getPatient",
        "tags": ["Patients"],
        "summary": "Get a patient by id",
        "description": "Returns the patient, or a tombstone if the record was merged. An `id` that belongs to another clinic and an `id` that does not exist both return the same `404 NOT_FOUND`.\n\nRequires the `patients:read` scope.",
        "security": [{ "bearerAuth": ["patients:read"] }],
        "parameters": [{ "$ref": "#/components/parameters/PatientId" }],
        "responses": {
          "200": {
            "description": "The patient.",
            "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PatientDetail" },
                "examples": {
                  "patient": {
                    "summary": "A patient",
                    "value": {
                      "requestId": "req_7c1f3a",
                      "patient": {
                        "id": "5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10",
                        "code": "BN-000412",
                        "name": "Nguyễn Văn A",
                        "phone": "0900000001",
                        "gender": "MALE",
                        "birthYear": 1984,
                        "patientType": "OLD",
                        "status": "ACTIVE",
                        "registeredDate": "2026-03-02",
                        "createdAt": "2026-03-02T01:30:00.000Z",
                        "updatedAt": "2026-10-04T08:12:45.120Z",
                        "deletedAt": null
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadDetailQuery" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/appointments": {
      "get": {
        "operationId": "listAppointments",
        "tags": ["Appointments"],
        "summary": "Sync appointments by update time",
        "description": "Returns appointments ordered by `(updatedAt, id)` ascending, starting from `updatedSince` (if provided) and after `cursor` (if provided). A cancelled appointment is still returned, with `status = CANCELLED`; only a deleted appointment is a tombstone.\n\nUse `phone` to match an appointment that is not yet linked to a patient record (`patientId = null`). `reason` is free text and may contain health information.\n\nRequires the `appointments:read` scope.",
        "security": [{ "bearerAuth": ["appointments:read"] }],
        "parameters": [
          { "$ref": "#/components/parameters/UpdatedSince" },
          { "$ref": "#/components/parameters/Cursor" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": {
            "description": "One page of appointments.",
            "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AppointmentList" },
                "examples": {
                  "page": {
                    "summary": "A confirmed appointment",
                    "value": {
                      "requestId": "req_7c1f3a",
                      "appointments": [
                        {
                          "id": "a41f9c0e-7d25-4b8a-9c36-0e5b1d7a2f44",
                          "code": "LH-20261020-003",
                          "patientId": "5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10",
                          "phone": "0900000001",
                          "scheduledAt": "2026-10-20T02:30:00.000Z",
                          "status": "BOOKED",
                          "doctorName": "BS. Trần Thị B",
                          "room": "Phòng 2",
                          "reason": "Tái khám",
                          "createdAt": "2026-10-15T07:00:00.000Z",
                          "updatedAt": "2026-10-15T07:00:00.000Z",
                          "deletedAt": null
                        }
                      ],
                      "nextCursor": null
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadListQuery" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/appointments/{id}": {
      "get": {
        "operationId": "getAppointment",
        "tags": ["Appointments"],
        "summary": "Get an appointment by id",
        "description": "Returns the appointment, or a tombstone if it was deleted. An `id` that belongs to another clinic and an `id` that does not exist both return the same `404 NOT_FOUND`.\n\nRequires the `appointments:read` scope.",
        "security": [{ "bearerAuth": ["appointments:read"] }],
        "parameters": [{ "$ref": "#/components/parameters/AppointmentId" }],
        "responses": {
          "200": {
            "description": "The appointment.",
            "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AppointmentDetail" },
                "examples": {
                  "appointment": {
                    "summary": "An appointment",
                    "value": {
                      "requestId": "req_7c1f3a",
                      "appointment": {
                        "id": "a41f9c0e-7d25-4b8a-9c36-0e5b1d7a2f44",
                        "code": "LH-20261020-003",
                        "patientId": "5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10",
                        "phone": "0900000001",
                        "scheduledAt": "2026-10-20T02:30:00.000Z",
                        "status": "BOOKED",
                        "doctorName": "BS. Trần Thị B",
                        "room": "Phòng 2",
                        "reason": "Tái khám",
                        "createdAt": "2026-10-15T07:00:00.000Z",
                        "updatedAt": "2026-10-15T07:00:00.000Z",
                        "deletedAt": null
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadDetailQuery" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/receipts": {
      "get": {
        "operationId": "listReceipts",
        "tags": ["Receipts"],
        "summary": "Sync receipts by update time",
        "description": "Returns receipts with their line items, ordered by `(updatedAt, id)` ascending, starting from `updatedSince` (if provided) and after `cursor` (if provided). Every amount is a whole number of Vietnamese dong (VND).\n\nThe collected revenue of a period is the sum of `paidAmount` over the receipts whose `receiptDate` is in the period, whose `status` is neither `CANCELLED` nor `REFUNDED`, and whose `deletedAt` is `null`. The iClinic Revenue report calculates it the same way. A refund always covers the whole receipt (`status = REFUNDED`); a cancelled receipt (`CANCELLED`) has collected no money.\n\nRequires the `receipts:read` scope.",
        "security": [{ "bearerAuth": ["receipts:read"] }],
        "parameters": [
          { "$ref": "#/components/parameters/UpdatedSince" },
          { "$ref": "#/components/parameters/Cursor" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": {
            "description": "One page of receipts.",
            "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ReceiptList" },
                "examples": {
                  "page": {
                    "summary": "A fully paid receipt",
                    "value": {
                      "requestId": "req_7c1f3a",
                      "receipts": [
                        {
                          "id": "e7d3b1a4-52c8-4f19-a0b6-9c4d8e2f1a37",
                          "code": "HD-20261004-002",
                          "patientId": "5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10",
                          "encounterId": "3f0c9a52-6d1b-4e7a-9b1e-2a4c8d5e7f10",
                          "total": 350000,
                          "paidAmount": 350000,
                          "paymentMethod": "CASH",
                          "status": "PAID",
                          "receiptDate": "2026-10-04",
                          "lines": [
                            {
                              "name": "Khám chuyên khoa",
                              "quantity": 1,
                              "unitPrice": 200000,
                              "total": 200000,
                              "sourceType": "SERVICE"
                            },
                            {
                              "name": "Vitamin B1",
                              "quantity": 10,
                              "unitPrice": 15000,
                              "total": 150000,
                              "sourceType": "MEDICATION"
                            }
                          ],
                          "createdAt": "2026-10-04T08:10:00.000Z",
                          "updatedAt": "2026-10-04T08:10:00.000Z",
                          "deletedAt": null
                        }
                      ],
                      "nextCursor": null
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadListQuery" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/receipts/{id}": {
      "get": {
        "operationId": "getReceipt",
        "tags": ["Receipts"],
        "summary": "Get a receipt by id",
        "description": "Returns the receipt with its line items, or a tombstone if it was deleted. An `id` that belongs to another clinic and an `id` that does not exist both return the same `404 NOT_FOUND`.\n\nRequires the `receipts:read` scope.",
        "security": [{ "bearerAuth": ["receipts:read"] }],
        "parameters": [{ "$ref": "#/components/parameters/ReceiptId" }],
        "responses": {
          "200": {
            "description": "The receipt.",
            "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ReceiptDetail" },
                "examples": {
                  "receipt": {
                    "summary": "A receipt with an outstanding balance",
                    "value": {
                      "requestId": "req_7c1f3a",
                      "receipt": {
                        "id": "e7d3b1a4-52c8-4f19-a0b6-9c4d8e2f1a37",
                        "code": "HD-20261004-002",
                        "patientId": "5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10",
                        "encounterId": null,
                        "total": 350000,
                        "paidAmount": 100000,
                        "paymentMethod": "BANK_TRANSFER",
                        "status": "PARTIAL",
                        "receiptDate": "2026-10-04",
                        "lines": [
                          {
                            "name": "Liệu trình châm cứu",
                            "quantity": 1,
                            "unitPrice": 350000,
                            "total": 350000,
                            "sourceType": "MANUAL"
                          }
                        ],
                        "createdAt": "2026-10-04T08:10:00.000Z",
                        "updatedAt": "2026-10-06T03:00:00.000Z",
                        "deletedAt": null
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadDetailQuery" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/therapy-plans": {
      "get": {
        "operationId": "listTherapyPlans",
        "tags": ["Therapy plans"],
        "summary": "Sync therapy plans by update time",
        "description": "Returns therapy plans ordered by `(updatedAt, id)` ascending, starting from `updatedSince` (if provided) and after `cursor` (if provided). `completedSessions` and `paidAmount` change when a session is recorded or a payment is collected, and `updatedAt` changes with them. The response does not include diagnoses or clinical notes.\n\nRequires the `therapy:read` scope.",
        "security": [{ "bearerAuth": ["therapy:read"] }],
        "parameters": [
          { "$ref": "#/components/parameters/UpdatedSince" },
          { "$ref": "#/components/parameters/Cursor" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": {
            "description": "One page of therapy plans.",
            "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TherapyPlanList" },
                "examples": {
                  "page": {
                    "summary": "A therapy plan in progress",
                    "value": {
                      "requestId": "req_7c1f3a",
                      "therapyPlans": [
                        {
                          "id": "7b2e4d90-1a63-4c85-b7f0-3d9a6c1e8b52",
                          "code": "LT-20261001-001",
                          "patientId": "5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10",
                          "name": "Liệu trình châm cứu 10 buổi",
                          "sessionCount": 10,
                          "completedSessions": 4,
                          "status": "IN_PROGRESS",
                          "totalPrice": 3500000,
                          "paidAmount": 3500000,
                          "createdAt": "2026-10-01T02:00:00.000Z",
                          "updatedAt": "2026-10-12T07:30:00.000Z",
                          "deletedAt": null
                        }
                      ],
                      "nextCursor": null
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadListQuery" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/therapy-plans/{id}": {
      "get": {
        "operationId": "getTherapyPlan",
        "tags": ["Therapy plans"],
        "summary": "Get a therapy plan by id",
        "description": "Returns the therapy plan, or a tombstone if it was deleted. An `id` that belongs to another clinic and an `id` that does not exist both return the same `404 NOT_FOUND`.\n\nRequires the `therapy:read` scope.",
        "security": [{ "bearerAuth": ["therapy:read"] }],
        "parameters": [{ "$ref": "#/components/parameters/TherapyPlanId" }],
        "responses": {
          "200": {
            "description": "The therapy plan.",
            "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TherapyPlanDetail" },
                "examples": {
                  "therapyPlan": {
                    "summary": "A therapy plan",
                    "value": {
                      "requestId": "req_7c1f3a",
                      "therapyPlan": {
                        "id": "7b2e4d90-1a63-4c85-b7f0-3d9a6c1e8b52",
                        "code": "LT-20261001-001",
                        "patientId": "5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10",
                        "name": "Liệu trình châm cứu 10 buổi",
                        "sessionCount": 10,
                        "completedSessions": 4,
                        "status": "IN_PROGRESS",
                        "totalPrice": 3500000,
                        "paidAmount": 3500000,
                        "createdAt": "2026-10-01T02:00:00.000Z",
                        "updatedAt": "2026-10-12T07:30:00.000Z",
                        "deletedAt": null
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadDetailQuery" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ick_…",
        "description": "A fixed API key, sent in the header `Authorization: Bearer ick_…`. The key does not expire on its own; it stops working only when a clinic admin revokes it. Each path requires one read scope, listed in the `security` field of each operation."
      }
    },
    "headers": {
      "XRequestId": {
        "description": "Request ID, identical to `requestId` in the response body. Include it when you contact support.",
        "schema": { "type": "string", "examples": ["req_7c1f3a"] }
      },
      "RetryAfter": {
        "description": "Number of seconds to wait before you retry.",
        "schema": { "type": "integer", "minimum": 1, "examples": [30] }
      }
    },
    "parameters": {
      "UpdatedSince": {
        "name": "updatedSince",
        "in": "query",
        "required": false,
        "description": "Return only records whose `updatedAt` is at or after this time (UTC; a record with exactly this time is included). Full ISO 8601 format, for example `2026-10-01T00:00:00.000Z`. Keep this value the same on every page of one sync.",
        "schema": { "type": "string", "format": "date-time" },
        "example": "2026-10-01T00:00:00.000Z"
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Opaque cursor taken from `nextCursor` of the previous page. Do not build or parse it yourself; use it only with the resource that issued it.",
        "schema": { "type": "string", "maxLength": 256 },
        "example": "djF8cGF0aWVudHN8MjAyNi0xMC0wNVQwMzowMDowMC4wMDBafGM5YTFkN2UyLTBiMzQtNGM2Zi04YTUxLTZlN2YzYjJkOWMwOA"
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum number of records per page, from 1 to 500.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 200 },
        "example": 200
      },
      "PatientId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The `id` of the patient (the `id` field in `/patients`).",
        "schema": { "type": "string" },
        "example": "5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10"
      },
      "AppointmentId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The `id` of the appointment (the `id` field in `/appointments`).",
        "schema": { "type": "string" },
        "example": "a41f9c0e-7d25-4b8a-9c36-0e5b1d7a2f44"
      },
      "ReceiptId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The `id` of the receipt (the `id` field in `/receipts`).",
        "schema": { "type": "string" },
        "example": "e7d3b1a4-52c8-4f19-a0b6-9c4d8e2f1a37"
      },
      "TherapyPlanId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The `id` of the therapy plan (the `id` field in `/therapy-plans`).",
        "schema": { "type": "string" },
        "example": "7b2e4d90-1a63-4c85-b7f0-3d9a6c1e8b52"
      }
    },
    "schemas": {
      "RequestId": {
        "type": "string",
        "description": "Request ID, identical to the `X-Request-Id` header.",
        "examples": ["req_7c1f3a"]
      },
      "CreatedAt": {
        "type": "string",
        "format": "date-time",
        "description": "Time the record was created (UTC).",
        "examples": ["2026-10-04T08:10:00.000Z"]
      },
      "UpdatedAt": {
        "type": "string",
        "format": "date-time",
        "description": "Time the record last changed (UTC). Together with `id`, it is the sort key used by incremental sync.",
        "examples": ["2026-10-04T08:12:45.120Z"]
      },
      "LiveDeletedAt": {
        "type": "null",
        "description": "Always `null` for a record that still exists.",
        "examples": [null]
      },
      "TombstoneDeletedAt": {
        "type": "string",
        "format": "date-time",
        "description": "Time the record was deleted or merged (UTC). A value means this is a tombstone: delete this record on your side.",
        "examples": ["2026-10-05T03:00:00.000Z"]
      },
      "NextCursor": {
        "type": ["string", "null"],
        "description": "Cursor for the next page; `null` when no data is left at the time of the call.",
        "examples": [
          "djF8cGF0aWVudHN8MjAyNi0xMC0wNVQwMzowMDowMC4wMDBafGM5YTFkN2UyLTBiMzQtNGM2Zi04YTUxLTZlN2YzYjJkOWMwOA",
          null
        ]
      },
      "PatientRef": {
        "type": ["string", "null"],
        "description": "`id` of the patient in `/patients`; `null` if not yet linked to a patient record.",
        "examples": ["5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10", null]
      },
      "Money": {
        "type": "integer",
        "description": "Whole amount in Vietnamese dong (VND).",
        "minimum": 0,
        "examples": [350000]
      },
      "Gender": {
        "type": "string",
        "description": "`MALE` male · `FEMALE` female · `OTHER` other · `UNKNOWN` not known.",
        "enum": ["MALE", "FEMALE", "OTHER", "UNKNOWN"],
        "examples": ["MALE"]
      },
      "PatientType": {
        "type": "string",
        "description": "`NEW` new patient · `OLD` returning patient · `RE_EXAMINATION` scheduled follow-up.",
        "enum": ["NEW", "OLD", "RE_EXAMINATION"],
        "examples": ["OLD"]
      },
      "PatientStatus": {
        "type": "string",
        "description": "`ACTIVE` in use · `INACTIVE` the clinic has deactivated this record.",
        "enum": ["ACTIVE", "INACTIVE"],
        "examples": ["ACTIVE"]
      },
      "EncounterStatus": {
        "type": "string",
        "description": "`NEW` arrived / checked in · `ONGOING` being examined · `FINISHED` examination finished · `REOPENED` reopened · `LOCKED` record locked · `CANCELLED` cancelled.",
        "enum": ["NEW", "ONGOING", "FINISHED", "REOPENED", "LOCKED", "CANCELLED"],
        "examples": ["FINISHED"]
      },
      "AppointmentStatus": {
        "type": "string",
        "description": "`REQUESTED` booked by the patient, waiting for confirmation · `BOOKED` confirmed · `ARRIVED` arrived · `IN_PROGRESS` being examined · `FINISHED` examination finished · `CANCELLED` cancelled · `ERROR` bad data, skip it.",
        "enum": ["REQUESTED", "BOOKED", "ARRIVED", "IN_PROGRESS", "FINISHED", "CANCELLED", "ERROR"],
        "examples": ["BOOKED"]
      },
      "ReceiptStatus": {
        "type": "string",
        "description": "`PAID` paid in full · `PARTIAL` a balance is still owed · `UNPAID` not paid · `CANCELLED` cancelled (no money collected) · `REFUNDED` refunded in full.",
        "enum": ["PAID", "PARTIAL", "UNPAID", "CANCELLED", "REFUNDED"],
        "examples": ["PAID"]
      },
      "ReceiptLineSourceType": {
        "type": "string",
        "description": "Source of the line: `SERVICE` consultation or service fee of the encounter · `PROCEDURE` procedure · `MEDICATION` prescribed medication · `MANUAL` line entered by hand (in the Sales screen of the staff app).",
        "enum": ["SERVICE", "PROCEDURE", "MEDICATION", "MANUAL"],
        "examples": ["SERVICE"]
      },
      "TherapyPlanStatus": {
        "type": "string",
        "description": "`NEW` no session done yet · `IN_PROGRESS` in progress · `COMPLETED` all sessions done · `CANCELLED` cancelled.",
        "enum": ["NEW", "IN_PROGRESS", "COMPLETED", "CANCELLED"],
        "examples": ["IN_PROGRESS"]
      },
      "Patient": {
        "type": "object",
        "description": "Minimal administrative record of a patient.",
        "required": [
          "id",
          "code",
          "name",
          "phone",
          "gender",
          "birthYear",
          "patientType",
          "status",
          "registeredDate",
          "createdAt",
          "updatedAt",
          "deletedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable ID of the patient; it never changes. Use it as the deduplication key.",
            "examples": ["5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10"]
          },
          "code": {
            "type": "string",
            "description": "Display code of the patient record. For reference only.",
            "examples": ["BN-000412"]
          },
          "name": {
            "type": "string",
            "description": "Full name of the patient.",
            "examples": ["Nguyễn Văn A"]
          },
          "phone": {
            "type": ["string", "null"],
            "description": "Phone number in the format `0…`; `null` if the clinic has no number.",
            "examples": ["0900000001", null]
          },
          "gender": { "$ref": "#/components/schemas/Gender" },
          "birthYear": {
            "type": ["integer", "null"],
            "description": "Year of birth; `null` if not available. The full date of birth is not returned.",
            "examples": [1984, null]
          },
          "patientType": { "$ref": "#/components/schemas/PatientType" },
          "status": { "$ref": "#/components/schemas/PatientStatus" },
          "registeredDate": {
            "type": "string",
            "format": "date",
            "description": "Date the record was created (clinic time), `YYYY-MM-DD`. This is not the date of the first visit: a record can be created before the first visit.",
            "examples": ["2026-03-02"]
          },
          "createdAt": { "$ref": "#/components/schemas/CreatedAt" },
          "updatedAt": { "$ref": "#/components/schemas/UpdatedAt" },
          "deletedAt": { "$ref": "#/components/schemas/LiveDeletedAt" }
        }
      },
      "PatientTombstone": {
        "type": "object",
        "description": "A patient record that was merged into another record. It no longer holds personal data.",
        "required": ["id", "deletedAt", "updatedAt", "mergedIntoId"],
        "properties": {
          "id": {
            "type": "string",
            "description": "`id` of the merged record.",
            "examples": ["c9a1d7e2-0b34-4c6f-8a51-6e7f3b2d9c08"]
          },
          "deletedAt": { "$ref": "#/components/schemas/TombstoneDeletedAt" },
          "updatedAt": { "$ref": "#/components/schemas/UpdatedAt" },
          "mergedIntoId": {
            "type": ["string", "null"],
            "description": "`id` of the record that remains after the merge; `null` if the record was deleted for a reason other than a merge. Appointments, receipts and therapy plans of the old record were moved to this record.",
            "examples": ["5b0d6a38-3f5a-4a52-8f7c-1d2e9c4b7a10", null]
          }
        }
      },
      "PatientList": {
        "type": "object",
        "description": "One page of patients.",
        "required": ["requestId", "patients", "nextCursor"],
        "properties": {
          "requestId": { "$ref": "#/components/schemas/RequestId" },
          "patients": {
            "type": "array",
            "description": "Patients or tombstones, ordered by `(updatedAt, id)` ascending.",
            "items": {
              "oneOf": [
                { "$ref": "#/components/schemas/Patient" },
                { "$ref": "#/components/schemas/PatientTombstone" }
              ]
            }
          },
          "nextCursor": { "$ref": "#/components/schemas/NextCursor" }
        }
      },
      "PatientDetail": {
        "type": "object",
        "description": "One patient.",
        "required": ["requestId", "patient"],
        "properties": {
          "requestId": { "$ref": "#/components/schemas/RequestId" },
          "patient": {
            "description": "A patient or a tombstone.",
            "oneOf": [
              { "$ref": "#/components/schemas/Patient" },
              { "$ref": "#/components/schemas/PatientTombstone" }
            ]
          }
        }
      },
      "Appointment": {
        "type": "object",
        "description": "An appointment.",
        "required": [
          "id",
          "code",
          "patientId",
          "phone",
          "scheduledAt",
          "status",
          "doctorName",
          "room",
          "reason",
          "createdAt",
          "updatedAt",
          "deletedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable ID of the appointment; it never changes. Use it as the deduplication key.",
            "examples": ["a41f9c0e-7d25-4b8a-9c36-0e5b1d7a2f44"]
          },
          "code": {
            "type": "string",
            "description": "Display code of the appointment. For reference only.",
            "examples": ["LH-20261020-003"]
          },
          "patientId": { "$ref": "#/components/schemas/PatientRef" },
          "phone": {
            "type": ["string", "null"],
            "description": "Phone number used to match an appointment that is not yet linked to a patient record; `null` if there is none.",
            "examples": ["0900000001", null]
          },
          "scheduledAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Appointment time (UTC); `null` if it cannot be determined. Clinic time is UTC+7.",
            "examples": ["2026-10-20T02:30:00.000Z", null]
          },
          "status": { "$ref": "#/components/schemas/AppointmentStatus" },
          "doctorName": {
            "type": "string",
            "description": "Name of the assigned doctor; empty string if no doctor is assigned.",
            "examples": ["BS. Trần Thị B"]
          },
          "room": {
            "type": "string",
            "description": "Examination room; empty string if no room is assigned.",
            "examples": ["Phòng 2"]
          },
          "reason": {
            "type": "string",
            "description": "Reason for the appointment. Free text that may contain health information; empty string if there is none.",
            "examples": ["Tái khám"]
          },
          "createdAt": { "$ref": "#/components/schemas/CreatedAt" },
          "updatedAt": { "$ref": "#/components/schemas/UpdatedAt" },
          "deletedAt": { "$ref": "#/components/schemas/LiveDeletedAt" }
        }
      },
      "AppointmentTombstone": {
        "type": "object",
        "description": "A deleted appointment. It no longer holds personal data.",
        "required": ["id", "deletedAt", "updatedAt"],
        "properties": {
          "id": {
            "type": "string",
            "description": "`id` of the deleted appointment.",
            "examples": ["a41f9c0e-7d25-4b8a-9c36-0e5b1d7a2f44"]
          },
          "deletedAt": { "$ref": "#/components/schemas/TombstoneDeletedAt" },
          "updatedAt": { "$ref": "#/components/schemas/UpdatedAt" }
        }
      },
      "AppointmentList": {
        "type": "object",
        "description": "One page of appointments.",
        "required": ["requestId", "appointments", "nextCursor"],
        "properties": {
          "requestId": { "$ref": "#/components/schemas/RequestId" },
          "appointments": {
            "type": "array",
            "description": "Appointments or tombstones, ordered by `(updatedAt, id)` ascending.",
            "items": {
              "oneOf": [
                { "$ref": "#/components/schemas/Appointment" },
                { "$ref": "#/components/schemas/AppointmentTombstone" }
              ]
            }
          },
          "nextCursor": { "$ref": "#/components/schemas/NextCursor" }
        }
      },
      "AppointmentDetail": {
        "type": "object",
        "description": "One appointment.",
        "required": ["requestId", "appointment"],
        "properties": {
          "requestId": { "$ref": "#/components/schemas/RequestId" },
          "appointment": {
            "description": "An appointment or a tombstone.",
            "oneOf": [
              { "$ref": "#/components/schemas/Appointment" },
              { "$ref": "#/components/schemas/AppointmentTombstone" }
            ]
          }
        }
      },
      "ReceiptLine": {
        "type": "object",
        "description": "One line item of a receipt.",
        "required": ["name", "quantity", "unitPrice", "total", "sourceType"],
        "properties": {
          "name": {
            "type": "string",
            "description": "Name of the service, procedure or item.",
            "examples": ["Khám chuyên khoa"]
          },
          "quantity": {
            "type": "number",
            "description": "Quantity.",
            "minimum": 0,
            "examples": [1]
          },
          "unitPrice": { "$ref": "#/components/schemas/Money" },
          "total": {
            "type": "integer",
            "description": "Total of the line after the line's own discount and value-added tax (VAT) (dong). The sum of the line totals can be greater than the `total` of the receipt when the receipt has a discount on its total.",
            "minimum": 0,
            "examples": [200000]
          },
          "sourceType": { "$ref": "#/components/schemas/ReceiptLineSourceType" }
        }
      },
      "Receipt": {
        "type": "object",
        "description": "A receipt with its line items.",
        "required": [
          "id",
          "code",
          "patientId",
          "encounterId",
          "total",
          "paidAmount",
          "paymentMethod",
          "status",
          "receiptDate",
          "lines",
          "createdAt",
          "updatedAt",
          "deletedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable ID of the receipt; it never changes. Use it as the deduplication key.",
            "examples": ["e7d3b1a4-52c8-4f19-a0b6-9c4d8e2f1a37"]
          },
          "code": {
            "type": "string",
            "description": "Display number of the receipt. For reference only.",
            "examples": ["HD-20261004-002"]
          },
          "patientId": { "$ref": "#/components/schemas/PatientRef" },
          "encounterId": {
            "type": ["string", "null"],
            "description": "`encounterUuid` of the encounter that the receipt collects payment for; `null` for a retail receipt or a receipt that collects payment for a therapy plan.",
            "examples": ["3f0c9a52-6d1b-4e7a-9b1e-2a4c8d5e7f10", null]
          },
          "total": {
            "type": "integer",
            "description": "Total payable on the receipt after discount (dong).",
            "minimum": 0,
            "examples": [350000]
          },
          "paidAmount": {
            "type": "integer",
            "description": "Amount collected (dong), including advance payments that were deducted and later payments against the balance. For a `REFUNDED` receipt, this is the amount collected before the refund.",
            "minimum": 0,
            "examples": [350000]
          },
          "paymentMethod": {
            "type": "string",
            "description": "Payment method at the time the receipt was created, in upper case. Common values are `CASH` (cash) and `BANK_TRANSFER` (bank transfer). Other values are possible; do not assume the list is closed.",
            "examples": ["CASH"]
          },
          "status": { "$ref": "#/components/schemas/ReceiptStatus" },
          "receiptDate": {
            "type": "string",
            "format": "date",
            "description": "Date of payment (clinic time), `YYYY-MM-DD`: the date on which the revenue is counted.",
            "examples": ["2026-10-04"]
          },
          "lines": {
            "type": "array",
            "description": "The line items of the receipt.",
            "items": { "$ref": "#/components/schemas/ReceiptLine" }
          },
          "createdAt": { "$ref": "#/components/schemas/CreatedAt" },
          "updatedAt": { "$ref": "#/components/schemas/UpdatedAt" },
          "deletedAt": { "$ref": "#/components/schemas/LiveDeletedAt" }
        }
      },
      "ReceiptTombstone": {
        "type": "object",
        "description": "A deleted receipt. It no longer holds personal data or amounts.",
        "required": ["id", "deletedAt", "updatedAt"],
        "properties": {
          "id": {
            "type": "string",
            "description": "`id` of the deleted receipt.",
            "examples": ["e7d3b1a4-52c8-4f19-a0b6-9c4d8e2f1a37"]
          },
          "deletedAt": { "$ref": "#/components/schemas/TombstoneDeletedAt" },
          "updatedAt": { "$ref": "#/components/schemas/UpdatedAt" }
        }
      },
      "ReceiptList": {
        "type": "object",
        "description": "One page of receipts.",
        "required": ["requestId", "receipts", "nextCursor"],
        "properties": {
          "requestId": { "$ref": "#/components/schemas/RequestId" },
          "receipts": {
            "type": "array",
            "description": "Receipts or tombstones, ordered by `(updatedAt, id)` ascending.",
            "items": {
              "oneOf": [
                { "$ref": "#/components/schemas/Receipt" },
                { "$ref": "#/components/schemas/ReceiptTombstone" }
              ]
            }
          },
          "nextCursor": { "$ref": "#/components/schemas/NextCursor" }
        }
      },
      "ReceiptDetail": {
        "type": "object",
        "description": "One receipt.",
        "required": ["requestId", "receipt"],
        "properties": {
          "requestId": { "$ref": "#/components/schemas/RequestId" },
          "receipt": {
            "description": "A receipt or a tombstone.",
            "oneOf": [
              { "$ref": "#/components/schemas/Receipt" },
              { "$ref": "#/components/schemas/ReceiptTombstone" }
            ]
          }
        }
      },
      "TherapyPlan": {
        "type": "object",
        "description": "A therapy plan (a package of several sessions).",
        "required": [
          "id",
          "code",
          "patientId",
          "name",
          "sessionCount",
          "completedSessions",
          "status",
          "totalPrice",
          "paidAmount",
          "createdAt",
          "updatedAt",
          "deletedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable ID of the therapy plan; it never changes. Use it as the deduplication key.",
            "examples": ["7b2e4d90-1a63-4c85-b7f0-3d9a6c1e8b52"]
          },
          "code": {
            "type": "string",
            "description": "Display code of the therapy plan. For reference only.",
            "examples": ["LT-20261001-001"]
          },
          "patientId": { "$ref": "#/components/schemas/PatientRef" },
          "name": {
            "type": "string",
            "description": "Name of the therapy plan.",
            "examples": ["Liệu trình châm cứu 10 buổi"]
          },
          "sessionCount": {
            "type": "integer",
            "description": "Total number of sessions in the therapy plan.",
            "minimum": 0,
            "examples": [10]
          },
          "completedSessions": {
            "type": "integer",
            "description": "Number of sessions completed.",
            "minimum": 0,
            "examples": [4]
          },
          "status": { "$ref": "#/components/schemas/TherapyPlanStatus" },
          "totalPrice": {
            "type": "integer",
            "description": "Total price of the therapy plan (dong).",
            "minimum": 0,
            "examples": [3500000]
          },
          "paidAmount": {
            "type": "integer",
            "description": "Amount collected for the therapy plan (dong); it decreases when a receipt that collected payment is refunded.",
            "minimum": 0,
            "examples": [3500000]
          },
          "createdAt": { "$ref": "#/components/schemas/CreatedAt" },
          "updatedAt": { "$ref": "#/components/schemas/UpdatedAt" },
          "deletedAt": { "$ref": "#/components/schemas/LiveDeletedAt" }
        }
      },
      "TherapyPlanTombstone": {
        "type": "object",
        "description": "A deleted therapy plan. It no longer holds personal data.",
        "required": ["id", "deletedAt", "updatedAt"],
        "properties": {
          "id": {
            "type": "string",
            "description": "`id` of the deleted therapy plan.",
            "examples": ["7b2e4d90-1a63-4c85-b7f0-3d9a6c1e8b52"]
          },
          "deletedAt": { "$ref": "#/components/schemas/TombstoneDeletedAt" },
          "updatedAt": { "$ref": "#/components/schemas/UpdatedAt" }
        }
      },
      "TherapyPlanList": {
        "type": "object",
        "description": "One page of therapy plans.",
        "required": ["requestId", "therapyPlans", "nextCursor"],
        "properties": {
          "requestId": { "$ref": "#/components/schemas/RequestId" },
          "therapyPlans": {
            "type": "array",
            "description": "Therapy plans or tombstones, ordered by `(updatedAt, id)` ascending.",
            "items": {
              "oneOf": [
                { "$ref": "#/components/schemas/TherapyPlan" },
                { "$ref": "#/components/schemas/TherapyPlanTombstone" }
              ]
            }
          },
          "nextCursor": { "$ref": "#/components/schemas/NextCursor" }
        }
      },
      "TherapyPlanDetail": {
        "type": "object",
        "description": "One therapy plan.",
        "required": ["requestId", "therapyPlan"],
        "properties": {
          "requestId": { "$ref": "#/components/schemas/RequestId" },
          "therapyPlan": {
            "description": "A therapy plan or a tombstone.",
            "oneOf": [
              { "$ref": "#/components/schemas/TherapyPlan" },
              { "$ref": "#/components/schemas/TherapyPlanTombstone" }
            ]
          }
        }
      },
      "Encounter": {
        "type": "object",
        "description": "One encounter.",
        "required": [
          "id",
          "encounterUuid",
          "patientId",
          "patientName",
          "phone",
          "visitDate",
          "checkedInAt",
          "completedAt",
          "status",
          "patientType",
          "diagnosis",
          "icd10Code",
          "followUpDate",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Display code (record code). For reference only; it can be reissued, so do not use it as a deduplication key.",
            "examples": ["BA-000123"]
          },
          "encounterUuid": {
            "type": "string",
            "description": "Stable ID of the encounter; it never changes. Use it as the deduplication key.",
            "examples": ["3f0c9a52-6d1b-4e7a-9b1e-2a4c8d5e7f10"]
          },
          "patientId": { "$ref": "#/components/schemas/PatientRef" },
          "patientName": {
            "type": "string",
            "description": "Name of the patient.",
            "examples": ["Nguyễn Văn A"]
          },
          "phone": {
            "type": ["string", "null"],
            "description": "Phone number; `null` if the clinic has no number.",
            "examples": ["0900000001", null]
          },
          "visitDate": {
            "type": "string",
            "format": "date",
            "description": "Date of the visit (clinic time). The system stores the date, not the time of the visit.",
            "examples": ["2026-09-19"]
          },
          "checkedInAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Check-in time (UTC) when known, otherwise `null`.",
            "examples": ["2026-09-19T02:14:05.000Z", null]
          },
          "completedAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Time the examination ended (UTC) or `null`.",
            "examples": ["2026-09-19T03:02:40.000Z", null]
          },
          "status": { "$ref": "#/components/schemas/EncounterStatus" },
          "patientType": { "$ref": "#/components/schemas/PatientType" },
          "diagnosis": {
            "type": "string",
            "description": "Diagnosis; empty string if there is none yet.",
            "examples": ["Tăng huyết áp"]
          },
          "icd10Code": {
            "type": "string",
            "description": "ICD-10 (International Classification of Diseases, 10th revision) code of the diagnosis; empty string if there is none yet.",
            "examples": ["I10"]
          },
          "followUpDate": {
            "type": ["string", "null"],
            "format": "date",
            "description": "Follow-up date set by the doctor (clinic time), `YYYY-MM-DD`; `null` if no follow-up is scheduled. Added in 1.1.0.",
            "examples": ["2026-10-19", null]
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Time the encounter last changed (UTC). Added in 1.1.0.",
            "examples": ["2026-09-19T03:02:40.000Z"]
          }
        }
      },
      "EncounterList": {
        "type": "object",
        "description": "One page of encounters.",
        "required": ["requestId", "from", "to", "page", "pageSize", "total", "encounters"],
        "properties": {
          "requestId": { "$ref": "#/components/schemas/RequestId" },
          "from": {
            "type": "string",
            "format": "date",
            "description": "Start date applied to the request.",
            "examples": ["2026-09-14"]
          },
          "to": {
            "type": "string",
            "format": "date",
            "description": "End date applied to the request.",
            "examples": ["2026-09-19"]
          },
          "page": {
            "type": "integer",
            "description": "Current page number, starting at 0.",
            "minimum": 0,
            "examples": [0]
          },
          "pageSize": {
            "type": "integer",
            "description": "Maximum number of encounters per page.",
            "minimum": 1,
            "maximum": 500,
            "examples": [200]
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "description": "Total number of encounters that match the filter, across all pages.",
            "examples": [1]
          },
          "encounters": {
            "type": "array",
            "description": "Encounters on this page, newest first.",
            "items": { "$ref": "#/components/schemas/Encounter" }
          }
        }
      },
      "ErrorCode": {
        "type": "string",
        "description": "Stable error code, in upper case. Handle errors by `code`, not by `message`.",
        "enum": [
          "INVALID_DATE",
          "INVALID_RANGE",
          "INVALID_STATUS",
          "INVALID_PAGE",
          "INVALID_UPDATED_SINCE",
          "INVALID_CURSOR",
          "INVALID_LIMIT",
          "UNKNOWN_PARAMETER",
          "UNAUTHORIZED",
          "INSUFFICIENT_SCOPE",
          "NOT_FOUND",
          "METHOD_NOT_ALLOWED",
          "RATE_LIMITED"
        ],
        "examples": ["INVALID_CURSOR"]
      },
      "Error": {
        "type": "object",
        "description": "Body of every error response.",
        "required": ["requestId", "error"],
        "properties": {
          "requestId": { "$ref": "#/components/schemas/RequestId" },
          "error": {
            "type": "object",
            "description": "Error details.",
            "required": ["code", "message"],
            "properties": {
              "code": { "$ref": "#/components/schemas/ErrorCode" },
              "message": {
                "type": "string",
                "description": "Error description in Vietnamese, for human readers. Handle errors by `code`, not by `message`.",
                "examples": ["API key không hợp lệ."]
              }
            }
          }
        }
      }
    },
    "responses": {
      "BadEncounterQuery": {
        "description": "Invalid `/encounters` parameter: `INVALID_DATE`, `INVALID_RANGE`, `INVALID_STATUS` or `INVALID_PAGE`.",
        "x-error-codes": ["INVALID_DATE", "INVALID_RANGE", "INVALID_STATUS", "INVALID_PAGE"],
        "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "requestId": "req_7c1f3a",
              "error": {
                "code": "INVALID_RANGE",
                "message": "`to` phải sau `from` và khoảng tối đa 31 ngày."
              }
            }
          }
        }
      },
      "BadListQuery": {
        "description": "Invalid list parameter: `INVALID_UPDATED_SINCE`, `INVALID_CURSOR`, `INVALID_LIMIT` or `UNKNOWN_PARAMETER` (a parameter that is not documented, including a misspelled name).",
        "x-error-codes": [
          "INVALID_UPDATED_SINCE",
          "INVALID_CURSOR",
          "INVALID_LIMIT",
          "UNKNOWN_PARAMETER"
        ],
        "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "requestId": "req_7c1f3a",
              "error": {
                "code": "INVALID_LIMIT",
                "message": "`limit` phải là số nguyên từ 1 đến 500."
              }
            }
          }
        }
      },
      "BadDetailQuery": {
        "description": "A request for a single record does not accept query parameters. Error code: `UNKNOWN_PARAMETER`.",
        "x-error-codes": ["UNKNOWN_PARAMETER"],
        "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "requestId": "req_7c1f3a",
              "error": {
                "code": "UNKNOWN_PARAMETER",
                "message": "Tham số `limit` không được hỗ trợ ở đường dẫn này."
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The key is missing, wrong or revoked, or the clinic is suspended.",
        "x-error-codes": ["UNAUTHORIZED"],
        "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "requestId": "req_7c1f3a",
              "error": { "code": "UNAUTHORIZED", "message": "API key không hợp lệ." }
            }
          }
        }
      },
      "InsufficientScope": {
        "description": "The key is valid but does not have the read scope that this path requires. The response does not reveal whether the path has any data. Ask the clinic admin to create a new key with the scope you need.",
        "x-error-codes": ["INSUFFICIENT_SCOPE"],
        "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "requestId": "req_7c1f3a",
              "error": {
                "code": "INSUFFICIENT_SCOPE",
                "message": "API key không có quyền truy cập dữ liệu này."
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "No record with this `id` exists in the key's clinic. A record of another clinic returns this same response.",
        "x-error-codes": ["NOT_FOUND"],
        "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "requestId": "req_7c1f3a",
              "error": { "code": "NOT_FOUND", "message": "Không tìm thấy bản ghi." }
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Only `GET` is supported.",
        "x-error-codes": ["METHOD_NOT_ALLOWED"],
        "headers": { "X-Request-Id": { "$ref": "#/components/headers/XRequestId" } },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "requestId": "req_7c1f3a",
              "error": { "code": "METHOD_NOT_ALLOWED", "message": "Chỉ hỗ trợ GET." }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "The rate limit of 60 requests per minute per key was exceeded, or the IP address is temporarily blocked after repeated wrong keys. Wait the number of seconds given in `Retry-After`, then retry.",
        "x-error-codes": ["RATE_LIMITED"],
        "headers": {
          "X-Request-Id": { "$ref": "#/components/headers/XRequestId" },
          "Retry-After": { "$ref": "#/components/headers/RetryAfter" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "requestId": "req_7c1f3a",
              "error": {
                "code": "RATE_LIMITED",
                "message": "Vượt giới hạn số yêu cầu. Vui lòng thử lại sau."
              }
            }
          }
        }
      }
    }
  }
}
