Book an appointment
Turn a slot into a confirmed scheduled appointment.
POST /v1/appointments/book-appointmentThe plain scheduled booking: one visit, one charge, paid at the clinic or already settled. If the visit involves a session pack — buying one, or spending one you already own — use book-appointment-unified instead. If the patient is about to pay online, use reserve-slot.
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
Send one of these. The first two are lookups; the third creates the record if it does not exist.
| Form | Field | Behaviour |
|---|---|---|
| Known patient | patientId | Used as-is. |
| Known patient, nested | patientPayload.id | Promoted to patientId. |
| New patient | patientPayload object | Find-or-create against the clinic, then booked. |
patientPayload accepts firstName, middleName, lastName, email,
countryCode, phoneNumber, dob, age, gender, bloodGroup,
mrnNumber and source. Blank strings are dropped rather than forwarded, so an
empty dob: "" from a form is harmless. A sibling patientAddress object
(addressLine1, city, state, country, zipcode, landmark, latitude,
longitude, …) is attached to the patient when creating one.
Both objects are stripped before the booking itself — Medos books by
patientId only.
Body
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId | number | Yes | Your workspace. Must be the one your key belongs to. |
addressId | number | Yes | The clinic location. workspaceAddressId is accepted as an alias. |
doctorId | number | Yes | The practitioner. |
patientId | number | Yes* | *Or patientPayload, per the table above. |
mode | string | Yes | OFFLINE, ONLINE, HOME_VISIT, HOME_COLLECTION, WALK_IN. |
fromDateTimeIso | string | Yes† | Slot start, ISO 8601 with an offset: 2026-09-20T09:00:00+05:30. Copy it from the slot. |
toDateTimeIso | string | Yes† | Slot end, same form. |
consultationCharge | number | Yes | The fee, >= 0. Omit only for a follow-up (followUp: true), where the doctor's policy sets it. |
paymentMode | string | Yes | CASH, CARD, UPI, BANK_TRANSFER, ONLINE, OTHER, or the name of a payment method the clinic configured. |
source | string | Yes | Send DEVELOPER_API from a server integration. |
currency | string | No | ISO 4217. Defaults to the location's own currency. |
discountAmount | number | No | Flat discount, <= consultationCharge. |
discountReason | string | No | Free text, ≤ 200 chars. |
paymentStatus | string | No | UNPAID (default), PENDING, PAID, … Set PAID when you have already taken the money. |
paymentReference | string | No | Your transaction id. Only meaningful when PAID. |
paymentMethodId | number | No | A configured payment method, preferred over the paymentMode string. |
type | string | No | CONSULTATION (default), EMERGENCY, PROCEDURE, LAB_TEST, SURGERY, VIRTUAL. A FOLLOW_UP sent here is booked as CONSULTATION — use followUp. |
followUp | boolean | No | true books this visit as a follow-up of the patient's qualifying consultation with this doctor. See Booking a follow-up. |
consultationDurationTierId | number | No | Overrides consultationCharge and the slot length from the tier. |
appointmentNotes | string | No | ≤ 500 chars. |
chiefComplaint | string | No | Reason for the visit, ≤ 500 chars. |
invoiceNotes | string | No | Printed on the invoice under the consultation line. |
roomId | number | No | A physical room at that location. Rejected for ONLINE. |
patientTimezone | string | No | IANA zone used when sending the patient confirmations and reminders. |
bookingChannel | string | No | How the patient reached you: WALK_IN, PHONE_CALL, WHATSAPP, EMAIL, TELEGRAM, INTERNAL_REFERRAL, EXTERNAL_REFERRAL, SELF_QR. |
vital | object | No | Intake vitals recorded in the same transaction as the booking. |
† Or the deprecated appointmentDate + fromDateTimeTs + toDateTimeTs
(HH:mm) trio. That form has no timezone and has silently misbooked visits for
non-Indian clinics — send the ISO pair.
Booking a follow-up
Send followUp: true. You do not name the visit being followed up — the gateway
does:
- It runs the patient-facing eligibility check for this patient, doctor, location, mode and date — the same strict rule the clinic's Self Check-in page applies.
- If the patient has a qualifying visit, the booking goes through as a
FOLLOW_UPlinked to that visit, and Medos prices it from the doctor's follow-up policy. - If not, nothing is booked and you get a
400:
{ "success": false, "message": "FOLLOW_UP_NOT_ELIGIBLE", "reason": "NO_PARENT" }reason is passed through from Medos when it gives one. Offer the patient an
ordinary consultation instead.
type and parentAppointmentId are ignored
A body carrying type: "FOLLOW_UP" is booked as a CONSULTATION, and
parentAppointmentId is dropped. Clinic staff may book a follow-up with no
previous visit, and every call on this API reaches Medos as staff — so only the
gateway picks the parent, from the patient-facing check.
A follow-up is always a single visit. It cannot be combined with a pack
bookingType on the unified route.
Try it
/v1/appointments/book-appointmentWith an API key the OTP gate is skipped and the booking is stamped SERVER_BOOKING. With a widget session it is OTP-gated.
Body
POST /v1/appointments/book-appointment{
"addressId": "1",
"doctorId": "4",
"mode": "OFFLINE",
"consultationCharge": "500",
"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
curl -sX POST "https://api.medos.one/v1/appointments/book-appointment" \
-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,
"mode": "OFFLINE",
"fromDateTimeIso": "2026-09-20T09:00:00+05:30",
"toDateTimeIso": "2026-09-20T09:30:00+05:30",
"consultationCharge": 500,
"currency": "INR",
"paymentMode": "CASH",
"source": "DEVELOPER_API",
"chiefComplaint": "Persistent cough for 3 days",
"patientPayload": {
"firstName": "Asha",
"lastName": "Nair",
"countryCode": "+91",
"phoneNumber": "9876543210",
"gender": "FEMALE"
}
}'Response
The created appointment:
{
"workspaceId": 1,
"addressId": 1,
"doctorId": 4,
"patientId": 774,
"mode": "OFFLINE",
"appointmentDate": "2026-09-20",
"fromDateTimeTs": "2026-09-20T09:00:00+05:30",
"toDateTimeTs": "2026-09-20T09:30:00+05:30",
"appointmentTimezone": "Asia/Kolkata",
"fromTime": "09:00",
"toTime": "09:30",
"bookingReference": "MED-9182-AK",
"consultationCharge": 500,
"currency": "INR",
"discountAmount": 0,
"paymentStatus": "UNPAID",
"paymentMode": "Cash",
"paymentMethodName": "Cash",
"bookingChannel": null,
"videoMeetingUrl": null
}| Field | Meaning |
|---|---|
bookingReference | The patient-facing handle for this booking, and the value reserve-slot hands to the payment rail as appointmentRef. Store it. |
patientId | The resolved patient — the id Medos created or matched when you sent a patientPayload. Store it too. |
fromDateTimeTs / toDateTimeTs | On the response these are full ISO 8601 instants with the clinic's offset, not the HH:mm the request field of the same name takes. fromTime / toTime carry the wall-clock rendering. |
paymentMode | The method's display name, resolved from the clinic's configuration — not necessarily the string you sent. |
consultationChargeBase / baseCurrency | Present only when the visit was charged in a currency other than the clinic's base one. |
videoMeetingUrl | Populated for ONLINE bookings once the meeting is provisioned. |
No numeric appointment id comes back
This response identifies the booking by bookingReference only. The
:appointmentId that reschedule,
status and
cancel take is a different, numeric id
that this payload does not carry — so capture it from your own records at the
point you have it, and talk to us if your integration needs to look one up by
reference.
Common failures
| Status | Cause |
|---|---|
400 | A field name Medos does not recognise. Unknown properties are rejected, not ignored — check for typos and for fields that belong to the unified route. |
400 | fromDateTimeIso without an offset. 2026-09-20T09:00:00 is refused; …+05:30 is accepted. |
400 | The body is not a JSON object (an array, for example). The response lists fieldErrors — see Errors. |
400 | FOLLOW_UP_NOT_ELIGIBLE — followUp: true for a patient with no qualifying visit. See Booking a follow-up. |
400 | discountAmount greater than consultationCharge. |
403 | requiresVerification: true — the OTP gate. Verify and retry within 30 minutes. |
5xx | Among other things, a slot taken between reading it and booking it surfaces here rather than as a clean conflict. Re-read available slots before retrying, and do not retry blindly. |
Booking is not idempotent
A retry books a second appointment. If a request times out, look the patient up
before sending it again — or use
reserve-slot, which honours an
Idempotency-Key.