{
  "openapi": "3.1.0",
  "info": {
    "title": "Mujib API",
    "version": "2026-11-04",
    "summary": "Read-only REST API for a Mujib workspace: its contacts and its bookings.",
    "description": "The Mujib public API reads the data of ONE workspace (one business): the workspace's customers (contacts) and its appointments (bookings). It is read-only: every operation is a GET, and no operation sends messages or changes data.\n\nAuthentication is an API key created in the Mujib dashboard (Integrations → For developers → API keys), sent in the `Authorization: Bearer mjb_live_…` header (or `X-Api-Key`). A key reads only the workspace that created it, and only what its scopes allow (`contacts.read`, `bookings.read`). A dashboard session cookie does not work here, and a key never goes in the URL.\n\nEvery error has the same envelope: `{ \"error\": { \"code\": \"…\", \"message\": \"…\" } }`. Lists are paginated with an opaque cursor (`nextCursor`). Each key may make 60 requests per clock minute; a wider per-address limit also applies to all traffic from one network address. Responses are never cached (`Cache-Control: no-store`).\n\nCall the API from a server. Browsers on other origins are refused by CORS, and a key must never ship in browser code.",
    "contact": {
      "name": "Mujib",
      "url": "https://docs.mujib.ai/en/developers/overview/"
    }
  },
  "servers": [
    {
      "url": "https://api.mujib.ai",
      "description": "Production"
    }
  ],
  "security": [
    { "bearerAuth": [] },
    { "apiKeyHeader": [] }
  ],
  "tags": [
    { "name": "Workspace", "description": "The key and the workspace it belongs to." },
    { "name": "Contacts", "description": "Customers of the workspace. Scope: `contacts.read`." },
    { "name": "Bookings", "description": "Appointments of the workspace. Scope: `bookings.read`." }
  ],
  "paths": {
    "/api/v1/me": {
      "get": {
        "operationId": "getMe",
        "tags": ["Workspace"],
        "summary": "The workspace and the key",
        "description": "Returns the name of the workspace the key belongs to, and the key's own name and scopes. Needs no scope: any valid key can call it. Use it to check that a key works.",
        "responses": {
          "200": {
            "description": "The key is valid.",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Me" },
                "example": {
                  "workspace": { "name": "مركز النخبة" },
                  "key": { "name": "تقارير المبيعات", "scopes": ["contacts.read", "bookings.read"] }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/Internal" }
        }
      }
    },
    "/api/v1/contacts": {
      "get": {
        "operationId": "listContacts",
        "tags": ["Contacts"],
        "summary": "List contacts",
        "description": "The workspace's contacts, newest first (by `createdAt`, the moment the contact was first seen; ties broken by `id`). Paginated: pass the previous page's `nextCursor` as `cursor`, keeping the other parameters the same, until `nextCursor` is `null`. Any query parameter not listed here is refused with 400.",
        "parameters": [
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Cursor" },
          { "$ref": "#/components/parameters/Since" }
        ],
        "responses": {
          "200": {
            "description": "One page of contacts.",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ContactList" },
                "example": {
                  "data": [
                    {
                      "id": "5d8f2a61-0c4e-4b7a-9f13-6e2b8c0d4a57",
                      "name": "سارة",
                      "phone": "962790000000",
                      "channel": "whatsapp",
                      "stage": "qualified",
                      "tags": ["استشارة"],
                      "createdAt": "2026-11-02T09:14:37.205Z",
                      "lastActiveAt": "2026-11-03T16:40:02.918Z"
                    },
                    {
                      "id": "9b1e3c74-2a5d-4f08-8e6b-1c7d0a3f5e92",
                      "name": null,
                      "phone": null,
                      "channel": "instagram",
                      "stage": "new",
                      "tags": [],
                      "createdAt": "2026-10-28T14:05:12.481Z",
                      "lastActiveAt": "2026-10-28T14:07:55.030Z"
                    }
                  ],
                  "nextCursor": "MjAyNi0xMC0yOFQxNDowNToxMi40ODFafDliMWUzYzc0LTJhNWQtNGYwOC04ZTZiLTFjN2QwYTNmNWU5Mg"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/Internal" }
        }
      }
    },
    "/api/v1/contacts/{id}": {
      "get": {
        "operationId": "getContact",
        "tags": ["Contacts"],
        "summary": "Get one contact",
        "description": "One contact of the workspace by its `id`. An id that does not exist, belongs to another workspace, or is not a well-formed id returns the same 404.",
        "parameters": [
          { "$ref": "#/components/parameters/ContactId" }
        ],
        "responses": {
          "200": {
            "description": "The contact.",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ContactResponse" },
                "example": {
                  "data": {
                    "id": "5d8f2a61-0c4e-4b7a-9f13-6e2b8c0d4a57",
                    "name": "سارة",
                    "phone": "962790000000",
                    "channel": "whatsapp",
                    "stage": "qualified",
                    "tags": ["استشارة"],
                    "createdAt": "2026-11-02T09:14:37.205Z",
                    "lastActiveAt": "2026-11-03T16:40:02.918Z"
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": {
            "description": "No such contact in this workspace.",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": { "code": "NOT_FOUND", "message": "Contact not found" } }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/Internal" }
        }
      }
    },
    "/api/v1/bookings": {
      "get": {
        "operationId": "listBookings",
        "tags": ["Bookings"],
        "summary": "List bookings",
        "description": "The workspace's appointments, in time order (by the appointment's local start time in its branch, then by `id`). `from` and `to` are calendar days in each branch's own time zone, both inclusive. Historical rows imported from an old register are not part of the API. Paginated: pass the previous page's `nextCursor` as `cursor`, keeping the other parameters the same, until `nextCursor` is `null`. Any query parameter not listed here is refused with 400.",
        "parameters": [
          { "$ref": "#/components/parameters/From" },
          { "$ref": "#/components/parameters/To" },
          { "$ref": "#/components/parameters/Status" },
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Cursor" }
        ],
        "responses": {
          "200": {
            "description": "One page of bookings.",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BookingList" },
                "example": {
                  "data": [
                    {
                      "id": "3f0c9a52-7d1e-4b8a-9c41-2e6f8a1b0d77",
                      "status": "confirmed",
                      "source": "agent",
                      "service": "consultation",
                      "startsAt": "2026-11-12T07:30:00Z",
                      "localTime": "2026-11-12T10:30:00",
                      "timezone": "Asia/Amman",
                      "durationMinutes": 30,
                      "branch": { "id": "e7f6a5b4-3c2d-4e1f-9a8b-7c6d5e4f3a21", "name": "الفرع الرئيسي" },
                      "specialist": "ليلى",
                      "customer": { "contactId": "5d8f2a61-0c4e-4b7a-9f13-6e2b8c0d4a57", "name": "سارة", "phone": "962790000000" },
                      "createdAt": "2026-11-04T18:22:09.117Z"
                    },
                    {
                      "id": "c2a7e915-6f3b-4d80-b4e1-7a9d5c0f2b36",
                      "status": "requested",
                      "source": "staff",
                      "service": "followup",
                      "startsAt": "2026-11-12T09:00:00Z",
                      "localTime": "2026-11-12T12:00:00",
                      "timezone": "Asia/Amman",
                      "durationMinutes": null,
                      "branch": { "id": "e7f6a5b4-3c2d-4e1f-9a8b-7c6d5e4f3a21", "name": "الفرع الرئيسي" },
                      "specialist": null,
                      "customer": { "contactId": null, "name": "أحمد", "phone": "0790000000" },
                      "createdAt": "2026-11-05T08:01:44.360Z"
                    }
                  ],
                  "nextCursor": "MjAyNi0xMS0xMlQxMjowMDowMHxjMmE3ZTkxNS02ZjNiLTRkODAtYjRlMS03YTlkNWMwZjJiMzY"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/Internal" }
        }
      }
    },
    "/api/v1/bookings/{id}": {
      "get": {
        "operationId": "getBooking",
        "tags": ["Bookings"],
        "summary": "Get one booking",
        "description": "One appointment of the workspace by its `id`. An id that does not exist, belongs to another workspace, is not a well-formed id, or is a historical row imported from an old register returns the same 404.",
        "parameters": [
          { "$ref": "#/components/parameters/BookingId" }
        ],
        "responses": {
          "200": {
            "description": "The booking.",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BookingResponse" },
                "example": {
                  "data": {
                    "id": "3f0c9a52-7d1e-4b8a-9c41-2e6f8a1b0d77",
                    "status": "confirmed",
                    "source": "agent",
                    "service": "consultation",
                    "startsAt": "2026-11-12T07:30:00Z",
                    "localTime": "2026-11-12T10:30:00",
                    "timezone": "Asia/Amman",
                    "durationMinutes": 30,
                    "branch": { "id": "e7f6a5b4-3c2d-4e1f-9a8b-7c6d5e4f3a21", "name": "الفرع الرئيسي" },
                    "specialist": "ليلى",
                    "customer": { "contactId": "5d8f2a61-0c4e-4b7a-9f13-6e2b8c0d4a57", "name": "سارة", "phone": "962790000000" },
                    "createdAt": "2026-11-04T18:22:09.117Z"
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": {
            "description": "No such booking in this workspace.",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": { "code": "NOT_FOUND", "message": "Booking not found" } }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/Internal" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "mjb_live_ followed by 32 base64url characters",
        "description": "An API key from the dashboard: `Authorization: Bearer mjb_live_…`. The key is shown once, when it is created. A missing, malformed, unknown, revoked or expired key, or a key of a workspace that is not active, returns 401."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "The same API key in the `X-Api-Key` header, for clients that cannot set `Authorization`. When both headers are sent, an `Authorization: Bearer …` header wins."
      }
    },
    "parameters": {
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Items per page.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "The `nextCursor` of the previous page. Opaque: pass it back exactly as received. A cursor that cannot be read returns 400 (`Invalid cursor`).",
        "schema": { "type": "string", "maxLength": 400 }
      },
      "Since": {
        "name": "since",
        "in": "query",
        "required": false,
        "description": "Only contacts first seen at or after this instant. An RFC 3339 date-time with `Z` or an offset, for example `2026-10-01T00:00:00Z`; URL-encode a `+` offset as `%2B`. A date without a time is refused.",
        "schema": { "type": "string", "format": "date-time" }
      },
      "From": {
        "name": "from",
        "in": "query",
        "required": false,
        "description": "First calendar day, inclusive, in each branch's own time zone (`YYYY-MM-DD`). Must be a real day, and not after `to`.",
        "schema": { "type": "string", "format": "date" }
      },
      "To": {
        "name": "to",
        "in": "query",
        "required": false,
        "description": "Last calendar day, inclusive, in each branch's own time zone (`YYYY-MM-DD`). Must be a real day, and not before `from`.",
        "schema": { "type": "string", "format": "date" }
      },
      "Status": {
        "name": "status",
        "in": "query",
        "required": false,
        "description": "Only bookings with this status.",
        "schema": { "$ref": "#/components/schemas/BookingStatus" }
      },
      "ContactId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "A contact `id` from a contacts response. A malformed id returns 404, like an unknown one.",
        "schema": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$" }
      },
      "BookingId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "A booking `id` from a bookings response. A malformed id returns 404, like an unknown one.",
        "schema": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$" }
      }
    },
    "headers": {
      "XRateLimitLimit": {
        "description": "Requests this key may make per clock minute. On a 429 from the per-address limit, that limit instead.",
        "schema": { "type": "integer", "example": 60 }
      },
      "XRateLimitRemaining": {
        "description": "Requests this key has left in the current minute. On a 429 from the per-address limit, what is left of that limit (0).",
        "schema": { "type": "integer", "minimum": 0, "example": 59 }
      },
      "XRateLimitReset": {
        "description": "Seconds until this key's minute starts over (at most 60). On a 429 from the per-address limit, the seconds until that limit's window starts over.",
        "schema": { "type": "integer", "minimum": 0, "example": 42 }
      },
      "RetryAfter": {
        "description": "Seconds to wait before the next request.",
        "schema": { "type": "integer", "example": 60 }
      },
      "CacheControl": {
        "description": "Always `no-store`: API responses are never cached.",
        "schema": { "type": "string", "const": "no-store" }
      },
      "WWWAuthenticate": {
        "description": "The authentication scheme the API expects.",
        "schema": { "type": "string", "const": "Bearer realm=\"mujib\"" }
      }
    },
    "responses": {
      "ValidationError": {
        "description": "A query parameter is invalid: an unknown parameter, a `limit` out of range, a malformed or impossible date, `from` after `to`, an unknown `status`, or a cursor that cannot be read.",
        "headers": {
          "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
          "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
          "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "examples": {
              "outOfRange": {
                "summary": "limit above the maximum",
                "value": {
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "Invalid input",
                    "issues": [
                      { "code": "too_big", "maximum": 100, "type": "number", "inclusive": true, "exact": false, "message": "Number must be less than or equal to 100", "path": ["limit"] }
                    ],
                    "issueCount": 1
                  }
                }
              },
              "unknownParameter": {
                "summary": "a parameter the endpoint does not take",
                "value": {
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "Invalid input",
                    "issues": [
                      { "code": "unrecognized_keys", "keys": ["page"], "path": [], "message": "Unrecognized key(s) in object: 'page'" }
                    ],
                    "issueCount": 1
                  }
                }
              },
              "invalidCursor": {
                "summary": "a cursor that cannot be read",
                "value": { "error": { "code": "VALIDATION_ERROR", "message": "Invalid cursor" } }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No API key, or a key that is malformed, unknown, revoked or expired, or that belongs to a workspace that is not active.",
        "headers": {
          "WWW-Authenticate": { "$ref": "#/components/headers/WWWAuthenticate" },
          "Cache-Control": { "$ref": "#/components/headers/CacheControl" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "UNAUTHORIZED", "message": "A valid API key is required (Authorization: Bearer mjb_live_…)." } }
          }
        }
      },
      "Forbidden": {
        "description": "The key is valid but does not have the scope this endpoint needs.",
        "headers": {
          "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
          "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
          "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "FORBIDDEN", "message": "This key does not have the bookings.read scope." } }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Either this key used its requests for the current minute (the headers then show the key's budget), or the wider per-address limit was reached (the headers then show that limit's own numbers). Wait `Retry-After` seconds.",
        "headers": {
          "Retry-After": { "$ref": "#/components/headers/RetryAfter" },
          "X-RateLimit-Limit": { "$ref": "#/components/headers/XRateLimitLimit" },
          "X-RateLimit-Remaining": { "$ref": "#/components/headers/XRateLimitRemaining" },
          "X-RateLimit-Reset": { "$ref": "#/components/headers/XRateLimitReset" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "examples": {
              "perKey": {
                "summary": "the key's own limit",
                "value": { "error": { "code": "RATE_LIMITED", "message": "At most 60 requests a minute per key." } }
              },
              "perAddress": {
                "summary": "the per-address limit",
                "value": { "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded, retry in 1 minute" } }
              }
            }
          }
        }
      },
      "Internal": {
        "description": "An unexpected server error. Retry later.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "INTERNAL", "message": "Something went wrong" } }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The envelope of every error response.",
        "required": ["error"],
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "additionalProperties": false,
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable, machine-readable: VALIDATION_ERROR (400), UNAUTHORIZED (401), FORBIDDEN (403), NOT_FOUND (404), RATE_LIMITED (429), INTERNAL (500). Branch on this, not on `message`.",
                "examples": ["VALIDATION_ERROR", "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "RATE_LIMITED", "INTERNAL"]
              },
              "message": {
                "type": "string",
                "description": "A human-readable explanation, in English. It may change; do not parse it."
              },
              "issues": {
                "type": "array",
                "description": "Only on a 400 for an invalid query parameter: the first problems found, each naming the parameter in `path`.",
                "items": {
                  "type": "object",
                  "required": ["code", "message", "path"],
                  "properties": {
                    "code": { "type": "string", "description": "The kind of problem, for example `too_big`, `invalid_string`, `invalid_enum_value`, `unrecognized_keys`, `custom`." },
                    "message": { "type": "string" },
                    "path": { "type": "array", "items": { "type": ["string", "integer"] }, "description": "The parameter the problem is about (empty for an unknown parameter, which is named in `keys`)." }
                  },
                  "additionalProperties": true
                }
              },
              "issueCount": {
                "type": "integer",
                "minimum": 1,
                "description": "Only with `issues`: how many problems there were in all."
              }
            }
          }
        }
      },
      "Scope": {
        "type": "string",
        "enum": ["contacts.read", "bookings.read"],
        "description": "`contacts.read` opens the contacts endpoints, `bookings.read` the bookings endpoints."
      },
      "Me": {
        "type": "object",
        "required": ["workspace", "key"],
        "additionalProperties": false,
        "properties": {
          "workspace": {
            "type": "object",
            "required": ["name"],
            "additionalProperties": false,
            "properties": {
              "name": { "type": "string", "description": "The workspace (business) name." }
            }
          },
          "key": {
            "type": "object",
            "required": ["name", "scopes"],
            "additionalProperties": false,
            "properties": {
              "name": { "type": "string", "description": "The name given to the key when it was created." },
              "scopes": { "type": "array", "items": { "$ref": "#/components/schemas/Scope" }, "description": "What the key may read." }
            }
          }
        }
      },
      "ContactStage": {
        "type": "string",
        "enum": ["new", "engaged", "qualified", "won", "lost"],
        "description": "The contact's pipeline stage, shown in the dashboard as New, Engaged, Qualified, Won and Lost."
      },
      "Contact": {
        "type": "object",
        "required": ["id", "name", "phone", "channel", "stage", "tags", "createdAt", "lastActiveAt"],
        "additionalProperties": false,
        "properties": {
          "id": { "type": "string", "description": "Opaque, stable id." },
          "name": { "type": ["string", "null"], "description": "The customer's name, or `null` when none is known." },
          "phone": { "type": ["string", "null"], "description": "The phone number as stored (often international digits without `+`, such as `962790000000`), or `null` when none is known (common for Messenger and Instagram, whose messages bring no phone number into Mujib)." },
          "channel": { "type": "string", "description": "Where the contact came from: `whatsapp`, `facebook` (Messenger), `instagram`, `manual` (added in the dashboard) or `import` (imported from a file)." },
          "stage": { "$ref": "#/components/schemas/ContactStage" },
          "tags": { "type": "array", "items": { "type": "string" }, "description": "The contact's tags; an empty array when none." },
          "createdAt": { "type": "string", "format": "date-time", "description": "When the contact was first seen, in UTC with milliseconds." },
          "lastActiveAt": { "type": "string", "format": "date-time", "description": "The contact's last activity, in UTC with milliseconds." }
        }
      },
      "ContactList": {
        "type": "object",
        "required": ["data", "nextCursor"],
        "additionalProperties": false,
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Contact" } },
          "nextCursor": { "type": ["string", "null"], "description": "Pass as `cursor` to get the next page; `null` on the last page." }
        }
      },
      "ContactResponse": {
        "type": "object",
        "required": ["data"],
        "additionalProperties": false,
        "properties": {
          "data": { "$ref": "#/components/schemas/Contact" }
        }
      },
      "BookingStatus": {
        "type": "string",
        "enum": ["requested", "confirmed", "cancelled", "completed", "no_show"],
        "description": "The booking's status, shown in the dashboard calendar as Pending (`requested`), Confirmed, Cancelled, Attended (`completed`) and No-show (`no_show`)."
      },
      "Booking": {
        "type": "object",
        "required": ["id", "status", "source", "service", "startsAt", "localTime", "timezone", "durationMinutes", "branch", "specialist", "customer", "createdAt"],
        "additionalProperties": false,
        "properties": {
          "id": { "type": "string", "description": "Opaque, stable id." },
          "status": { "$ref": "#/components/schemas/BookingStatus" },
          "source": { "type": "string", "enum": ["agent", "staff"], "description": "Who made the booking: `agent` (the AI agent, in a conversation) or `staff` (someone in the dashboard)." },
          "service": { "type": "string", "description": "The visit type's id, as the business defined it in its settings (for example `consultation`). The id, not the display label." },
          "startsAt": { "type": "string", "format": "date-time", "description": "The start as a UTC instant, without milliseconds: `YYYY-MM-DDTHH:MM:SSZ`." },
          "localTime": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$", "description": "The start as the branch's wall-clock time, with no offset: `YYYY-MM-DDTHH:MM:SS`. This is the time the customer was given." },
          "timezone": { "type": "string", "description": "The IANA time zone `localTime` is in: the branch's zone, else the workspace's default.", "examples": ["Asia/Amman"] },
          "durationMinutes": { "type": ["integer", "null"], "description": "Length in minutes, or `null` when the business's default length applies." },
          "branch": {
            "type": ["object", "null"],
            "description": "The branch, or `null` when the booking has none.",
            "required": ["id", "name"],
            "additionalProperties": false,
            "properties": {
              "id": { "type": "string" },
              "name": { "type": ["string", "null"] }
            }
          },
          "specialist": { "type": ["string", "null"], "description": "The specialist's name, or `null` when none is assigned." },
          "customer": {
            "type": "object",
            "required": ["contactId", "name", "phone"],
            "additionalProperties": false,
            "properties": {
              "contactId": { "type": ["string", "null"], "description": "The contact's `id` (for `GET /api/v1/contacts/{id}`), or `null` for a walk-in booked without a contact." },
              "name": { "type": ["string", "null"], "description": "The name written on the booking, else the contact's name; `null` when neither is known." },
              "phone": { "type": ["string", "null"], "description": "The phone written on the booking, else the contact's phone; `null` when neither is known." }
            }
          },
          "createdAt": { "type": "string", "format": "date-time", "description": "When the booking was made, in UTC with milliseconds." }
        }
      },
      "BookingList": {
        "type": "object",
        "required": ["data", "nextCursor"],
        "additionalProperties": false,
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Booking" } },
          "nextCursor": { "type": ["string", "null"], "description": "Pass as `cursor` to get the next page; `null` on the last page." }
        }
      },
      "BookingResponse": {
        "type": "object",
        "required": ["data"],
        "additionalProperties": false,
        "properties": {
          "data": { "$ref": "#/components/schemas/Booking" }
        }
      }
    }
  }
}
