# Ask Medwiki Partner API — Reference

**Version 1.0** · Server-to-server integration for Ask Medwiki partners

Ask Medwiki answers health questions in a short, guided conversation. This API lets your **backend** hold those conversations on behalf of your users and show the answers in your own app. If you want a ready-made chat bubble on a website instead, use the browser widget ([Integration Guide](https://nucleus.medwiki.co/widget/integration-guide)).

---

## Contents

1. [Before you start](#before-you-start)
2. [Quick start](#quick-start)
3. [Authentication](#authentication)
4. [Conversation model](#conversation-model)
5. [Endpoints](#endpoints)
6. [The turn response](#the-turn-response)
7. [Medwiki India answer flow](#medwiki-india-answer-flow)
8. [Errors](#errors)
9. [Limits, timeouts and retries](#limits-timeouts-and-retries)
10. [Code examples](#code-examples)
11. [Security checklist](#security-checklist)
12. [Testing](#testing)
13. [Support](#support)

---

## Before you start

You need two things from Medwiki:

| Item | Example | Notes |
|---|---|---|
| **Partner ID** | `YOUR_PARTNER_ID` | Identifies your integration. Sent in request bodies. |
| **API key** | `amk_…` (47 characters) | Secret. Sent in a header on every request. |

**Base URL**

```
https://nucleus.medwiki.co/api/ask-medwiki
```

All requests and responses are JSON over HTTPS. Send `Content-Type: application/json` on `POST` requests.

---

## Quick start

```bash
KEY="amk_…"                                   # your API key
BASE="https://nucleus.medwiki.co/api/ask-medwiki"

# 1. Start a conversation
curl -s -X POST "$BASE/public/session" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"partner_id": "YOUR_PARTNER_ID", "phone_number": "+919876543210"}'   # phone_number is optional
# → {"session_id": "02e0d1b8-…", "partner_id": "YOUR_PARTNER_ID", "welcome_message": "Hi! Ask me any health question."}

# 2. Send the user's message
curl -s -X POST "$BASE/public/turn" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "X-End-User-IP: 203.0.113.7" \
  -d '{"session_id": "02e0d1b8-…", "user_input": "What is a normal blood pressure for adults?"}'
# → {"system_response": "A normal blood pressure for adults is typically around 120/80 mmHg. …",
#    "system_question": "Do you have any specific concerns about blood pressure?",
#    "conversation_state": "ACTIVE", "variant": "HEALTH", "turn_number": 1, …}
```

Show `system_response` to the user, then `system_question` as the follow-up. Send the user's reply as the next turn with the **same** `session_id`.

---

## Authentication

Send your API key on **every** request, in either header:

```
Authorization: Bearer amk_…
```
```
X-API-Key: amk_…
```

- The key authenticates your **partner**. It only works with your own Partner ID and with sessions created for it.
- Browser `Origin` headers are ignored for API partners — a request without a valid key is always rejected, wherever it comes from.
- **Never expose the key to browsers or mobile apps.** Call this API from your server and pass the results to your front end.

| Situation | Response |
|---|---|
| No key sent | `401 {"detail": "API key required: send 'Authorization: Bearer <key>'"}` |
| Wrong, revoked, or another partner's key | `401 {"detail": "Invalid API key"}` |

**Rotation.** Medwiki can issue a second key while the first still works. Deploy the new key, confirm traffic, then ask Medwiki to revoke the old one — no downtime. If a key leaks, ask for it to be revoked immediately.

---

## Conversation model

```
POST /public/session ──► session_id
        │
        ▼
POST /public/turn  ◄──── user message ──── repeat while conversation_state == "ACTIVE"
        │
        ▼
conversation_state == "INACTIVE"  ──►  start a new session for the next conversation
```

- A **session** is one conversation with one end user. Create a new session per conversation; do not share a session between users.
- Each **turn** is one user message and Ask Medwiki's reply. The session remembers earlier turns, so follow-up questions ("what about for children?") work.
- Ask Medwiki picks the conversation type (`variant`) from the first message: a short medical term gets a definition (`TERM`); anything else becomes a general `HEALTH` conversation. You can also start a specific type with [`/public/turn/helper`](#post-publicturnhelper).
- The conversation ends (`conversation_state: "INACTIVE"`) when Ask Medwiki judges it complete or the user disengages. Further turns on that session return `400`.
- Sessions do not expire on a timer; you can resume one later with its `session_id`.

> **Partners on the Medwiki India flow** (e.g. `askMedwiki_IN`) work differently: each turn is a standalone question answered with context from the user's recent questions, answers are HTML, and the conversation never ends on its own. See [Medwiki India answer flow](#medwiki-india-answer-flow).

---

## Endpoints

| Method | Path | Purpose |
|---|---|---|
| `POST` | [`/public/session`](#post-publicsession) | Start a conversation |
| `POST` | [`/public/turn`](#post-publicturn) | Send a user message, get the reply |
| `POST` | [`/public/turn/helper`](#post-publicturnhelper) | Start (or switch to) a guided conversation type |
| `GET` | [`/public/config`](#get-publicconfig) | Read your partner's display settings |

All paths are relative to the base URL.

### `POST /public/session`

Creates a conversation bound to your partner.

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `partner_id` | string | yes | Your Partner ID |
| `phone_number` | string | no | The end user's phone number, stored against the session. 6–15 digits, optional leading `+`; spaces, dashes, dots and brackets are ignored. An invalid value returns `422`. |
| `device_id` | string | no | A stable ID of your choosing for the end user — your own user/account ID, an app install ID, or a random ID you keep in a cookie. 8–128 characters (letters, digits, `. _ : @ -`). Sessions sent with the same value are grouped as one user even when no phone number is known; if a phone number arrives later, the two are linked. |
| `client_context` | object | no | Facts about the visit that you already hold, for audience analysis. Only these keys are kept, each up to 300 characters; anything else is ignored: `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`, `referrer`, `landing_page`, `page_url`, `ga_client_id` (from your `_ga` cookie), `gclid`, `fbp` and `fbc` (from your `_fbp` / `_fbc` cookies), `device_type`, `os`, `browser`, `screen`, `locale`, `timezone`, `app_version`, `city`, `region`, `country`, and `ad_consent` (boolean: `true` only if this user has agreed to their data being used for advertising). Send it on the session, or on any later turn to add keys. Do not send raw cookie strings. |

**Response `200`**

| Field | Type | Description |
|---|---|---|
| `session_id` | string (UUID) | Use in every turn of this conversation |
| `partner_id` | string | Echo of your Partner ID |
| `welcome_message` | string | Optional greeting to show before the first message |

```json
{
  "session_id": "02e0d1b8-3197-42a7-a53c-ce90010abc40",
  "partner_id": "YOUR_PARTNER_ID",
  "welcome_message": "Hi! Ask me any health question."
}
```

Creating a session does not count toward your quota, but it is refused with `429` once the quota is reached.

### `POST /public/turn`

Sends one user message and returns Ask Medwiki's reply. Counts as **one turn** toward your quota, unless the input is rejected as not health-related.

**Headers**

| Header | Required | Description |
|---|---|---|
| `Authorization` / `X-API-Key` | yes | Your API key |
| `X-End-User-IP` | recommended | The end user's IP address. Used to group conversation history per user; without it every conversation is attributed to your server's IP. |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `session_id` | string | yes | From `/public/session` or `/public/turn/helper` |
| `user_input` | string | yes | The user's message, 1–5,000 characters. Leading/trailing whitespace is trimmed. |
| `phone_number` | string | no | The end user's phone number, same format as on `/public/session`. Send it here if you only learn it mid-conversation; it replaces any number already stored for the session. Omitting it keeps the stored one. |
| `device_id` | string | no | Same as on `/public/session`. |

**Response `200`** — see [The turn response](#the-turn-response).

Typical latency is **15–60 seconds** (several model calls plus an answer-quality check). Set your HTTP timeout to at least **90 seconds**.

### `POST /public/turn/helper`

Starts a conversation of a specific type, or switches an existing session to it. Returns an opening question to show the user; continue with `/public/turn`. Does not count toward your quota.

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `partner_id` | string | yes | Your Partner ID |
| `variant` | string | yes | One of `HEALTH`, `SYMPTOM`, `MEDICINE`, `FACT_CHECK` (must be enabled for your partner — see `/public/config` → `helpers`) |
| `session_id` | string | no | Existing session to switch. Omit to create a new session. |
| `phone_number` | string | no | The end user's phone number, same format as on `/public/session`. |
| `device_id` | string | no | Same as on `/public/session`. |

| Variant | Use it when the user wants to… |
|---|---|
| `HEALTH` | ask a general health question |
| `SYMPTOM` | describe symptoms and understand what they might mean |
| `MEDICINE` | ask about a medicine — uses, side effects, precautions |
| `FACT_CHECK` | check whether a health claim is true |

**Response `200`**

| Field | Type | Description |
|---|---|---|
| `session_id` | string | The session to use for the following turns (new if you omitted one) |
| `partner_id` | string | Echo of your Partner ID |
| `variant` | string | The conversation type now active |
| `starting_question` | string | Opening question to show the user |

```json
{
  "session_id": "5b1c…",
  "partner_id": "YOUR_PARTNER_ID",
  "variant": "SYMPTOM",
  "starting_question": "What symptoms are you experiencing?"
}
```

### `GET /public/config`

Returns your partner's display settings, e.g. to label helper buttons in your UI.

**Query parameters**

| Parameter | Required | Description |
|---|---|---|
| `partner` | yes | Your Partner ID |

**Response `200`**

| Field | Type | Description |
|---|---|---|
| `partner_id` | string | Your Partner ID |
| `name` | string | Display name |
| `theme_color` | string | Hex colour, e.g. `#7b2ff7` |
| `logo_url` | string \| null | Logo image URL |
| `welcome_message` | string | Greeting |
| `locale` | string | Answer language code, e.g. `en`, `hi` |
| `language` | string | Answer language name, e.g. `English` |
| `helpers` | string[] | Enabled helper variants |
| `helper_labels` | object | Custom labels per variant (may be empty) |
| `ui_strings` | object | Suggested UI text in the partner's language (placeholder, button labels, disclaimer, error text) |

---

## The turn response

Every successful `/public/turn` returns these fields:

| Field | Type | Description |
|---|---|---|
| `system_response` | string | **The answer.** Show this to the user. Plain text; may contain line breaks. |
| `system_question` | string | Follow-up question to show after the answer. May be empty. |
| `conversation_state` | string | `ACTIVE` — keep sending turns. `INACTIVE` — conversation over; start a new session next time. |
| `variant` | string \| null | Conversation type: `HEALTH`, `TERM`, `SYMPTOM`, `MEDICINE`, `FACT_CHECK`. `null` when the input was rejected. |
| `turn_number` | integer | 1-based number of this turn in the session |
| `processing_time_ms` | number | Server processing time |

Present in some responses:

| Field | When | Description |
|---|---|---|
| `input_status` | Input was not a health question | Always `"REJECTED"`. `system_response` then tells the user what they can ask. The session stays usable. |
| `term_data` | Short input recognised as a medical term | Object with `medical_term`, `category`, `term_definition`, `term_explanation`, `engagement_prompts` (3 suggested follow-up questions). `system_response` already contains the explanation. |
| `keywords` | Normal answers | Medical keywords detected so far in the session |
| `pipeline_steps` | Always | Internal diagnostics. Do not build logic on it; its shape may change. |

**Example — normal answer**

```json
{
  "system_response": "A normal blood pressure for adults is typically around 120/80 mmHg. Blood pressure can vary based on factors like age, activity level, and overall health.",
  "system_question": "Do you have any specific concerns about blood pressure?",
  "conversation_state": "ACTIVE",
  "variant": "HEALTH",
  "turn_number": 1,
  "keywords": ["blood pressure"],
  "processing_time_ms": 21450.3,
  "pipeline_steps": [ … ]
}
```

**Example — medical term**

```json
{
  "system_response": "Hypertension means your blood pressure stays higher than normal over time. …",
  "system_question": "Would you like to know what causes hypertension?",
  "conversation_state": "ACTIVE",
  "variant": "TERM",
  "term_data": {
    "medical_term": "Hypertension",
    "category": "Condition",
    "term_definition": "Persistently raised blood pressure in the arteries.",
    "term_explanation": "Hypertension means your blood pressure stays higher than normal over time. …",
    "engagement_prompts": ["Would you like to know what causes hypertension?", "…", "…"]
  },
  "turn_number": 1,
  "processing_time_ms": 6120.8,
  "pipeline_steps": [ … ]
}
```

**Example — rejected input**

```json
{
  "system_response": "I can help with health questions — for example symptoms, medicines or medical terms.",
  "system_question": "",
  "conversation_state": "ACTIVE",
  "variant": null,
  "input_status": "REJECTED",
  "turn_number": 1,
  "processing_time_ms": 3804.1,
  "pipeline_steps": [ … ]
}
```

**Displaying answers.** Ask Medwiki gives general health information, not diagnosis. Show a disclaimer near the answers, e.g. *"Information only — not medical advice. Consult a doctor for medical concerns."* (`/public/config` → `ui_strings.disclaimer` has a localised version).

---

## Medwiki India answer flow

Partners on this flow (e.g. `askMedwiki_IN`) get the same answers as the Ask Medwiki chat on medwiki.co.in, without the age and gender question.

**How a turn is answered**

1. **One-word medical term** (e.g. `appendicitis`, `Tofacitinib`): the stored Medwiki answer is returned instantly, with links to the matching medwiki.co.in page(s) and related FAQs. No AI call.
   A message that is **only a term** — one word or several (`sleep medicine`) — gets its explanation followed by a closing question, "What would you like to know about sleep medicine?", as the last paragraph of `system_response`. The user's short reply ("side effects", "dosage") is then answered about that term.
2. Otherwise the message is **checked**. Non-health, unclear or inappropriate input gets a short message asking the user to rephrase (`input_status: "REJECTED"`). A greeting ("Hi", an automated opening message), "how does this work?" or a request to change language gets its own friendly reply instead of that message (the greeting reply ends with a short medical disclaimer, as separate paragraphs divided by blank lines — keep the line breaks when you show it): `input_status` is still `"REJECTED"` (it is not a health question and is not counted as a turn) and `reply_type` is `"GREETING"`, `"HOW_IT_WORKS"`, `"LANGUAGE_REQUEST"` or `"CONTEXT_INFO"`. `CONTEXT_INFO` is a message that only gives background ("I have diabetes and take metformin"): the reply says it has been noted, and it is used to shape the answers to that user's later questions. Show `system_response` as an ordinary message in all these cases.
3. A short **profile of the user** is built from their last 5 questions and used to tailor the answer.
4. The **answer** is written in one language, with 5 suggested follow-up questions. The language is the one the user writes in. It changes only when they write in another language or ask for one ("reply in Hindi"); a language they asked for stays until they ask for another, and a message with no language of its own (a medicine name, a number) keeps the conversation's language. A user who writes Hindi in Latin letters (Hinglish) is answered in Hinglish, not Devanagari. `language` in the response is the language of the reply (`HINDI` for Hinglish too).
5. English answers are **quality-checked** in the background after the response is sent.

**Use `X-End-User-IP` with a stable per-user value.** Context (last 5 questions) and the user profile are keyed by it, across sessions — send the same value for the same person every time (e.g. `<ip>_<your user id>`). Without it, context only covers the current session.

**Send `phone_number` when you know it.** Add it to the body of `/public/session` (or any `/public/turn`) and the conversation is recorded against that phone number, so all of a person's conversations can be found together. It is optional and does not change the answer: context and the user profile are still keyed by `X-End-User-IP`, so keep sending that too. If you have no phone number, send a `device_id` instead. See [`POST /public/session`](#post-publicsession) for the formats.

**Response fields** — in addition to `system_response`, `conversation_state` (always `ACTIVE`), `variant` (`TERM`, `HEALTH`, or `null` when rejected), `turn_number`, `processing_time_ms` and `input_status`:

| Field | Type | Description |
|---|---|---|
| `system_response` | string (**HTML**) | The answer. May contain `<p>`, `<b>`, `<ul>`, `<br>` and `<a href>` links to medwiki.co.in. Sanitise before inserting into a page (allow basic formatting tags and `https` links only). |
| `suggested_questions` | string[] | Up to 5 follow-up questions, in the user's language. Show them as tappable suggestions. Empty when rejected. |
| `faqs` | object[] | `{question, answer}` pairs related to a one-word term. Empty otherwise. |
| `term` | object | One-word term answers only: `medical_term`, `type` (e.g. `MEDICINE`, `DISEASES`, `BRAND`), `links` (medwiki.co.in page URLs). |
| `language` | string | Language of the answer: `ENGLISH`, `HINDI`, `KANNADA`, `BHOJPURI`, `BENGALI`, `TELUGU`, `URDU`, `GUJARATI` or `SPANISH`. |
| `revised_question` | string | The question as Ask Medwiki understood it (cleaned up; `what is <term>` for one-word terms). |
| `question_type` | string | Topic class from the question checker, e.g. `Symptoms`, `Medicines`. |
| `chat_id` | integer \| null | Medwiki answer ID. `null` when rejected. |

`system_question` is always empty on this flow — use `suggested_questions` instead. `/public/turn/helper` is not used.

**Example**

```json
{
  "system_response": "<p>Frequent morning headaches can be caused by poor sleep quality, sleep apnea (breathing interruptions during sleep), stress, or dehydration. …</p>",
  "system_question": "",
  "suggested_questions": [
    "What lifestyle changes can help reduce morning headaches?",
    "Could my medication be causing morning headaches?",
    "…"
  ],
  "faqs": [],
  "variant": "HEALTH",
  "language": "ENGLISH",
  "revised_question": "What causes frequent headaches in the morning?",
  "question_type": "Symptoms",
  "chat_id": 995953,
  "conversation_state": "ACTIVE",
  "turn_number": 1,
  "processing_time_ms": 15210.4
}
```

Typical latency: under 10 s for one-word terms, 10–20 s for full answers.

### Profile questions (optional)

A full answer takes 10–20 seconds. Ask Medwiki can fill that wait with one short multiple-choice question about the user (age group, gender, who the questions are for, diagnosed condition, where they live, and so on), so later answers fit them better. This is switched on per partner by Medwiki. When it is on for you, there are two extra calls.

**1. Ask for a question when you send a turn.** Right as you send `/public/turn`, call `/public/profile-question` with the same session and the same `user_input` — the two calls are matched on it. A question is only asked alongside a health question, so this call waits until the turn has been checked and normally returns after a few seconds, well before the answer (15 seconds at most):

```bash
curl -s -X POST "$BASE/public/profile-question" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"session_id": "02e0d1b8-…", "user_input": "Can I stop metformin if my sugar is normal?"}'
```

```json
{
  "profile_question": {
    "key": "age_group",
    "intro": "While we get your answer, a quick question:",
    "text": "Which age group are you in?",
    "options": [
      {"value": "under_18", "label": "Under 18"},
      {"value": "18_39", "label": "18–39"},
      {"value": "40_59", "label": "40–59"},
      {"value": "60_plus", "label": "60 or above"}
    ],
    "reply_hint": "Reply with the number. This is optional — your answer is on its way."
  }
}
```

`profile_question` is `null` when the message is not a health question (a greeting, "how does this work?", a rejected message, a one-word term answered instantly), when `user_input` is missing or does not match a turn in flight, or when there is nothing left to ask. Otherwise show `intro` and `text` next to your loading indicator, with each option's `label` as a tappable button. Leave it on screen when the answer arrives; the user may answer it then, or ignore it and ask their next question.

**2. Send the answer.** When the user taps an option — the turn may still be loading:

```bash
curl -s -X POST "$BASE/public/profile-answer" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"session_id": "02e0d1b8-…", "key": "age_group", "value": "40_59"}'
# → {"system_response": "Thank you — this helps us personalise what we show you.", "input_status": "PROFILE_ANSWER", …}
```

Show `system_response` as a short acknowledgement. Both calls are instant, make no AI call and do not count toward your quota. `/public/profile-answer` returns `400` if the session has no open question with that `key`, or `value` is not one of its options. (If you cannot call it, send the user's reply as the next `/public/turn` message: the option's `label`, its `value`, its position such as `2`, or the user's own words ("I am 45", "I live in a village") are all recognised as the answer and come back with `input_status: "PROFILE_ANSWER"`. The question stays open for this until the user's next health question has been answered.)

**Rules**

- One question per turn at most, so a conversation can carry several. Calling `/public/profile-question` again for the same turn returns the same question.
- A question a user has already seen — answered or ignored — is never asked again, so each user sees each question once (eleven in total today). This relies on you identifying the user (`phone_number`, `device_id` or `X-End-User-IP`).
- A question that is returned counts as asked whether or not you display it — only call `/public/profile-question` when you are going to show the result.
- `/public/turn` responses no longer carry a `profile_question` field.
- The question is in the language of the answer being prepared, and entirely in it (intro, question, options, hint). A language is used once Medwiki has approved its wording; until then `profile_question` is `null` for that language.
- After the age group, questions related to what the user just asked come first (a medicine question → daily medicines, where they buy them; a disease question → diagnosed conditions, doctor visits), then the rest.
- `reply_hint` is a line for text-only channels; ignore it if you show buttons.
- Only send answers the user actually chose. Do not pre-fill or guess.
- A user who answers "Under 18" is not asked anything further.

---

## Errors

Errors return a non-2xx status and a JSON body `{"detail": "<message>"}`.

| Status | `detail` | Meaning / what to do |
|---|---|---|
| `400` | `Conversation has ended (state: INACTIVE)` | Session is finished. Create a new session. |
| `400` | `Invalid variant` | `variant` must be `HEALTH`, `SYMPTOM`, `MEDICINE` or `FACT_CHECK`. |
| `400` | `Session is not bound to a partner` | Session wasn't created through this API. Create a new one. |
| `401` | `API key required: send 'Authorization: Bearer <key>'` | Add the key header. |
| `401` | `Invalid API key` | Key is wrong, revoked, or belongs to another partner. |
| `403` | `Partner is disabled` | Your integration is switched off — contact Medwiki. |
| `403` | `Helper not enabled for this partner` | Use a variant listed in `/public/config` → `helpers`. |
| `403` | `Session does not belong to this partner` | Only use sessions created with your Partner ID. |
| `404` | `Unknown partner` | Check `partner_id`. |
| `404` | `Session not found` | Check `session_id`, or create a new session. |
| `422` | *(list of field errors)* | Request body failed validation, e.g. missing `user_input` or longer than 5,000 characters. Body is `{"detail": [{"loc": […], "msg": "…", "type": "…"}]}`. |
| `429` | `Daily turn limit reached (N)` / `Monthly turn limit reached (N)` | Quota used up. See [Limits](#limits-timeouts-and-retries). |
| `500` | varies | Server problem. Retry later (see below). |
| `502` / `503` / `504` | varies | Temporary upstream or platform issue. Retry later. |

---

## Limits, timeouts and retries

| Limit | Value |
|---|---|
| Turns per day | 1,000 (resets at 00:00 UTC) |
| Turns per month | 20,000 (resets on the 1st, UTC) |
| `user_input` length | 5,000 characters |
| Recommended HTTP timeout | 90 seconds for `/public/turn`, 30 seconds for the others |

- Every answered `/public/turn` counts as one turn. Rejected inputs (`input_status: "REJECTED"`), sessions, helpers and config calls are not counted.
- Need higher limits? Contact Medwiki with your expected volume.

**Retries**

- `/public/session`, `/public/turn/helper` and `/public/config` are safe to retry.
- `/public/turn` is **not idempotent**: if a request times out on your side, the turn may still have been processed and counted. Do not retry turns automatically on timeouts; show the user an error and let them resend. Retrying on `5xx` responses is fine.
- On `429`, do not retry until the limit resets.
- Use exponential backoff (e.g. 2 s, 4 s, 8 s; max 3 attempts) for `5xx`.

---

## Code examples

### Python

```python
import os
import requests

BASE = "https://nucleus.medwiki.co/api/ask-medwiki"
PARTNER_ID = "YOUR_PARTNER_ID"
HEADERS = {"Authorization": f"Bearer {os.environ['ASK_MEDWIKI_KEY']}"}


class AskMedwikiError(Exception):
    def __init__(self, status, detail):
        super().__init__(f"{status}: {detail}")
        self.status, self.detail = status, detail


def _post(path, body, timeout, extra_headers=None):
    r = requests.post(f"{BASE}{path}", json=body, timeout=timeout,
                      headers={**HEADERS, **(extra_headers or {})})
    if not r.ok:
        try:
            detail = r.json().get("detail")
        except ValueError:
            detail = r.text
        raise AskMedwikiError(r.status_code, detail)
    return r.json()


def start_session(phone_number=None):
    body = {"partner_id": PARTNER_ID}
    if phone_number:  # optional: records the conversation against this user
        body["phone_number"] = phone_number
    return _post("/public/session", body, timeout=30)["session_id"]


def ask(session_id, text, end_user_ip=None):
    headers = {"X-End-User-IP": end_user_ip} if end_user_ip else None
    return _post("/public/turn", {"session_id": session_id, "user_input": text},
                 timeout=90, extra_headers=headers)


session_id = start_session()
reply = ask(session_id, "What is a normal blood pressure for adults?", end_user_ip="203.0.113.7")
print(reply["system_response"])
if reply.get("system_question"):
    print(reply["system_question"])
if reply["conversation_state"] == "INACTIVE":
    session_id = None  # start a new session next time
```

### Node.js (18+)

```javascript
const BASE = "https://nucleus.medwiki.co/api/ask-medwiki";
const PARTNER_ID = "YOUR_PARTNER_ID";
const KEY = process.env.ASK_MEDWIKI_KEY;

async function post(path, body, { timeoutMs = 30000, headers = {} } = {}) {
  const res = await fetch(BASE + path, {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${KEY}`, ...headers },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(timeoutMs),
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw Object.assign(new Error(`${res.status}: ${JSON.stringify(data.detail)}`), { status: res.status });
  return data;
}

async function startSession(phoneNumber) {
  // phoneNumber is optional: records the conversation against this user
  const body = phoneNumber ? { partner_id: PARTNER_ID, phone_number: phoneNumber } : { partner_id: PARTNER_ID };
  return (await post("/public/session", body)).session_id;
}

async function ask(sessionId, text, endUserIp) {
  return post("/public/turn", { session_id: sessionId, user_input: text }, {
    timeoutMs: 90000,
    headers: endUserIp ? { "X-End-User-IP": endUserIp } : {},
  });
}

const sessionId = await startSession();
const reply = await ask(sessionId, "What is a normal blood pressure for adults?", "203.0.113.7");
console.log(reply.system_response, reply.system_question);
```

### Guided conversation (any language)

```bash
# Start a symptom conversation
curl -s -X POST "$BASE/public/turn/helper" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"partner_id": "YOUR_PARTNER_ID", "variant": "SYMPTOM"}'
# → {"session_id": "5b1c…", "variant": "SYMPTOM", "starting_question": "What symptoms are you experiencing?", …}

# Then answer the starting question as a normal turn
curl -s -X POST "$BASE/public/turn" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"session_id": "5b1c…", "user_input": "Sore throat and mild fever since yesterday"}'
```

---

## Security checklist

- [ ] The API key is stored in a secret manager or environment variable, not in source code.
- [ ] The key is only used server-side; it never reaches browsers, mobile apps or logs.
- [ ] `X-End-User-IP` is set from your own trusted request context, not from user-supplied data.
- [ ] Session IDs are kept per user and never shown to other users.
- [ ] A disclaimer is shown alongside answers.
- [ ] You have a plan to rotate the key (ask Medwiki for a second key, deploy, revoke the old one).

---

## Testing

Medwiki hosts a test chat for every partner:

```
https://nucleus.medwiki.co/widget?partner=YOUR_PARTNER_ID
```

For API partners it asks for your API key first. The key is kept only in that browser tab. Test conversations count toward your quota.

---

## Support

Contact your Medwiki account manager for new or revoked keys, higher limits, changing the answer language, or enabling helper types. Include your Partner ID and, for a specific problem, the `session_id` and approximate time (UTC).
