Skip to main content

OdontoX Mobile API — Complete Specification

Base URL: https://api.odontox.io/api/v1/protected
Auth: Authorization: Bearer {JWT_TOKEN}
Last Updated: 2026-05-10

Table of Contents

  1. Appointments API
  2. Messages API
  3. Mobile Permissions API
  4. Common Patterns

Appointments API

Shared Schema

All appointment objects contain these fields:
{
  id: string (UUID);
  clinicId: string (UUID);
  patientId: string (UUID);
  doctorId?: string (UUID);
  appointmentDate: string (YYYY-MM-DD);
  appointmentTime: string (HH:MM, 24h format);
  appointmentType: string;
  durationMinutes: number;
  status: 'requested' | 'scheduled' | 'confirmed' | 'in_progress' | 'completed' | 'cancelled' | 'no_show';
  operatory?: string;
  roomId?: string (UUID);
  notes?: string;
  createdAt: string (ISO 8601);
  updatedAt: string (ISO 8601);
  // Enriched in responses:
  patientName?: string;
  patientPhone?: string;
  patientEmail?: string;
  patientNumber?: string;
  patientInsurance?: string;
  doctorName?: string;
  invoiceId?: string (UUID);
  invoiceStatus?: string;
  invoiceBalance?: number;
  invoiceTotalAmount?: number;
  roomName?: string;
  roomColor?: string;
}

GET /appointments

Query Parameters:
  • startDate (optional, YYYY-MM-DD) — filter appointments on or after this date
  • endDate (optional, YYYY-MM-DD) — filter appointments on or before this date
  • limit (optional, 1-100, default 50) — pagination limit
  • offset (optional, default 0) — pagination offset
  • doctorId (optional, UUID) — filter by doctor
Response:
{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "clinicId": "550e8400-e29b-41d4-a716-446655440001",
      "patientId": "550e8400-e29b-41d4-a716-446655440002",
      "doctorId": "550e8400-e29b-41d4-a716-446655440003",
      "appointmentDate": "2026-05-15",
      "appointmentTime": "14:30",
      "appointmentType": "Checkup",
      "durationMinutes": 30,
      "status": "confirmed",
      "operatory": "Room 1",
      "roomId": "550e8400-e29b-41d4-a716-446655440004",
      "notes": "Patient complained of sensitivity",
      "patientName": "Ahmed Hassan",
      "patientPhone": "+92-300-1234567",
      "patientEmail": "[email protected]",
      "patientNumber": "P-2026-001",
      "patientInsurance": "PMDC",
      "doctorName": "Dr. Fatima Khan",
      "invoiceId": "550e8400-e29b-41d4-a716-446655440005",
      "invoiceStatus": "paid",
      "invoiceBalance": 0,
      "invoiceTotalAmount": 5000,
      "roomName": "Room 1",
      "roomColor": "#FF5733",
      "createdAt": "2026-05-09T10:30:00.000Z",
      "updatedAt": "2026-05-10T08:15:00.000Z"
    }
  ],
  "pagination": {
    "limit": 50,
    "offset": 0,
    "count": 1
  }
}

GET /appointments/available-slots

Required Query Parameters:
  • date (YYYY-MM-DD) — the date to check availability for
Optional Query Parameters:
  • doctorId (UUID) — filter available slots for a specific doctor
Response:
{
  "success": true,
  "date": "2026-05-15",
  "doctorId": "550e8400-e29b-41d4-a716-446655440003",
  "slots": [
    {
      "time": "09:00",
      "available": true,
      "doctorId": "550e8400-e29b-41d4-a716-446655440003",
      "doctorName": "Dr. Fatima Khan",
      "duration": 30,
      "bufferMinutes": 30,
      "notes": "Morning slot"
    },
    {
      "time": "09:30",
      "available": true,
      "doctorId": "550e8400-e29b-41d4-a716-446655440003",
      "doctorName": "Dr. Fatima Khan",
      "duration": 30,
      "bufferMinutes": 30,
      "notes": "Morning slot"
    },
    {
      "time": "14:00",
      "available": false,
      "doctorId": "550e8400-e29b-41d4-a716-446655440003",
      "doctorName": "Dr. Fatima Khan",
      "reason": "Doctor already has appointment"
    },
    {
      "time": "14:30",
      "available": true,
      "doctorId": "550e8400-e29b-41d4-a716-446655440003",
      "doctorName": "Dr. Fatima Khan",
      "duration": 30,
      "bufferMinutes": 30,
      "notes": "Afternoon slot"
    }
  ],
  "clinicHours": {
    "openTime": "09:00",
    "closeTime": "17:00",
    "isClosed": false
  },
  "bufferMinutes": 30,
  "note": "30-minute buffer applied between appointments"
}
Important Notes:
  • 30-minute buffer is applied on both /appointments and /available-slots endpoints (deployed 2026-05-09)
  • Past time slots are filtered out automatically
  • Respects clinic operating hours from clinics.operating_hours JSON
  • Returns unavailable slots with reasons

