Server-side API

Developer API errors

What each failure means, and which ones are worth retrying.

Errors specific to server-side authentication are collected here. Business errors from the endpoints themselves are passed through from the Medos core unchanged.

401 — the key was not accepted

{ "message": "Invalid API key", "error": "Unauthorized", "statusCode": 401 }

The same body for every cause, deliberately: telling you which check failed would help someone probing a guessed key more than it helps you.

CauseHow to spot it
Wrong or truncated keyCopied without the mk_ prefix, or cut short. Compare the length against your secret manager's copy.
An SDK widget keyWidget keys are public by design and are refused here. Create a key of type Developer API.
Key deactivated, deleted or expiredCheck the key's status in the dashboard.
Header missing or misnamedIt must be exactly x-api-key.

A request with no credential at all gets a more specific message, since there is nothing to protect:

{
  "message": "Missing credentials. Send an x-api-key header, or Authorization: Bearer <session token>.",
  "statusCode": 401
}

Isolate it with one request

Call GET /v2/workspaces with nothing but x-api-key, from the server that will run your integration. If that works, the key and address are fine and the problem is in how your application builds requests.

403 — wrong IP address

{
  "message": "This API key is not allowed from 203.0.113.9. Add it to the key's allowed IPs.",
  "statusCode": 403
}

The address in the message is the one Medos saw. Add it to the key's Allowed IP addresses by rotating, or fix your egress so requests leave from an address you already registered.

Check IPv4 vs IPv6 before anything else

A dual-stack client often arrives over IPv6 even when you expected IPv4, so an allowlist containing only 203.0.113.10 rejects the very server you meant to allow. The address in the error tells you which family you actually arrived on.

403 — not available to this key

{
  "message": "This route is not available to API keys. Use the documented developer-api endpoints.",
  "error": "Forbidden",
  "statusCode": 403
}

The route exists, but it is not one of the documented endpoints — usually one the widget uses. API keys reach only the documented list, so retrying will not help. You only see this with a valid key: a bad key gets the 401 above on any route.

403 — not entitled to this operation

{
  "success": false,
  "message": "This workspace's plan does not include devapi_patients_list. Contact your medos administrator to enable it.",
  "requiredFeature": "devapi_patients_list"
}

The key is valid and the route is one API keys can call, but this workspace has not been granted this operation. Branch on requiredFeature rather than on the message.

It is per operation, not per key: lookup by MRN can be allowed while the full roster is refused. See Entitlements. Retrying will not help — an administrator has to enable the item.

Two things worth knowing. A grant takes up to ten minutes to take effect, because the gateway caches the enabled set per workspace; if you were just granted something and are still seeing 403, waiting is usually the answer. And if every gated call starts answering 403 at once, check the subscription rather than the individual items.

400 — no end user named

{
  "success": false,
  "message": "Send an x-end-user-id header identifying the patient this request is for. ..."
}

You called an OTP route or a 🔒 route without x-end-user-id. Add a stable id for the patient and send the same value on every call in that patient's flow. See OTP-gated routes.

A separate 400 means the header was present but malformed: it must be 1–128 printable ASCII characters, with no spaces.

403 — phone not verified

A different 403, from an OTP-gated route:

{ "success": false, "message": "...", "requiresVerification": true }

Run the OTP pair with the same x-end-user-id, then retry within 30 minutes. A 403 here after a successful verify almost always means the x-end-user-id changed between the two calls.

400 — request failed validation

Before calling Medos, the gateway rejects requests that could never succeed:

  • a booking, queue, pack purchase, appointment change or staff body that is not a JSON object (an array, for example)
  • a confirm-payments body with none of sessionRef, appointmentRef or gatewayReferenceId
  • a pack confirm body with no sessionRef
{
  "statusCode": 400,
  "status": "failed",
  "message": "value must contain at least one of [sessionRef, appointmentRef, gatewayReferenceId]",
  "fieldErrors": [
    {
      "field": "",
      "error": "value must contain at least one of [sessionRef, appointmentRef, gatewayReferenceId]",
      "value": { "provider": "RAZORPAY" }
    }
  ]
}

These checks are deliberately loose. Extra fields pass them, and so does every field Medos itself treats as optional. Medos then validates the full request and returns its own 400 errors unchanged.

429 — rate limited

Back off for Retry-After seconds. See Rate limits.

500 — unexpected error

{
  "statusCode": 500,
  "status": "failed",
  "message": "Something went wrong, please contact support with UUID: 01J9ZC3V7K8QW2M4N6P8R0T2V4"
}

The UUID is the request id. Quote it when you contact us so we can find the request in our logs. To use your own correlation id, send it as x-request-id: 1–64 characters of A–Z, a–z, 0–9, _ or -. A value in any other format is replaced with a generated id.

A 5xx that Medos returns is passed through with its own body.

Trying these yourself

Every rule above is one you can trigger in a few seconds from the playground on any endpoint page, which is the fastest way to learn the contract:

Remove a character from the API key. A 401, with the same message as a key that does not exist — the gateway does not tell a guesser which part was wrong.

Paste an SDK widget key instead. Also a 401. A widget key is public by design, so it is refused anywhere the key alone is the credential.

Open Send OTP with the End-user ID empty. A 400 asking for x-end-user-id. One key serves every patient you have, so verification has to say whose it is.

Try patient search by phone before verifying a phone. A 403 with requiresVerification. Run send and verify with an End-user ID, then retry with the same one.

Now change the End-user ID and search again. A 403 again. A different id is a different patient, who has not verified anything.

Open a write such as record vitals and clear the Acting user ID. A 400 naming x-acting-user-id.

Clear an optional parameter. Watch it disappear from the Request line. An omitted parameter and an empty one are different requests, and this is where that becomes visible.

Two failures do not look like HTTP statuses at all. A request your browser's address is not allowed to make returns a 403 naming the address — see IP allowlist, and note your browser may reach us over IPv6 even when your server does not, so the address in the error is often not the one you expected. And a request blocked by CORS shows up as a failure to connect rather than a status, so it looks nothing like the 401 and 403 above.

What to retry

StatusRetry?
400No — fix the request first: x-end-user-id, the fieldErrors listed, or the Medos message.
401No — fix the key first. Retrying an unchanged request fails identically.
403No.
429Yes, after Retry-After.
5xxYes, with exponential backoff.

Retrying a write can double it

A 5xx or a timeout on a booking or purchase does not tell you whether it happened. Send an Idempotency-Key on the routes that accept one, and reuse it on the retry, so a second attempt returns the first result instead of booking twice.

On this page