{
  "openapi": "3.1.0",
  "info": {
    "title": "Dentum Clinic API",
    "version": "1.0.0",
    "summary": "Bookings, statuses and busy times for a dental clinic branch connected to Dentum.",
    "description": "For clinics and CRM vendors. The key is created in the clinic cabinet (Integrations) and is shown once. All times are Asia/Tashkent (+05:00). Webhooks: booking.created, booking.updated, ping — signed with Dentum-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t + '.' + raw body)>; retries after 1m, 5m, 30m, 2h, 6h, 12h. Docs: https://dentum.uz/en/klinikam/api/",
    "contact": { "email": "info@dentum.uz", "url": "https://dentum.uz/en/klinikam/api/" }
  },
  "servers": [{ "url": "https://dentum.uz/api/cabinet/v1" }],
  "security": [{ "apiKey": [] }],
  "paths": {
    "/clinic": {
      "get": {
        "operationId": "getClinic",
        "summary": "The branch this key belongs to, its schedule and whether online booking is on",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ClinicResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/bookings": {
      "get": {
        "operationId": "listBookings",
        "summary": "Bookings created or changed after `since` (ascending; continue with next_since)",
        "parameters": [
          { "name": "since", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "ISO 8601. Default: everything." },
          { "name": "status", "in": "query", "schema": { "$ref": "#/components/schemas/Status" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 100 } }
        ],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "items", "next_since", "has_more"], "properties": { "ok": { "const": true }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/Booking" } }, "next_since": { "type": "string", "format": "date-time" }, "has_more": { "type": "boolean" } } } } } },
          "400": { "description": "Bad since" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/bookings/{ref}": {
      "get": {
        "operationId": "getBooking",
        "summary": "One booking by its request number",
        "parameters": [{ "$ref": "#/components/parameters/Ref" }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "const": true }, "booking": { "$ref": "#/components/schemas/Booking" } } } } } },
          "404": { "description": "Not found (or belongs to another clinic)" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/bookings/{ref}/status": {
      "post": {
        "operationId": "setBookingStatus",
        "summary": "Confirm, decline or record the visit. The patient is notified in Telegram on confirm/decline.",
        "parameters": [{ "$ref": "#/components/parameters/Ref" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["status"], "properties": { "status": { "type": "string", "enum": ["confirmed", "declined", "arrived", "paid", "noshow"] } } } } } },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "const": true }, "booking": { "$ref": "#/components/schemas/Booking" }, "previous_status": { "$ref": "#/components/schemas/Status" } } } } } },
          "415": { "description": "Content-Type must be application/json" },
          "422": { "description": "Unknown status" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/busy": {
      "get": {
        "operationId": "listBusy",
        "summary": "Stored busy times",
        "parameters": [
          { "name": "from", "in": "query", "required": true, "schema": { "type": "string", "format": "date" } },
          { "name": "to", "in": "query", "required": true, "schema": { "type": "string", "format": "date" } }
        ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "const": true }, "blocks": { "type": "array", "items": { "$ref": "#/components/schemas/BusyBlock" } } } } } } } }
      },
      "put": {
        "operationId": "putBusy",
        "summary": "Replace busy times from your CRM inside [from, to) for this source (idempotent). Dentum will not offer these times to patients or AI agents.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["from", "to", "blocks"], "properties": {
          "from": { "type": "string", "format": "date-time" }, "to": { "type": "string", "format": "date-time", "description": "Window ≤ 62 days" },
          "source": { "type": "string", "maxLength": 32, "default": "api" },
          "blocks": { "type": "array", "maxItems": 2000, "items": { "type": "object", "required": ["start", "end"], "properties": { "start": { "type": "string", "format": "date-time" }, "end": { "type": "string", "format": "date-time" }, "id": { "type": "string", "maxLength": 80 } } } }
        } } } } },
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "const": true }, "stored": { "type": "integer" } } } } } }, "422": { "description": "Bad window or block" } }
      }
    }
  },
  "webhooks": {
    "booking.created": { "post": { "summary": "A new booking for your branch", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Event" } } } }, "responses": { "200": { "description": "Reply 2xx within 8 seconds" } } } },
    "booking.updated": { "post": { "summary": "Status (or time) changed; previous_status and via are included", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Event" } } } }, "responses": { "200": { "description": "Reply 2xx within 8 seconds" } } } }
  },
  "components": {
    "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "bearerFormat": "dk_live_…" } },
    "parameters": { "Ref": { "name": "ref", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^D-\\d{6}-[A-Z0-9]{4}$" }, "example": "D-261001-K3F7" } },
    "responses": {
      "Unauthorized": { "description": "Missing or invalid API key" },
      "RateLimited": { "description": "More than 120 requests per minute for this key" }
    },
    "schemas": {
      "Status": { "type": "string", "enum": ["requested", "confirmed", "declined", "arrived", "paid", "noshow"] },
      "Booking": {
        "type": "object",
        "properties": {
          "ref": { "type": "string", "example": "D-261001-K3F7" },
          "status": { "$ref": "#/components/schemas/Status" },
          "service": { "type": ["string", "null"] },
          "source": { "type": ["string", "null"], "description": "web, widget, agent, bot, call, walkin, manual" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" },
          "slot": { "type": "object", "properties": { "start": { "type": ["string", "null"], "format": "date-time", "example": "2026-10-03T10:00:00+05:00" }, "preference": { "type": ["string", "null"] }, "timezone": { "const": "Asia/Tashkent" } } },
          "patient": { "type": "object", "properties": { "name": { "type": ["string", "null"] }, "phone": { "type": ["string", "null"] }, "telegram_linked": { "type": "boolean" } } }
        }
      },
      "Schedule": {
        "type": "object",
        "properties": {
          "days": { "type": "array", "items": { "type": "integer", "minimum": 0, "maximum": 6 }, "description": "0 = Sunday" },
          "open": { "type": "string", "example": "09:00" }, "close": { "type": "string", "example": "20:00" },
          "stepMin": { "type": "integer", "enum": [15, 20, 30, 45, 60, 90, 120] }, "capacity": { "type": "integer", "minimum": 1, "maximum": 20 },
          "leadHours": { "type": "integer" }, "horizonDays": { "type": "integer" }, "breakFrom": { "type": "string" }, "breakTo": { "type": "string" }
        }
      },
      "ClinicResponse": {
        "type": "object",
        "properties": {
          "ok": { "const": true },
          "clinic": { "type": "object", "properties": { "id": { "type": "string" }, "slug": { "type": "string" }, "name": { "type": "string" }, "city": { "type": "string" } } },
          "online": { "type": "boolean" }, "schedule": { "oneOf": [{ "$ref": "#/components/schemas/Schedule" }, { "type": "null" }] }, "timezone": { "const": "Asia/Tashkent" }
        }
      },
      "BusyBlock": { "type": "object", "properties": { "start": { "type": "string", "format": "date-time" }, "end": { "type": "string", "format": "date-time" }, "source": { "type": "string" }, "id": { "type": ["string", "null"] } } },
      "Event": {
        "type": "object",
        "required": ["id", "type", "created", "clinic", "data"],
        "properties": {
          "id": { "type": "string", "example": "evt_3kM2…" },
          "type": { "type": "string", "enum": ["booking.created", "booking.updated", "ping"] },
          "created": { "type": "string", "format": "date-time" },
          "clinic": { "type": "object", "properties": { "id": { "type": "string" }, "slug": { "type": "string" }, "name": { "type": "string" } } },
          "data": { "$ref": "#/components/schemas/Booking" },
          "previous_status": { "$ref": "#/components/schemas/Status" },
          "via": { "type": "string", "enum": ["telegram", "cabinet", "api"] }
        }
      }
    }
  }
}