POST /appointments

Request Body:
{
  "patientId": "550e8400-e29b-41d4-a716-446655440002",
  "doctorId": "550e8400-e29b-41d4-a716-446655440003",
  "appointmentDate": "2026-05-15",
  "appointmentTime": "14:30",
  "appointmentType": "Checkup",
  "durationMinutes": 30,
  "status": "scheduled",
  "operatory": "Room 1",
  "roomId": "550e8400-e29b-41d4-a716-446655440004",
  "notes": "Patient requested morning appointment if possible"
}
Important Behavior:
  • Patient role users automatically force status: 'requested' (staff approval required)
  • Staff (doctor/receptionist) can create status: 'scheduled' directly
  • Applies 30-minute buffer validation with existing appointments
  • Checks clinic operating hours and business day
  • Detects doctor + time conflicts
  • Detects operatory/room conflicts
  • Triggers email + WhatsApp confirmation (for scheduled status)
  • Creates notifications for patient, doctor, and receptionists
  • Records activity in audit trail
Response:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "clinicId": "550e8400-e29b-41d4-a716-446655440001",
  "patientId": "550e8400-e29b-41d4-a716-446655440002",
  "doctorId": "550e8400-e29b-41d4-a716-446655440003",
  "appointmentDate": "2026-05-15",
  "appointmentTime": "14:30",
  "appointmentType": "Checkup",
  "durationMinutes": 30,
  "status": "scheduled",
  "operatory": "Room 1",
  "roomId": "550e8400-e29b-41d4-a716-446655440004",
  "notes": "Patient requested morning appointment if possible",
  "createdAt": "2026-05-10T10:30:00.000Z",
  "updatedAt": "2026-05-10T10:30:00.000Z"
}
Error Responses:
// Patient booking their own appointment (automatic status: 'requested')
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "requested"
}

// Time conflict (409)
{
  "message": "Time slot conflict: Doctor already has an appointment from 14:00 to 14:30",
  "code": "SLOT_CONFLICT",
  "conflicts": [
    {
      "appointmentId": "550e8400-e29b-41d4-a716-446655440099",
      "startTime": "14:00",
      "endTime": "14:30",
      "doctorId": "550e8400-e29b-41d4-a716-446655440003"
    }
  ]
}

// Past date (400)
{
  "error": "Cannot move appointments to the past"
}

// Outside business hours (400)
{
  "error": "Clinic is closed on Sundays"
}

GET /appointments/:id

Path Parameters:
  • id (UUID) — appointment ID
Response:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "clinicId": "550e8400-e29b-41d4-a716-446655440001",
  "patientId": "550e8400-e29b-41d4-a716-446655440002",
  "doctorId": "550e8400-e29b-41d4-a716-446655440003",
  "appointmentDate": "2026-05-15",
  "appointmentTime": "14:30",
  "appointmentType": "Checkup",
  "durationMinutes": 30,
  "status": "confirmed",
  "operatory": "Room 1",
  "roomId": "550e8400-e29b-41d4-a716-446655440004",
  "notes": "Patient complained of sensitivity",
  "patientName": "Ahmed Hassan",
  "patientPhone": "+92-300-1234567",
  "patientEmail": "[email protected]",
  "doctorName": "Dr. Fatima Khan",
  "invoiceId": "550e8400-e29b-41d4-a716-446655440005",
  "invoiceStatus": "paid",
  "invoiceBalance": 0,
  "invoiceTotalAmount": 5000,
  "roomName": "Room 1",
  "roomColor": "#FF5733",
  "createdAt": "2026-05-09T10:30:00.000Z",
  "updatedAt": "2026-05-10T08:15:00.000Z"
}

