# Ask Qiri API — Integration Guide

> Audience: frontend / BFF engineers integrating the Qiri clinical intelligence
> platform into their UI. This document covers authentication, request format,
> the streaming response protocol, feedback submission, error handling, and
> security considerations.

## 1. Prerequisites

Before a user can call the Ask Qiri API, their account must be provisioned on
this platform via the
[Onboarding Provisioning API](./onboarding-provisioning-api.md). That call
returns a `pharmacy_id` which is required as a header on every request.

## 2. Authentication

Every request carries two headers:

| Header | Value | Source |
|--------|-------|--------|
| `Authorization` | `Bearer <Firebase ID token>` | Obtained from Firebase Auth (project `qiri-ai-89c2c`) via `getIdToken()` on the client. Tokens expire hourly; use `onIdTokenChanged` to keep them fresh. |
| `X-Pharmacy-Id` | `<pharmacy_id UUID>` | The `pharmacy_id` returned by the provisioning endpoint. Selects which pharmacy tenant the request is scoped to. The user must have an active membership at this pharmacy or the request is rejected with `403`. |

The backend verifies the Firebase ID token cryptographically on every request
(RS256 signature against Google's rotating JWKS, audience = Firebase project
ID). There is no trusted "logged-in" flag on the client.

## 3. Endpoints

### 3.1 Streaming chat (primary) — `POST /api/twin/stream/{session_id}`

The main Ask Qiri endpoint. Returns a Server-Sent Events (SSE) stream with
incremental answer tokens, status updates, and a final structured completion
event.

**Path parameter:**

| Param | Type | Description |
|-------|------|-------------|
| `session_id` | `string` (UUID) | Chat session identifier. Generate a new UUID for a new conversation; reuse to continue an existing one. The platform creates and manages the session automatically. |

**Request body** (`application/json`):

