Server-side APIEndpointsAppointments

Book (unified)

One booking call that also buys a session pack, spends one, or issues a queue token.

POST /v1/appointments/book-appointment-unified

The canonical write path. It books the same scheduled visit as book-appointment, but adds the two things that route cannot express — buying a session pack in the same transaction, and drawing a visit from a pack the patient already owns — and can route the booking into the queue system instead of the calendar.

OTP-gated

The patient's phone must have been verified in the last 30 minutes, and this request must carry the same x-end-user-id as that verification. See OTP-gated routes. Calling with an x-api-key instead of a session token? The gate does not apply — see Booking without OTP.

Which booking type

bookingType is required for scheduled bookings and decides what else you must send.

bookingTypeUse it whenAlso requiredCharge
ONE_TIME_APPOINTMENTA normal paid visitconsultationChargeWhat you send
PACKAGE_PURCHASEThe patient buys a pack and takes their first session nowpackageConfigId, packageAmountOverridden to the pack
USE_ACTIVE_PACKAGEThe visit is drawn from a pack they already ownpatientPackageIdZero — the pack funds it

packageConfigId comes from the pack catalog; patientPackageId comes from the patient's active packs on search-by-phone or from plans[] on available slots.

Body

Patient identification works exactly as on book-appointment: patientId, patientPayload.id, or a patientPayload object to find-or-create.

FieldTypeRequiredDescription
workspaceIdnumberYesYour workspace.
addressIdnumberYesThe clinic location. workspaceAddressId is accepted as an alias.
doctorIdnumberYesThe practitioner.
patientIdnumberYes**Or a patientPayload.
systemstringNoSCHEDULED (default) or QMS.
bookingTypestringYesOne of the three above. Not needed when system is QMS.
modestringYesOFFLINE, ONLINE, HOME_VISIT, HOME_COLLECTION, WALK_IN.
fromDateTimeIsostringYesSlot start, ISO 8601 with an offset.
toDateTimeIsostringYesSlot end, same form.
paymentModestringYesCASH, CARD, UPI, BANK_TRANSFER, ONLINE, OTHER, or a configured method name.
sourcestringYesDEVELOPER_API from a server integration.
consultationChargenumberConditionalRequired for ONE_TIME_APPOINTMENT, except a follow-up.
packageConfigIdnumberConditionalRequired for PACKAGE_PURCHASE.
packageAmountnumberConditionalRequired for PACKAGE_PURCHASE.
patientPackageIdnumberConditionalRequired for USE_ACTIVE_PACKAGE.
currencystringNoISO 4217. Defaults to the location's currency.
discountAmountnumberNoONE_TIME_APPOINTMENT and PACKAGE_PURCHASE only — must be 0 on USE_ACTIVE_PACKAGE.
discountReasonstringNo≤ 200 chars.
paymentStatusstringNoDefaults to UNPAID for a one-time visit; the pack paths land PAID.
paymentReferencestringNoYour transaction id, when already paid.
typestringNoCONSULTATION, EMERGENCY, … A FOLLOW_UP sent here is booked as CONSULTATION, and parentAppointmentId is dropped.
followUpbooleanNotrue books a follow-up, linked to the patient's qualifying visit by the gateway. Omit bookingType or send ONE_TIME_APPOINTMENT. See Booking a follow-up.
consultationDurationTierIdnumberNoONE_TIME_APPOINTMENT only. Resolves fee and length from the tier.
roomIdnumberNoRejected for ONLINE.
appointmentNotes, chiefComplaintstringNo≤ 500 chars each.
patientTimezonestringNoIANA zone for the patient's confirmations and reminders.
bookingChannelstringNoWALK_IN, PHONE_CALL, WHATSAPP, SELF_QR, …
vitalobjectNoIntake vitals, saved in the same transaction.
qmsobjectConditionalRequired when system is QMS. See below.

Try it

POST/v1/appointments/book-appointment-unified

Body

Request
POST /v1/appointments/book-appointment-unified
{
  "addressId": "1",
  "doctorId": "4",
  "bookingType": "CONSULTATION",
  "mode": "OFFLINE",
  "paymentMode": "CASH"
}
to https://api.medos.one
Use a dedicated test key, and put your browser's address on its allowlist

A Developer API key authenticates on its own, so it only works from the addresses registered against it — and this page calls from your browser, not your servers. Unless your own public address is on the list you get a 403 naming it, which is the allowlist doing its job. Your browser may also reach us over IPv6 even when your server does not, so the address in the error is often not the one you expected. The key here is kept in memory only and never written to storage, but create a test key for it and deactivate that key when you are done.

Request

A pack purchase that also books the first session:

curl -sX POST "https://api.medos.one/v1/appointments/book-appointment-unified" \
  -H "x-api-key: $MEDOS_API_KEY" \
  -H "x-end-user-id: $PATIENT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": 1,
    "addressId": 1,
    "doctorId": 4,
    "patientId": 774,
    "system": "SCHEDULED",
    "bookingType": "PACKAGE_PURCHASE",
    "packageConfigId": 12,
    "packageAmount": 4500,
    "currency": "INR",
    "mode": "OFFLINE",
    "fromDateTimeIso": "2026-09-20T09:00:00+05:30",
    "toDateTimeIso": "2026-09-20T09:30:00+05:30",
    "paymentMode": "UPI",
    "paymentStatus": "PAID",
    "paymentReference": "txn_88213",
    "source": "DEVELOPER_API"
  }'

Response

A discriminated result — system says which half is populated:

{
  "system": "SCHEDULED",
  "appointment": {
    "workspaceId": 1,
    "patientId": 774,
    "bookingReference": "MED-9182-AK",
    "appointmentDate": "2026-09-20",
    "fromDateTimeTs": "2026-09-20T09:00:00+05:30",
    "toDateTimeTs": "2026-09-20T09:30:00+05:30",
    "appointmentTimezone": "Asia/Kolkata",
    "consultationCharge": 0,
    "currency": "INR",
    "paymentStatus": "PAID"
  },
  "visit": null
}

appointment is the same payload book-appointment returns. For system: "QMS" it is visit that is populated instead, carrying tokenNumber, tokenDisplay, position and publicStatusToken.

Booking into the queue

Set system: "QMS" and nest the queue booking under qms. The top-level workspaceId, addressId, doctorId and patientId are copied down for you, so the nested object carries only the queue-specific fields — qmsWorkShiftId, appointmentDate, mode and bookingChannel at minimum.

In practice, prefer the dedicated routes: they take the same fields flat, and they are what the queue documentation describes.

Common failures

StatusCause
400bookingType missing on a scheduled booking, or the field that type requires is absent — packageConfigId + packageAmount, or patientPackageId.
400discountAmount non-zero on USE_ACTIVE_PACKAGE, or greater than the amount it discounts.
400An unknown property. Fields from the plain booking route that do not exist here — such as paymentCollectionType — are rejected, not ignored.
400system: "QMS" with no qms body.
400followUp: true with bookingType PACKAGE_PURCHASE or USE_ACTIVE_PACKAGE — a follow-up is booked as a single visit.
400FOLLOW_UP_NOT_ELIGIBLE — the patient has no qualifying visit to follow up.
403The OTP gate.

Not idempotent

A retry books again. Use reserve-slot when you need an Idempotency-Key.

On this page