PUT /appointments/:id

Full update endpoint — replace all mutable fields. Request Body:
{
  "appointmentDate": "2026-05-16",
  "appointmentTime": "15:00",
  "appointmentType": "Checkup",
  "durationMinutes": 30,
  "status": "confirmed",
  "doctorId": "550e8400-e29b-41d4-a716-446655440003",
  "operatory": "Room 2",
  "roomId": "550e8400-e29b-41d4-a716-446655440004",
  "notes": "Rescheduled per patient request"
}
Important Behavior:
  • Patients cannot use PUT (403 error) — use PATCH /appointments/:id/status instead
  • Validates all same checks as POST (no past dates, conflicts, business hours)
  • Sends email + WhatsApp rescheduling notifications
  • Records activity as ‘rescheduled’ if date or time changed
  • Does NOT support partial updates — provide all required fields
Response:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "clinicId": "550e8400-e29b-41d4-a716-446655440001",
  "patientId": "550e8400-e29b-41d4-a716-446655440002",
  "doctorId": "550e8400-e29b-41d4-a716-446655440003",
  "appointmentDate": "2026-05-16",
  "appointmentTime": "15:00",
  "appointmentType": "Checkup",
  "durationMinutes": 30,
  "status": "confirmed",
  "operatory": "Room 2",
  "roomId": "550e8400-e29b-41d4-a716-446655440004",
  "notes": "Rescheduled per patient request",
  "createdAt": "2026-05-09T10:30:00.000Z",
  "updatedAt": "2026-05-10T12:00:00.000Z"
}

PATCH /appointments/:id/status

Status-change-only endpoint with optional invoice generation. Request Body:
{
  "status": "completed",
  "reason": "Treatment completed successfully",
  "operatory": "Room 1",
  "roomId": "550e8400-e29b-41d4-a716-446655440004",
  "invoice": {
    "items": [
      {
        "description": "Tooth whitening",
        "quantity": 1,
        "unitPrice": 5000,
        "cost": 5000
      },
      {
        "description": "Scaling and polishing",
        "quantity": 1,
        "unitPrice": 3000
      }
    ],
    "discountAmount": 500,
    "taxAmount": 800,
    "notes": "10% discount applied",
    "dueDate": "2026-06-15"
  }
}
Valid Status Transitions:
  • requestedscheduled, confirmed, cancelled
  • scheduledconfirmed, in_progress, cancelled
  • confirmedin_progress, cancelled
  • in_progresscompleted, cancelled
  • completed → (immutable, but can be audited)
  • cancelled → (immutable)
  • no_showcancelled (reschedule with new appointment)
Important Behavior:
  • Invoice auto-generation if invoice object provided
  • Sends email notification to patient
  • Sends WhatsApp notification if configured
  • Records activity as ‘status_changed’
  • Creates clinic notifications for admins/receptionists
  • Updates audit trail
Response:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "clinicId": "550e8400-e29b-41d4-a716-446655440001",
  "patientId": "550e8400-e29b-41d4-a716-446655440002",
  "doctorId": "550e8400-e29b-41d4-a716-446655440003",
  "appointmentDate": "2026-05-15",
  "appointmentTime": "14:30",
  "appointmentType": "Checkup",
  "durationMinutes": 30,
  "status": "completed",
  "operatory": "Room 1",
  "roomId": "550e8400-e29b-41d4-a716-446655440004",
  "notes": "Treatment completed successfully",
  "createdAt": "2026-05-09T10:30:00.000Z",
  "updatedAt": "2026-05-10T16:00:00.000Z",
  "invoiceId": "550e8400-e29b-41d4-a716-446655440005"
}

DELETE /appointments/:id

Soft-delete: marks appointment as ‘cancelled’. Response:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "clinicId": "550e8400-e29b-41d4-a716-446655440001",
  "patientId": "550e8400-e29b-41d4-a716-446655440002",
  "appointmentDate": "2026-05-15",
  "appointmentTime": "14:30",
  "appointmentType": "Checkup",
  "durationMinutes": 30,
  "status": "cancelled",
  "operatory": "Room 1",
  "roomId": "550e8400-e29b-41d4-a716-446655440004",
  "notes": "Cancelled by patient request",
  "createdAt": "2026-05-09T10:30:00.000Z",
  "updatedAt": "2026-05-10T16:30:00.000Z"
}

