Pre-book a token
Reserve a queue token on a shift that has not started yet.
POST /v1/qms-appointments/pre/bookBooks a numbered token in a queue shift before the queue opens — the patient
turns up later and is called in token order. Use it for any shift whose
isStarted is still false.
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.
Identifying the patient
As with the scheduled routes: send patientId, or a patientPayload object to
find-or-create the record. The inline patient objects are stripped before the
booking — the queue books by patientId only.
Body
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId | number | Yes | Your workspace. |
addressId | number | Yes | The clinic location. |
doctorId | number | Yes | The practitioner. |
patientId | number | Yes* | *Or a patientPayload to create one. |
qmsWorkShiftId | number | Yes | The shift, from doctor shifts. |
appointmentDate | string | Yes | YYYY-MM-DD. Must not be in the past. |
mode | string | Yes | OFFLINE or ONLINE. |
bookingChannel | string | Yes | How the patient reached you: WALK_IN, PHONE_CALL, WHATSAPP, EMAIL, TELEGRAM, INTERNAL_REFERRAL, EXTERNAL_REFERRAL, SELF_QR. |
isInternational | boolean | No | true charges the shift's internationalFee in USD instead of the domestic fee. Defaults to false. |
discountAmount | number | No | Flat discount, must not exceed the base fee. |
discountReason | string | No | Up to 100 characters. |
couponCode | string | No | Validated against the clinic's coupons; overrides discountAmount. |
type | string | No | NEW or FOLLOW_UP. |
followUpOfAppointmentId | number | No | Required when type is FOLLOW_UP. Must be a COMPLETED visit with the same patient and doctor. |
paymentMode | string | No | CASH, UPI, … or a configured method name. |
paymentMethodId | number | No | A configured payment method, preferred over the string. |
paymentStatus | string | No | Defaults to PENDING. PAID also persists paymentReference. |
paymentReference | string | No | Stored only when paymentStatus is PAID. |
source | string | No | DEVELOPER_API from a server integration. |
patientPhoneNumber | string | No | Recorded on the visit. |
patientEmail | string | No | Recorded on the visit. |
reasonForVisit | string | No | Up to 500 characters. |
symptoms | string | No | Up to 2000 characters. |
appointmentNotes | string | No | Up to 2000 characters. |
referral | object | No | Required when bookingChannel is a referral channel. |
vital | object | No | Intake vitals, saved in the same transaction. |
Try it
/v1/qms-appointments/pre/bookBody
POST /v1/qms-appointments/pre/book{
"addressId": "1",
"doctorId": "4",
"appointmentDate": "2026-10-12",
"mode": "OFFLINE"
}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
curl -sX POST "https://api.medos.one/v1/qms-appointments/pre/book" \
-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,
"qmsWorkShiftId": 331,
"appointmentDate": "2026-09-20",
"mode": "OFFLINE",
"bookingChannel": "PHONE_CALL",
"paymentMode": "CASH",
"source": "DEVELOPER_API",
"reasonForVisit": "Annual check-up"
}'Response
201, with the visit wrapped in an envelope:
{
"success": true,
"message": "Appointment booked",
"data": {
"id": 5541,
"workspaceId": 1,
"addressId": 1,
"doctorId": 4,
"patientId": 774,
"qmsWorkShiftId": 331,
"appointmentDate": "2026-09-20",
"tokenNumber": 13,
"tokenDisplay": "P-013",
"position": 13,
"status": "SCHEDULED",
"baseFee": 400,
"discountAmount": 0,
"finalAmount": 400,
"currency": "INR",
"paymentStatus": "PENDING",
"publicStatusToken": "qst_8f21c0d4",
"appointmentTimezone": "Asia/Kolkata"
}
}| Field | Meaning |
|---|---|
tokenDisplay | What the patient is told — P-013. Show this, not tokenNumber. |
position | Where they currently stand in the queue. It moves as the day goes on. |
publicStatusToken | An opaque handle for a patient-facing "where am I in the queue" view. |
finalAmount | Base fee minus any discount or coupon, in currency. |
id | The visit's numeric id. |
A queue booking has no times
There is no fromDateTimeIso to send or read. slotStartDateTime on the
response is an estimate the queue maintains, not a commitment.
Common failures
| Status | Cause |
|---|---|
400 | bookingChannel missing — it is required, and has no default. |
400 | type: "FOLLOW_UP" without followUpOfAppointmentId, or one that is not a completed visit with the same patient and doctor. |
400 | The shift is full — bookedCount has reached maxAppointments. |
403 | appointmentDate is in the past. |
403 | The OTP gate. |
Not idempotent on this route
An Idempotency-Key header is not forwarded here, so a retry books a second
token. If you need de-duplication, reserve through
reserve-slot with a
qmsWorkShiftId in the body — that path does honour the header.