```json
{
  "query": "What are the interactions between metformin and lisinopril?",
  "patient_hash": null,
  "prescription_session_id": null,
  "lab_results": null,
  "medications": null,
  "geography": "AU"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `query` | `string` | yes | The pharmacist's question. |
| `patient_hash` | `string \| null` | no | De-identified patient context hash (for session scoping). |
| `prescription_session_id` | `string \| null` | no | Link to an active prescription session — injects medication lines, safety checks, and recommendation into context. |
| `lab_results` | `array \| null` | no | Lab results for interpretation. Each item: `{ test_name, value, unit?, reference_low?, reference_high?, reference_range? }`. |
| `medications` | `array \| null` | no | List of medication names (strings) for context. |
| `geography` | `string \| null` | no | ISO-3166 alpha-2 region used for geo-scoped source restrictions (`AU`, `NZ`, `US`, `GB`, `CA`, `IE`, `SG`). Invalid or absent values default to `AU`. |

**Response:** `text/event-stream` (SSE). See [Section 4](#4-sse-event-protocol).

### 3.2 Non-streaming chat — `POST /api/twin/chat`

Returns the full response in a single JSON payload (no streaming). Useful for
batch/backend integrations where SSE is inconvenient.

**Request body** (`application/json`):

```json
{
  "query": "What is the PBS listing for rosuvastatin?",
  "session_id": "uuid-here",
  "patient_hash": null,
  "geography": "AU"
}
```

| Field | Type | Required |
|-------|------|----------|
| `query` | `string` | yes |
| `session_id` | `string` | yes |
| `patient_hash` | `string \| null` | no |
| `geography` | `string \| null` | no |

**Response** (`application/json`):

```json
{
  "answer": "...",
  "sources": [...],
  "confidence": 0.92,
  "safety_results": [...],
  "reasoning_trace": "..."
}
```

## 4. SSE event protocol

The streaming endpoint emits `data: {json}\n\n` lines. Each JSON object has a
`type` field that determines its shape.

### 4.1 `status` — progress indicator

```json
{ "type": "status", "content": "Retrieving clinical evidence...", "stage": "retrieving", "stage_index": 3, "total_stages": 7 }
```

Display to the user as a loading/progress message.

### 4.2 `token` — incremental answer text

```json
{ "type": "token", "content": "Metformin is a biguanide..." }
```

Append `content` to the answer being built. Tokens arrive in ~80-character
chunks.

### 4.3 `complete` — final structured result

```json
{
  "type": "complete",
  "answer": "Full answer text...",
  "sources": [
    { "url": "https://...", "title": "TGA PI — Metformin", "source_type": "TGA Product Information", "tier": 1 }
  ],
  "confidence": 0.92,
  "search_tier": 1,
  "safety_results": [
    { "rule_id": "...", "severity": "WARN", "message": "..." }
  ],
  "reasoning_trace": "Intent: dosing | Grounding: F | ...",
  "model_used": "gemini-2.5-flash",
  "trace_id": "uuid-trace-id",
  "cached": false,
  "suggested_questions": [
    { "question": "What are the key contraindications and precautions for metformin?", "perspective": "safety" },
    { "question": "What parameters should be monitored while taking metformin?", "perspective": "monitoring" },
    { "question": "What are the preferred alternatives if metformin is unsuitable?", "perspective": "alternatives" }
  ]
}
```

**Key fields:**

| Field | Type | Description |
|-------|------|-------------|
| `answer` | `string` | The complete answer (same as concatenated tokens). |
| `sources` | `array` | Cited sources with URL, title, type, and search tier (1=KG/QBrain, 2=DeepSearch, 3=PubMed/APIs, 4=Web, 5=LLM). |
| `confidence` | `float` | 0.0–1.0 confidence score. |
| `search_tier` | `int` | Which tier provided the primary evidence. |
| `safety_results` | `array` | Safety engine outcomes: `BLOCK`, `HOLD`, `WARN`, or `PASS`. A `BLOCK` means the response was suppressed for safety. |
| `reasoning_trace` | `string` | Diagnostic trace of the reasoning pipeline. |
| `model_used` | `string` | Which LLM model generated the answer. |
| `trace_id` | `string` | Unique trace identifier — **required for feedback submission** (see Section 5). |

**Optional domain-specific fields** (present when the query triggers a
specialised engine):

| Field | When present |
|-------|--------------|
| `interaction_data` | Drug interaction queries — structured interaction report. |
| `pgx_data` | Pharmacogenomics queries — gene-drug pair analysis. |
| `lab_data` | Lab result interpretation — structured lab report. |
| `shortage_alerts` | When referenced drugs have active TGA shortage alerts. |
| `suggested_questions` | Grounded answers mentioning at least one drug — up to 3 suggested follow-up questions (see Section 4.5). |

### 4.5 `suggested_questions` — next-best follow-up questions

Grounded answers that mention at least one drug include up to **3 suggested
follow-up questions** on the `complete` event. Each suggestion covers a
clinical dimension (perspective) the pharmacist has *not* asked about yet, so
the conversation systematically covers the angles a senior pharmacist would
check — safety, dosing, interactions, monitoring, alternatives, special
populations, efficacy, and PBS/regulatory.

```json
{ "question": "Does metformin require renal or hepatic dose adjustment?", "perspective": "dosing" }
```

| Field | Type | Description |
|-------|------|-------------|
| `question` | `string` | Ready-to-send follow-up question. |
| `perspective` | `string` | The clinical dimension it explores: `safety` \| `dosing` \| `interactions` \| `efficacy` \| `alternatives` \| `monitoring` \| `populations` \| `regulatory`. |

**How to use:**

1. Render the questions as clickable chips/buttons under the answer.
2. On click, send the `question` string as the next `query` to
   `POST /api/twin/stream/{session_id}` using the **same** `session_id` — the
   platform uses the conversation history to avoid re-suggesting dimensions
   already covered.
3. Show suggestions only for the most recent answer; earlier turns' chips are
   stale once the conversation moves on.

**Behaviour notes:**

- Suggestions are generated server-side from the query intent, the answer
  content, and the knowledge graph — no extra request is needed.
- The field is **omitted** when no drug was mentioned, when generation timed
  out, or on safety-`BLOCK` responses. Treat it as optional and hide the UI
  when absent or empty.
- Generation runs after the answer has finished streaming, so it never delays
  the answer itself.

### 4.4 `error` — failure

```json
{ "type": "error", "detail": "PharmaTwin error: ..." }
```

Display the error and stop reading the stream.

## 5. Feedback submission

After receiving a `complete` event, the UI should allow the pharmacist to
accept, modify, reject, or flag the response. Submit feedback via:

```
POST /api/feedback
Authorization: Bearer <token>
X-Pharmacy-Id: <pharmacy_id>
```

**Request body:**

```json
{
  "session_id": "the-session-uuid",
  "trace_id": "the-trace-id-from-complete-event",
  "action": "accept",
  "note": ""
}
```

| Field | Type | Values |
|-------|------|--------|
| `session_id` | `string` | The same session_id used in the chat request. |
| `trace_id` | `string` | The `trace_id` from the `complete` SSE event. |
| `action` | `string` | `accept` \| `modify` \| `reject` \| `flag` |
| `note` | `string` | Optional free-text note from the pharmacist. |

**Response:** `{ "status": "ok" }`

This feedback is logged to the context graph (BigQuery) for continuous learning
and audit.

## 6. Error codes

| HTTP status | Meaning | Action |
|-------------|---------|--------|
| `401` | Missing, expired, or invalid Firebase ID token. | Re-authenticate via Firebase and retry. |
| `403` | Valid token but no active membership at the requested `X-Pharmacy-Id`, or pharmacy not verified/active. | Check that the user was provisioned and the pharmacy_id is correct. |
| `503` | Backend service not initialized (e.g. LLM router, RAG stack). | Retry after a short delay; the service may be starting up. |
| `500` | Internal error. | Log the response body and report. |

On a `401`, the recommended UX is to dispatch an `auth:expired` event and
redirect to re-authentication rather than leaving the user on a stale session.

## 7. Example: TypeScript SSE client

```typescript
async function askQiri(
  query: string,
  sessionId: string,
  firebaseToken: string,
  pharmacyId: string,
): Promise<void> {
  const resp = await fetch(`${API_URL}/api/twin/stream/${sessionId}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${firebaseToken}`,
      "X-Pharmacy-Id": pharmacyId,
    },
    body: JSON.stringify({ query }),
  });

  if (!resp.ok) {
    if (resp.status === 401) window.dispatchEvent(new CustomEvent("auth:expired"));
    throw new Error(`${resp.status}: ${await resp.text()}`);
  }

  const reader = resp.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop()!;

    for (const line of lines) {
      if (!line.startsWith("data: ")) continue;
      const event = JSON.parse(line.slice(6));

      switch (event.type) {
        case "status":
          showProgress(event.content);
          break;
        case "token":
          appendToAnswer(event.content);
          break;
        case "complete":
          finalizeAnswer(event);
          storeTraceId(event.trace_id); // needed for feedback
          break;
        case "error":
          showError(event.detail);
          break;
      }
    }
  }
}
```

## 8. Security notes

- **No backend secrets in the browser.** The Firebase web config (API key,
  project ID, etc.) is public by design — it only identifies the project, not
  authorises access. The `X-Platform-Service-Token` used for provisioning is a
  server-side secret and must **never** be included in client-side code.
- **Token verification is server-side.** Never trust a client-set "logged-in"
  flag. The backend verifies every token cryptographically.
- **Tenant isolation.** The `X-Pharmacy-Id` header is validated against the
  caller's own memberships — one pharmacy's data cannot be accessed by another.
- **Safety engine.** Responses with `BLOCK` severity in `safety_results` are
  suppressed. The UI should never override or hide a `BLOCK` result.