Messages API

Shared Message Schema

{
  id: string (UUID);
  clinicId: string (UUID);
  patientId?: string (UUID);
  recipientId?: string (UUID);
  sentBy: string (UUIDuser ID);
  type: 'sms' | 'email' | 'portal';
  direction: 'inbound' | 'outbound';
  subject?: string;
  message: string;
  status: 'unread' | 'read';
  isStarred: boolean;
  readAt?: string (ISO 8601);
  createdAt: string (ISO 8601);
  // Enriched in responses:
  senderFirstName?: string;
  senderLastName?: string;
  senderRole?: string;
  patient?: {
    id: string;
    firstName: string;
    lastName: string;
    patientNumber: string;
  };
  recipient?: {
    id: string;
    firstName: string;
    lastName: string;
    role: string;
  };
}

GET /messages

List all messages with rich filtering and pagination. Query Parameters:
  • patientId (optional, UUID) — filter by patient
  • status (optional, ‘unread’ | ‘read’) — filter by read status
  • type (optional, ‘sms’ | ‘email’ | ‘portal’) — filter by message channel
  • direction (optional, ‘inbound’ | ‘outbound’) — filter by direction
  • page (optional, default 1) — pagination page
  • limit (optional, 1-100, default 50) — results per page
Response:
{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "patientId": "550e8400-e29b-41d4-a716-446655440002",
      "clinicId": "550e8400-e29b-41d4-a716-446655440001",
      "type": "sms",
      "direction": "outbound",
      "subject": "Appointment Confirmation",
      "message": "Your appointment with Dr. Fatima Khan is confirmed for May 15, 2026 at 2:30 PM.",
      "status": "read",
      "isStarred": false,
      "sentBy": "550e8400-e29b-41d4-a716-446655440003",
      "readAt": "2026-05-10T10:15:00.000Z",
      "createdAt": "2026-05-10T09:00:00.000Z",
      "senderFirstName": "Fatima",
      "senderLastName": "Khan",
      "senderRole": "doctor",
      "patient": {
        "id": "550e8400-e29b-41d4-a716-446655440002",
        "firstName": "Ahmed",
        "lastName": "Hassan",
        "patientNumber": "P-2026-001"
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 42,
    "totalPages": 1
  }
}

GET /messages/conversations

Get list of all conversations grouped by participant. Query Parameters:
  • type (optional, ‘patient’ | ‘staff’) — filter conversation type
Response (Staff View):
[
  {
    "id": "patient-550e8400-e29b-41d4-a716-446655440002",
    "participantId": "550e8400-e29b-41d4-a716-446655440002",
    "participantName": "Ahmed Hassan",
    "type": "patient",
    "lastMessage": "Thanks for the reminder",
    "lastMessageTime": "2026-05-10T15:30:00.000Z",
    "lastMessageChannel": "sms",
    "unreadCount": 2,
    "messageCount": 24,
    "hasWhatsApp": true,
    "hasPortal": true
  },
  {
    "id": "staff-550e8400-e29b-41d4-a716-446655440004",
    "participantId": "550e8400-e29b-41d4-a716-446655440004",
    "participantName": "Dr. Hassan Ali",
    "type": "staff",
    "lastMessage": "Can you confirm patient for tomorrow?",
    "lastMessageTime": "2026-05-10T14:00:00.000Z",
    "lastMessageChannel": "portal",
    "unreadCount": 0,
    "messageCount": 5,
    "hasWhatsApp": false,
    "hasPortal": true
  }
]
Response (Patient View):
[
  {
    "id": "patient-550e8400-e29b-41d4-a716-446655440002-Doctor",
    "participantId": "550e8400-e29b-41d4-a716-446655440003",
    "participantName": "Dr. Fatima Khan",
    "type": "Doctor",
    "lastMessage": "Your appointment is confirmed",
    "lastMessageTime": "2026-05-10T10:00:00.000Z",
    "lastMessageChannel": "sms",
    "unreadCount": 0,
    "messageCount": 3,
    "hasWhatsApp": true,
    "hasPortal": false
  },
  {
    "id": "patient-550e8400-e29b-41d4-a716-446655440002-Clinic",
    "participantId": "550e8400-e29b-41d4-a716-446655440001",
    "participantName": "OdontoX Clinic",
    "type": "Clinic",
    "lastMessage": "Appointment reminder: May 15, 2:30 PM",
    "lastMessageTime": "2026-05-09T09:00:00.000Z",
    "lastMessageChannel": "sms",
    "unreadCount": 1,
    "messageCount": 8,
    "hasWhatsApp": true,
    "hasPortal": true
  }
]
Important Notes:
  • Conversations are grouped by participant (not individual messages)
  • Separate conversations for Doctor vs Clinic staff (for patient view)
  • unreadCount is from the logged-in user’s perspective
  • hasWhatsApp and hasPortal indicate available channels
  • Sorted by most recent message time

