Book (unified)
One booking call that also buys a session pack, spends one, or issues a queue token.
POST /v1/appointments/book-appointment-unifiedThe 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.
bookingType | Use it when | Also required | Charge |
|---|---|---|---|
ONE_TIME_APPOINTMENT | A normal paid visit | consultationCharge | What you send |
PACKAGE_PURCHASE | The patient buys a pack and takes their first session now | packageConfigId, packageAmount | Overridden to the pack |
USE_ACTIVE_PACKAGE | The visit is drawn from a pack they already own | patientPackageId | Zero — 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.
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId | number | Yes | Your workspace. |
addressId | number | Yes | The clinic location. workspaceAddressId is accepted as an alias. |
doctorId | number | Yes | The practitioner. |
patientId | number | Yes* | *Or a patientPayload. |
system | string | No | SCHEDULED (default) or QMS. |
bookingType | string | Yes | One of the three above. Not needed when system is QMS. |
mode | string | Yes | OFFLINE, ONLINE, HOME_VISIT, HOME_COLLECTION, WALK_IN. |
fromDateTimeIso | string | Yes | Slot start, ISO 8601 with an offset. |
toDateTimeIso | string | Yes | Slot end, same form. |
paymentMode | string | Yes | CASH, CARD, UPI, BANK_TRANSFER, ONLINE, OTHER, or a configured method name. |
source | string | Yes | DEVELOPER_API from a server integration. |
consultationCharge | number | Conditional | Required for ONE_TIME_APPOINTMENT, except a follow-up. |
packageConfigId | number | Conditional | Required for PACKAGE_PURCHASE. |
packageAmount | number | Conditional | Required for PACKAGE_PURCHASE. |
patientPackageId | number | Conditional | Required for USE_ACTIVE_PACKAGE. |
currency | string | No | ISO 4217. Defaults to the location's currency. |
discountAmount | number | No | ONE_TIME_APPOINTMENT and PACKAGE_PURCHASE only — must be 0 on USE_ACTIVE_PACKAGE. |
discountReason | string | No | ≤ 200 chars. |
paymentStatus | string | No | Defaults to UNPAID for a one-time visit; the pack paths land PAID. |
paymentReference | string | No | Your transaction id, when already paid. |
type | string | No | CONSULTATION, EMERGENCY, … A FOLLOW_UP sent here is booked as CONSULTATION, and parentAppointmentId is dropped. |
followUp | boolean | No | true 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. |
consultationDurationTierId | number | No | ONE_TIME_APPOINTMENT only. Resolves fee and length from the tier. |
roomId | number | No | Rejected for ONLINE. |
appointmentNotes, chiefComplaint | string | No | ≤ 500 chars each. |
patientTimezone | string | No | IANA zone for the patient's confirmations and reminders. |
bookingChannel | string | No | WALK_IN, PHONE_CALL, WHATSAPP, SELF_QR, … |
vital | object | No | Intake vitals, saved in the same transaction. |
qms | object | Conditional | Required when system is QMS. See below. |
Try it
/v1/appointments/book-appointment-unifiedBody
POST /v1/appointments/book-appointment-unified{
"addressId": "1",
"doctorId": "4",
"bookingType": "CONSULTATION",
"mode": "OFFLINE",
"paymentMode": "CASH"
}https://api.medos.oneUse 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.
Pre-book a token
A token on a shift that has not started.
Immediate token
Join the queue that is running now.
Common failures
| Status | Cause |
|---|---|
400 | bookingType missing on a scheduled booking, or the field that type requires is absent — packageConfigId + packageAmount, or patientPackageId. |
400 | discountAmount non-zero on USE_ACTIVE_PACKAGE, or greater than the amount it discounts. |
400 | An unknown property. Fields from the plain booking route that do not exist here — such as paymentCollectionType — are rejected, not ignored. |
400 | system: "QMS" with no qms body. |
400 | followUp: true with bookingType PACKAGE_PURCHASE or USE_ACTIVE_PACKAGE — a follow-up is booked as a single visit. |
400 | FOLLOW_UP_NOT_ELIGIBLE — the patient has no qualifying visit to follow up. |
403 | The OTP gate. |
Not idempotent
A retry books again. Use reserve-slot
when you need an Idempotency-Key.