Server-side APIEndpointsAppointments

Book an appointment

Turn a slot into a confirmed scheduled appointment.

POST /v1/appointments/book-appointment

The 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.

FormFieldBehaviour
Known patientpatientIdUsed as-is.
Known patient, nestedpatientPayload.idPromoted to patientId.
New patientpatientPayload objectFind-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

FieldTypeRequiredDescription
workspaceIdnumberYesYour workspace. Must be the one your key belongs to.
addressIdnumberYesThe clinic location. workspaceAddressId is accepted as an alias.
doctorIdnumberYesThe practitioner.
patientIdnumberYes**Or patientPayload, per the table above.
modestringYesOFFLINE, ONLINE, HOME_VISIT, HOME_COLLECTION, WALK_IN.
fromDateTimeIsostringYes†Slot start, ISO 8601 with an offset: 2026-09-20T09:00:00+05:30. Copy it from the slot.
toDateTimeIsostringYes†Slot end, same form.
consultationChargenumberYesThe fee, >= 0. Omit only for a follow-up (followUp: true), where the doctor's policy sets it.
paymentModestringYesCASH, CARD, UPI, BANK_TRANSFER, ONLINE, OTHER, or the name of a payment method the clinic configured.
sourcestringYesSend DEVELOPER_API from a server integration.
currencystringNoISO 4217. Defaults to the location's own currency.
discountAmountnumberNoFlat discount, <= consultationCharge.
discountReasonstringNoFree text, ≤ 200 chars.
paymentStatusstringNoUNPAID (default), PENDING, PAID, … Set PAID when you have already taken the money.
paymentReferencestringNoYour transaction id. Only meaningful when PAID.
paymentMethodIdnumberNoA configured payment method, preferred over the paymentMode string.
typestringNoCONSULTATION (default), EMERGENCY, PROCEDURE, LAB_TEST, SURGERY, VIRTUAL. A FOLLOW_UP sent here is booked as CONSULTATION — use followUp.
followUpbooleanNotrue books this visit as a follow-up of the patient's qualifying consultation with this doctor. See Booking a follow-up.
consultationDurationTierIdnumberNoOverrides consultationCharge and the slot length from the tier.
appointmentNotesstringNo≤ 500 chars.
chiefComplaintstringNoReason for the visit, ≤ 500 chars.
invoiceNotesstringNoPrinted on the invoice under the consultation line.
roomIdnumberNoA physical room at that location. Rejected for ONLINE.
patientTimezonestringNoIANA zone used when sending the patient confirmations and reminders.
bookingChannelstringNoHow the patient reached you: WALK_IN, PHONE_CALL, WHATSAPP, EMAIL, TELEGRAM, INTERNAL_REFERRAL, EXTERNAL_REFERRAL, SELF_QR.
vitalobjectNoIntake 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:

  1. 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.
  2. If the patient has a qualifying visit, the booking goes through as a FOLLOW_UP linked to that visit, and Medos prices it from the doctor's follow-up policy.
  3. 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

POST/v1/appointments/book-appointment

With an API key the OTP gate is skipped and the booking is stamped SERVER_BOOKING. With a widget session it is OTP-gated.

Body

Request
POST /v1/appointments/book-appointment
{
  "addressId": "1",
  "doctorId": "4",
  "mode": "OFFLINE",
  "consultationCharge": "500",
  "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

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
}
FieldMeaning
bookingReferenceThe patient-facing handle for this booking, and the value reserve-slot hands to the payment rail as appointmentRef. Store it.
patientIdThe resolved patient — the id Medos created or matched when you sent a patientPayload. Store it too.
fromDateTimeTs / toDateTimeTsOn 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.
paymentModeThe method's display name, resolved from the clinic's configuration — not necessarily the string you sent.
consultationChargeBase / baseCurrencyPresent only when the visit was charged in a currency other than the clinic's base one.
videoMeetingUrlPopulated 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

StatusCause
400A field name Medos does not recognise. Unknown properties are rejected, not ignored — check for typos and for fields that belong to the unified route.
400fromDateTimeIso without an offset. 2026-09-20T09:00:00 is refused; …+05:30 is accepted.
400The body is not a JSON object (an array, for example). The response lists fieldErrors — see Errors.
400FOLLOW_UP_NOT_ELIGIBLE — followUp: true for a patient with no qualifying visit. See Booking a follow-up.
400discountAmount greater than consultationCharge.
403requiresVerification: true — the OTP gate. Verify and retry within 30 minutes.
5xxAmong 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.

Next

On this page