GET /messages/conversations/:id

Get all messages in a conversation. Path Parameters:
  • id (string) — conversation ID (format: patient-{uuid} or staff-{uuid})
Response:
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "patientId": "550e8400-e29b-41d4-a716-446655440002",
    "clinicId": "550e8400-e29b-41d4-a716-446655440001",
    "type": "portal",
    "direction": "inbound",
    "subject": "Appointment Request",
    "message": "Can I book an appointment for next week?",
    "status": "read",
    "isStarred": false,
    "sentBy": "550e8400-e29b-41d4-a716-446655440002",
    "readAt": "2026-05-10T10:30:00.000Z",
    "createdAt": "2026-05-09T14:00:00.000Z",
    "senderFirstName": "Ahmed",
    "senderLastName": "Hassan",
    "senderRole": "patient"
  },
  {
    "id": "550e8400-e29b-41d4-a716-446655440001",
    "patientId": "550e8400-e29b-41d4-a716-446655440002",
    "clinicId": "550e8400-e29b-41d4-a716-446655440001",
    "type": "portal",
    "direction": "outbound",
    "subject": "RE: Appointment Request",
    "message": "Sure! We have slots available on May 15 at 2:30 PM",
    "status": "read",
    "isStarred": false,
    "sentBy": "550e8400-e29b-41d4-a716-446655440003",
    "readAt": "2026-05-09T15:00:00.000Z",
    "createdAt": "2026-05-09T14:30:00.000Z",
    "senderFirstName": "Fatima",
    "senderLastName": "Khan",
    "senderRole": "doctor"
  }
]

POST /messages

Send a message to a patient or staff member. Request Body:
{
  "patientId": "550e8400-e29b-41d4-a716-446655440002",
  "type": "portal",
  "subject": "Appointment Confirmation",
  "message": "Your appointment is confirmed for May 15, 2026 at 2:30 PM",
  "status": "unread"
}
Alternative (Staff-to-Staff):
{
  "recipientId": "550e8400-e29b-41d4-a716-446655440004",
  "type": "portal",
  "message": "Can you confirm the patient's insurance details?",
  "status": "unread"
}
Important Behavior:
  • Direction inferred from user role (patient = ‘inbound’, staff = ‘outbound’)
  • Patient users automatically set patientId to their own patient record
  • Conversation initialization rules:
    • Patients → Staff: always allowed
    • Receptionists → Patients: always allowed
    • Doctors → Patients: only allowed once per conversation (can reply to existing)
    • Admins/Superadmins: always allowed
  • Email notification sent if:
    • First message ever in conversation, OR
    • Last message > 30 minutes ago (new session)
  • Real-time SSE event published to clinic subscribers
  • Creates clinic notifications for relevant staff
Response:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "clinicId": "550e8400-e29b-41d4-a716-446655440001",
  "patientId": "550e8400-e29b-41d4-a716-446655440002",
  "recipientId": null,
  "sentBy": "550e8400-e29b-41d4-a716-446655440003",
  "type": "portal",
  "direction": "outbound",
  "subject": "Appointment Confirmation",
  "message": "Your appointment is confirmed for May 15, 2026 at 2:30 PM",
  "status": "unread",
  "isStarred": false,
  "readAt": null,
  "createdAt": "2026-05-10T10:30:00.000Z",
  "patient": {
    "id": "550e8400-e29b-41d4-a716-446655440002",
    "firstName": "Ahmed",
    "lastName": "Hassan",
    "patientNumber": "P-2026-001"
  }
}
Error Responses:
// Doctor trying to initiate second conversation (400)
{
  "error": "Cannot initiate conversation",
  "message": "Conversation already exists. You can only send the initial message if no conversation exists."
}

