Server-side API

Authentication

Send your API key on every request, keep it secret, and name the patient on OTP-gated routes.

Every server-side request carries your API key. There is no token to sign and no keypair to manage.

1. Create a key

In the dashboard, go to Workspace Settings → API Keys (/dashboard/workspace-settings/api-keys).

Choose Add API Key, set Key Type to Developer API.

Add the IP addresses your servers call from. At least one is required — see IP allowlist.

Create the key and copy the API key (mk_…).

You see the key in full exactly once

After you close that dialog the dashboard shows it masked, for good, and there is no way to retrieve it. Put it in your secret manager before you do anything else. Lost it? Rotate — create a replacement and retire the old one.

2. Send it

x-api-key: mk_…

That is the whole credential for most routes.

const res = await fetch(
  "https://api.medos.one/v1/appointments/available-slots?addressId=1&doctorId=5&appointmentDate=2026-09-20&mode=OFFLINE",
  { headers: { "x-api-key": process.env.MEDOS_API_KEY } },
);
import os, requests

requests.get(
    "https://api.medos.one/v1/appointments/available-slots",
    params={"addressId": 1, "doctorId": 5, "appointmentDate": "2026-09-20", "mode": "OFFLINE"},
    headers={"x-api-key": os.environ["MEDOS_API_KEY"]},
)
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.medos.one/v1/appointments/available-slots"
                + "?addressId=1&doctorId=5&appointmentDate=2026-09-20&mode=OFFLINE"))
        .header("x-api-key", System.getenv("MEDOS_API_KEY"))
        .GET()
        .build();

HttpResponse<String> response = HttpClient.newHttpClient()
        .send(request, HttpResponse.BodyHandlers.ofString());

Keeping the key safe

The API key authenticates on its own, so anyone holding it can call the API as you — from an address on your allowlist. Treat it like a database password.

DoDon't
Keep it in a secret manager or an environment variableCommit it to a repository, even a private one
Send it only from server-side code, over HTTPSPut it in a browser, a mobile app or a desktop client
Register the narrowest addresses you canRegister a whole cloud provider's range if you can pin your egress
Rotate it when someone with access leavesShare one key across unrelated integrations

The allowlist is your safety net

If the key does leak, the allowlist is what stops it being used — a request from any address you did not register is refused with a 403, even with the right key. Rotate anyway: an allowlist bounds a leak, it does not undo one.

A key that should never run in a browser is also refused if you try: the widget's session endpoint will not accept a Developer API key, and a Developer API route will not accept a widget key. The two kinds of key do not stand in for each other.

OTP-gated routes

Booking and patient lookup require a patient's phone to have been verified by one-time code in the last 30 minutes. It is the patient-consent check, not an anti-bot measure. If your system has already identified the patient, book through the routes without OTP instead.

Because one API key serves every patient you have, these routes need one more header, naming the patient the request is for:

x-api-key: mk_…
x-end-user-id: patient-8841

POST /v1/otp/send with { countryCode, phoneNumber } and x-end-user-id.

POST /v1/otp/verify with { countryCode, phoneNumber, otpCode } and the same x-end-user-id.

Call the gated route within 30 minutes, with the same x-end-user-id.

What to use as the value. Your own identifier for that patient — a patient id or a CRM id is ideal. It must be stable across the three calls and distinct per patient. It is hashed before Medos stores it, so an email address works too, though an opaque id is better practice. It must be 1–128 printable ASCII characters, with no spaces.

Why the header is required, not optional

Verification is tracked per x-end-user-id under your key. If it were tracked per key alone, verifying patient A and then patient B would overwrite A's verification — and A's next booking would run as B. So the OTP routes and every gated route answer 400 without the header rather than guess. Routes that are not gated do not need it.

The daily limit on one-time codes (50 per 24 hours) is counted per x-end-user-id too, so each patient has their own allowance.

Gated routes are marked 🔒 in Endpoints.

Booking without OTP

The one-time code exists for patient-facing screens, where nothing else proves that the person booking owns the phone they typed. A server integration often knows its patient already — a front desk, a CRM, a patient app with its own login — and sending them a WhatsApp code adds nothing.

So the gate follows the credential, not the path. On the booking routes:

Calling withOTP gate
Authorization: Bearer st_… (a widget session)Enforced. The phone must have been verified in the last 30 minutes.
x-api-key (a server integration)Skipped. Your server is trusted to have identified its patient.

The routes are the documented ones — there is no second set of paths:

Same body, same response. Two things differ when you call with an API key:

  • No x-end-user-id. There is no verification to keep separate per patient.
  • Your workspace is pinned. A workspaceId in the body is overwritten with the one your key belongs to, and the booking is stamped as coming from the Developer API.

On this page