// Patient trying to message staff directly (403)
{
  "error": "Patients cannot send direct messages to staff"
}

// Invalid patient (404)
{
  "error": "Patient not found or not in your clinic"
}

GET /messages/contacts

Get list of contacts the user can message. Response (Patient):
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440003",
    "firstName": "Fatima",
    "lastName": "Khan",
    "role": "doctor",
    "email": "[email protected]",
    "type": "staff",
    "name": "Fatima Khan"
  },
  {
    "id": "550e8400-e29b-41d4-a716-446655440004",
    "firstName": "Zainab",
    "lastName": "Ahmed",
    "role": "receptionist",
    "email": "[email protected]",
    "type": "staff",
    "name": "Zainab Ahmed"
  }
]
Response (Staff):
{
  "patients": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440002",
      "firstName": "Ahmed",
      "lastName": "Hassan",
      "email": "[email protected]",
      "patientNumber": "P-2026-001",
      "type": "patient",
      "contactType": "patient",
      "name": "Ahmed Hassan"
    }
  ],
  "staff": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440003",
      "firstName": "Fatima",
      "lastName": "Khan",
      "role": "doctor",
      "email": "[email protected]",
      "type": "staff",
      "contactType": "staff",
      "name": "Fatima Khan"
    }
  ],
  "all": [
    { "id": "550e8400-e29b-41d4-a716-446655440002", "type": "patient", "contactType": "patient", "name": "Ahmed Hassan" },
    { "id": "550e8400-e29b-41d4-a716-446655440003", "type": "staff", "contactType": "staff", "name": "Fatima Khan" }
  ]
}

GET /messages/receptionists

Get list of receptionists in user’s clinic (for patients to contact). Response:
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440004",
    "email": "[email protected]",
    "firstName": "Zainab",
    "lastName": "Ahmed",
    "role": "receptionist"
  }
]

PUT /messages/:id

Update message metadata (star/unstar, mark read). Request Body:
{
  "status": "read",
  "isStarred": true,
  "readAt": "2026-05-10T10:30:00.000Z"
}
Response:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "clinicId": "550e8400-e29b-41d4-a716-446655440001",
  "patientId": "550e8400-e29b-41d4-a716-446655440002",
  "sentBy": "550e8400-e29b-41d4-a716-446655440003",
  "type": "sms",
  "direction": "outbound",
  "subject": "Appointment Confirmation",
  "message": "Your appointment is confirmed",
  "status": "read",
  "isStarred": true,
  "readAt": "2026-05-10T10:30:00.000Z",
  "createdAt": "2026-05-10T09:00:00.000Z"
}

DELETE /messages/:id

Soft-delete a message (marks as deleted, not physically removed). Response:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "deleted"
}

POST /messages/conversations/:id/read

Mark all messages in a conversation as read. Path Parameters:
  • id (string) — conversation ID (format: patient-{uuid} or staff-{uuid})
Response:
{
  "success": true,
  "conversationId": "patient-550e8400-e29b-41d4-a716-446655440002",
  "messagesMarkedRead": 5,
  "readAt": "2026-05-10T10:30:00.000Z"
}

POST /messages/conversations/:id/send

Send a message to a conversation (alternative to POST /messages). Path Parameters:
  • id (string) — conversation ID
Request Body:
{
  "type": "portal",
  "message": "Thank you for the appointment",
  "subject": "RE: Appointment"
}
Response:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "clinicId": "550e8400-e29b-41d4-a716-446655440001",
  "patientId": "550e8400-e29b-41d4-a716-446655440002",
  "sentBy": "550e8400-e29b-41d4-a716-446655440003",
  "type": "portal",
  "direction": "inbound",
  "subject": "RE: Appointment",
  "message": "Thank you for the appointment",
  "status": "unread",
  "isStarred": false,
  "createdAt": "2026-05-10T10:30:00.000Z"
}

POST /messages/whatsapp/send

Send a WhatsApp message via Zepto SMS gateway. Request Body:
{
  "patientId": "550e8400-e29b-41d4-a716-446655440002",
  "message": "Your appointment reminder: May 15, 2026 at 2:30 PM",
  "type": "sms"
}
Response:
{
  "success": true,
  "messageId": "550e8400-e29b-41d4-a716-446655440000",
  "phoneNumber": "+92-300-1234567",
  "status": "sent",
  "timestamp": "2026-05-10T10:30:00.000Z"
}

Mobile Permissions API

GET /mobile/permissions

Get modules enabled for current user’s role. Response:
{
  "modules": [
    "appointments",
    "medical_history",
    "messages",
    "prescriptions",
    "invoices",
    "notifications"
  ]
}
Default Modules by Role:
RoleModules
patientappointments, medical_history, messages, prescriptions, invoices, notifications
doctorappointments, patients, clinical_notes, dental_charts, prescriptions, messages, invoices, treatment_plans, notifications
receptionistappointments, patients, messages, invoices, notifications
adminAll modules

GET /mobile/permissions/clinic/:clinicId/:role

Get modules for a specific role at a clinic (admin endpoint). Response:
[
  { "module": "appointments", "label": "appointments", "enabled": true },
  { "module": "messages", "label": "messages", "enabled": true },
  { "module": "prescriptions", "label": "prescriptions", "enabled": false },
  { "module": "invoices", "label": "invoices", "enabled": true }
]

PATCH /mobile/permissions/clinic/:clinicId/:role/:module

Toggle a module for a role at a clinic (admin endpoint). Request Body:
{
  "enabled": false
}
Response:
{
  "ok": true,
  "module": "prescriptions",
  "role": "receptionist",
  "clinic": "550e8400-e29b-41d4-a716-446655440001",
  "enabled": false,
  "updatedAt": "2026-05-10T10:30:00.000Z"
}

Common Patterns

Authentication

All requests require the Authorization header:
curl -H "Authorization: Bearer {JWT_TOKEN}" \
  https://api.odontox.io/api/v1/protected/appointments

Error Responses

Standard error format:
{
  "error": "Error type",
  "message": "Detailed error message",
  "code": "ERROR_CODE",
  "timestamp": "2026-05-10T10:30:00.000Z"
}
Common HTTP Status Codes:
CodeMeaning
200Success
201Created
400Bad request (validation error)
403Forbidden (insufficient permissions)
404Not found
409Conflict (e.g., appointment time conflict)
500Server error

Pagination

Responses with pagination include:
{
  "data": [...],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 150,
    "totalPages": 3
  }
}

Date/Time Format

  • Dates: YYYY-MM-DD (e.g., 2026-05-15)
  • Times: HH:MM 24-hour format (e.g., 14:30)
  • Timestamps: ISO 8601 (e.g., 2026-05-10T10:30:00.000Z)

Clinic Context

Multi-clinic users can specify clinic with header:
curl -H "Authorization: Bearer {JWT_TOKEN}" \
  -H "X-Clinic-Id: {CLINIC_UUID}" \
  https://api.odontox.io/api/v1/protected/appointments

Summary of Query Contracts

Message Query Contracts

EndpointQuery SupportPurpose
GET /messagespatientId, status, type, direction, page, limitList all messages with filters
GET /messages/conversationstype (‘patient’ | ‘staff’)Group conversations by participant
GET /messages/conversations/:idNone (uses path param)Get messages in a conversation
GET /messages/receptionistsNoneList clinic receptionists
GET /messages/contactsNoneList messageable contacts
GET /messages/:idNone (uses path param)Get single message

Answer to User Questions

Q: Does GET /messages support conversationId/threadId?
A: No. Queries use patientId only. Use GET /messages/conversations to get conversation groupings, then GET /messages/conversations/:id to get thread messages.
Q: Is /mobile/permissions still needed?
A: Yes. It’s used to determine which modules are available in the mobile app per role. Current response: { modules: string[] } listing enabled features.
Q: Should I use PATCH /appointments/:id or PUT /appointments/:id?
A:
  • PATCH /appointments/:id/status — for status changes only (preferred for mobile)
  • PUT /appointments/:id — for full appointment updates (requires all fields, not mobile-friendly)

Deployment Notes

Last Deployed Features:
  • ✅ 30-minute buffer on /appointments + /available-slots (2026-05-09)
  • ✅ Message direction inference from user role (2026-05-09)
  • ✅ Doctor schedule integration for slot availability
  • ✅ WhatsApp + Email notifications on appointment changes
  • ✅ Real-time SSE events for messages
  • ✅ Conversation grouping with separate Doctor/Clinic threads for patients

Updated: 2026-05-10
Maintainer: Development Team