# Clinical Insights — Medical Justification API: integration guide

*For the portal development team. Version 2.0, 2026-09-15.*

## What this is

When a clinician orders a laboratory test in your portal, your system sends
us the **ordered tests, the diagnosis codes and, optionally, the prescribed
medications, why the test is being ordered and the patient's circumstance**,
and we answer one question: **is that test medically justified for those
diagnoses according to the clinical guidelines we hold?** The answer is a **medical-necessity rationale** — Markdown, normally
a short paragraph and currently aimed at 6 sentences, written so a reviewer can attach it to the order,
with page-level citations and a fixed disclaimer at the end. (Which
guidance the rationale leans on first, and its length, are tuned by us as
the guidelines and the laboratory's needs change; citations, the closing
disclaimer and the human-review marking are fixed.) Your portal shows that text to the reviewer and
carries it into your normal order workflow.

It is **not** a coverage determination, a payment prediction, or a guarantee.
Every response says so, and every response is marked `human_review`.

## The endpoint

**This address and this credential are the permanent ones.** You will not be
asked to point at a different host for a later phase, to send additional
fields, or to repeat the integration — when we move the service between our
own environments, the address does not change and your portal sees nothing.
The only thing we might ever ask of you again is a replacement access key, and
only if one leaks.

| | |
|---|---|
| Base URL | `https://api.clinicalinsightsco.com` (the front page links this guide and the contract) |
| Endpoint | `POST /v1/medical-justifications` |
| Contract | `GET /v1/openapi.yaml` (OpenAPI 3.1); this guide at `GET /v1/guide` (Markdown source at `/v1/guide.md`) |
| Liveness | `GET /healthz` |
| Credential | a bearer token, sent to you separately — never in email or tickets |
| Rate limit | 10 requests per minute per credential |
| Latency | typically 6–12 seconds; the service gives up at 60 seconds |

**While you are building and verifying the connection, use made-up cases.**
Your own checks — that requests leave your portal, that we receive them, that
the panel renders — need nothing real, and a diagnosis code becomes patient
information the moment it is attached to an actual person. Once your work is
finished and the integration is deployed, the timing of real traffic is
governed on our side: until the agreements covering patient information are in
place, the endpoint answers `503 endpoint_disabled`, which under rule 1 above
means your portal simply shows no panel. Nothing for you to schedule, switch
or revisit.

## Two rules for the user interface — please get these right first time

You integrate once and are not asked to come back, so these two behaviours
matter more than anything else in this guide. Both are about what a clinician
sees when we do **not** return an answer, which will be a normal and frequent
outcome.

**1. On anything other than `200`, render nothing.** No error box, no empty
panel, no spinner left behind — the ordering screen should look exactly as it
does today. We answer only for the test types whose clinical guidelines we
hold, so a request outside that coverage is declined by design, and the
endpoint can also be closed deliberately for maintenance (`503
endpoint_disabled`). A clinician must never see an error from us while placing
an order; they simply see the justification panel when there is one, and no
panel when there is not. Log the failure on your side by all means — just do
not surface it.

**2. Never block the ordering workflow on our response.** Call us
asynchronously and let the clinician carry on. A typical answer takes 6–12
seconds and we give up at 60. If the panel arrives after they have moved on,
that is fine; if it never arrives, the order proceeds exactly as it does
today. Nothing in your submission path should wait on us.

## Authentication

```
Authorization: Bearer <your token>
Content-Type: application/json
```

Missing or wrong token → `401`. Keep the token out of URLs and logs. If it
leaks, tell us and we rotate it within the hour.

## The request

The fields mirror your order form. Where your form has a free-text box or an
identifier, this contract does not.

```json
{
  "request_id": "3f1c9a2e-7b7d-4e0a-9c0f-2d5b8e6a1f10",
  "test_codes": ["udt-definitive"],
  "test_category": "TOX",
  "diagnosis_codes": ["F11.20"],
  "specimen_type": "urine",
  "medications": ["buprenorphine"],
  "clinical_purpose": "discrepant-presumptive-result",
  "patient_circumstance": "recent-unexpected-result",
  "patient_context": { "age_years": 41, "sex": "male" },
  "relevant_test_history": [
    { "test_code": "udt-presumptive", "days_ago": 20, "result_flag": "positive" }
  ]
}
```

| field | required | rules |
|---|---|---|
| `request_id` | yes | a UUID you generate per order event. It is the idempotency key (see below). |
| `test_codes` | yes | 1–5 controlled codes: `udt-presumptive`, `udt-definitive`, `drug-test-both`. A requisition may carry more than one tier. **Tell us which test codes your portal sends and we add them** — a small change on our side. The list covers what the laboratory runs today; clinicians order from that menu. |
| `diagnosis_codes` | yes | 1–10 ICD-10-CM codes, e.g. `F11.20`, `G89.29`. Format is validated. |
| `test_category` | no | `TOX`. Your form's Test Category. **A category we hold no guidelines for — `PGX`, molecular, microbiology, pathology — is refused with `400`, not answered.** We would rather decline than reason from evidence that does not apply. |
| `specimen_type` | no | `urine`, `oral-fluid`, `blood`, `buccal-swab`. |
| `medications` | no | up to 10 controlled codes: `buprenorphine`, `methadone`, `naltrexone`, `opioid-analgesic`, `benzodiazepine`, `stimulant-adhd`, `gabapentinoid`, `antidepressant`, `antipsychotic`, `cannabis-medical`. **Do not forward your form's "other medication if not found in the list" box** — free text is rejected. Tell us what your clinicians prescribe and we add codes. |
| `clinical_purpose` | no | one controlled code: `baseline-assessment`, `treatment-monitoring`, `adherence-verification`, `suspected-nonadherence`, `suspected-relapse`, `discrepant-presumptive-result`, `medication-change`, `transition-of-care`, `chronic-opioid-therapy-monitoring`. |
| `patient_circumstance` | no | the "because", as one controlled code: `new-patient-intake`, `early-in-treatment`, `stable-in-treatment`, `recent-unexpected-result`, `recent-expected-result`, `behavioral-indicators`, `dose-adjustment`, `co-occurring-pain-management`, `return-to-care-after-gap`. |
| `patient_context` | no | `age_years` (0–89), `sex` (`female` / `male` / `unknown`), `pregnant` (boolean). Nothing else about the person. |
| `relevant_test_history` | no | up to 3 prior tests: `test_code`, `days_ago` (0–3650), `result_flag` (`negative` / `positive` / `unknown` / `not_applicable`). Passed as context; the rationale itself carries no frequency-of-testing material. |

### Two field choices worth explaining

**The "because" is a code, not a sentence.** The workflow asks the provider
why this patient needs this test, and a free-text answer is the natural way
to capture it — but a sentence a clinician writes about a real patient *is*
patient information, and this service is built to hold none. A vocabulary
keeps the meaning and drops the risk. If a circumstance you need is missing,
tell us and it is added as a code; that is a one-line change.

**Ages of 90 and over are refused.** An age above 89 is an identifier under
HIPAA's safe-harbour rule, and the guidelines reason about adulthood rather
than a particular year.

**Anything else is rejected with `400`**, and the message names the field and
what it carries. From your order form, that means: patient name, date of
birth, patient number, address, phone, email, primary and secondary
insurance, policy and group numbers, bill-to, ordering physician, location,
requisition or accession number, collection and schedule dates, the
"other medication" box, and any notes field. The contract carries the minimum
the guidelines need and nothing that identifies a person.

**Need an order reference to match answers back to requisitions?** Use
`request_id` — you generate it, you can store it against your order, and it
comes back on every response. Send your requisition number and the request is
refused.

## The response

```json
{
  "request_id": "3f1c9a2e-7b7d-4e0a-9c0f-2d5b8e6a1f10",
  "status": "completed",
  "disposition": "human_review",
  "justification": "# Medical Guideline Justification for Requested Definitive UDT\n\n**Diagnosis Code Population:** F11.20 (Opioid dependence, uncomplicated) identifies a patient in diagnosis and treatment for substance use disorder (SUD), governed by **Group B - Diagnosis and treatment for substance abuse or dependence** [LCD - Urine Drug Testing (L36668).pdf, p.15].\n\n**Guideline Support for Definitive Testing:**\n\nDefinitive UDT is reasonable and necessary in this scenario. The LCD states: \"Definitive testing to confirm a presumptive UDT positive result, upon the order of the clinician, is reasonable and necessary when the result is inconsistent with the expected result …\" [LCD - Urine Drug Testing (L36668).pdf, p.20]. …",
  "citations": [
    { "source": "LCD - Urine Drug Testing (L36668).pdf", "page": 15, "label": "LCD (L36668)", "policy_number": "L36668", "retrieved": true },
    { "source": "LCD - Urine Drug Testing (L36668).pdf", "page": 20, "label": "LCD (L36668)", "policy_number": "L36668", "retrieved": true }
  ],
  "unsupported_citations": [],
  "sources": [ { "source": "…", "page": 17, "label": "ICD" }, "…" ],
  "clinical_purpose": "discrepant-presumptive-result",
  "patient_circumstance": "recent-unexpected-result",
  "methodology": "Retrieved from the published clinical guidelines and coverage policies held by this service, selected for the submitted diagnosis codes and ordered test, with each statement cited to a named document and page.",
  "notice": "Guideline-based medical justification for clinician and laboratory review. Not a coverage determination, payment prediction, or guarantee. …",
  "model_version": "…", "prompt_version": "system-bee3053e015f.w36e80ff6", "corpus_version": "2887chunks-e294f6e83b50",
  "contract_version": "v1", "wording_version": 0,
  "timing": { "elapsed_seconds": 7.02, "input_tokens": 14344, "output_tokens": 499, "passages_used": 44, "reasoning": false }
}
```

(A real response from the test environment, shortened.)

| field | what to do with it |
|---|---|
| `justification` | The rationale, as **Markdown** — render it as Markdown, not as plain text. It is normally a short paragraph and is currently aimed at 6 sentences, but length and structure follow the current wording and are **not** contractual: it may occasionally carry headings or several paragraphs. What *is* guaranteed: inline citations of the form `[<document>, p.<page>]`, and the disclaimer as the last line, exactly once. |
| `disclaimer` | The fixed sentence that closes every rationale ("Coverage policies cited for supporting context may not apply to every patient, payer, or plan."), also returned on its own for portals that render it separately. It is always the last sentence of `justification` and appears exactly once, so you may render it separately or trim it. |
| `clinical_purpose` / `patient_circumstance` | echoed back from your request, for the labelled sections on your review screen. Echoed rather than parsed out of the rationale, so what the provider attests to is exactly what was asked. |
| `methodology` | a fixed sentence describing how the answer was produced. It describes the method, not the case, so it is the same on every response — safe to render as a static line. |
| `citations` | the citations the text actually made, resolved to our documents and pages. Each carries `label` and, for a coverage policy, `policy_number` (e.g. `L36668`) — the number a reviewer looks up. Render as a source list if you like. |
| `unsupported_citations` | normally empty. If not, the text cited a page we did not supply for this request — display the justification with a caution. |
| `sources` | every guideline page we consulted, in the order supplied. Useful for an "evidence considered" panel; safe to hide. |
| `disposition` | always `human_review` in v1. We do not classify outcomes yet; when we do, new values will arrive in a new contract version, never silently. |
| `model_version` / `prompt_version` / `corpus_version` / `wording_version` | store them with the result. They let either of us reproduce an answer later. `prompt_version` ends in `.w<hash>` identifying the rationale wording in force, and `wording_version` increments whenever we tune it — so two answers that read differently can always be told apart. |
| `timing` | for your dashboards. |

## Idempotency and retries

- Repeat a request with the **same `request_id` and identical body** and you
  get the stored result back in well under a second, with the header
  `Idempotent-Replay: true`. Safe to retry on any network failure.
  **This depends on us storing the answer, which we do in the test
  environment and will not do once real patient data is allowed.** When
  answer retention is off, a repeat of a *completed* `request_id` is refused
  with `409 already_processed` instead — there is nothing kept to hand back.
  Build the retry path to treat `409 already_processed` as "this one
  succeeded already, do not re-run it", and keep your own copy of any answer
  you need to show again.
- The same `request_id` with a **different body** is refused with `409
  request_id_reused`. Generate a new UUID per distinct request.
- On `429`, `503` or `504`, honour the `Retry-After` header (seconds) and
  retry with the same `request_id`. On `502`, the request itself could not be
  answered; retrying will not help — surface it to the reviewer.
- `409 in_progress` means your earlier attempt is still running; retry after
  `Retry-After`.

## Errors

Every error is a JSON envelope; the message names the rule, never your data.

```json
{ "error": { "code": "invalid_request", "message": "diagnosis_codes must contain 1-5 ICD-10-CM codes", "retryable": false },
  "request_id": "3f1c9a2e-7b7d-4e0a-9c0f-2d5b8e6a1f10" }
```

| status | code | meaning | retry? |
|---|---|---|---|
| 400 | `invalid_request`, `invalid_json` | the body broke a contract rule | no — fix the request |
| 401 | `unauthorized` | token missing or wrong | no |
| 409 | `request_id_reused` | same id, different body | no — new UUID |
| 409 | `in_progress` | earlier attempt still running | yes, after `Retry-After` |
| 413 / 415 | `payload_too_large`, `unsupported_media_type` | body over 16 KB, or not JSON | no |
| 429 | `rate_limited` | more than 10 requests in a minute | yes, after `Retry-After` |
| 502 | `no_passages`, `internal_error` | could not be answered | no — show to reviewer |
| 503 | `endpoint_disabled`, `throttled`, `upstream_unavailable` | temporarily unavailable | yes, after `Retry-After` |
| 504 | `timeout` | did not finish in 60 s | yes, after `Retry-After` |

## Three worked examples

All three are synthetic and safe to send as often as you like (idempotent —
reuse the ids for replays, change them for fresh runs).

1. **Opioid dependence on buprenorphine, definitive test after a positive
   presumptive** — the request shown above. Expect a short rationale, currently aimed at 6 sentences,
   grounded in the ASAM drug-testing guidance, ending with the disclaimer.
2. **Chronic pain on long-term opioid therapy, presumptive test, no history**
   — `test_requested: udt-presumptive`, `diagnosis_codes: ["G89.29",
   "Z79.891"]`, `medications: ["opioid-analgesic"]`. Expect a rationale for
   monitoring a patient prescribed opioids for pain.
3. **Stimulant use disorder, both tests** —
   `test_requested: drug-test-both`, `diagnosis_codes: ["F15.20"]`, with
   one negative presumptive result 7 days ago. Expect a rationale drawing
   on the stimulant-use-disorder guideline.

Measured on 2026-09-15 from outside our network: 7.2 s, 6.2 s and 7.5 s.

## Good to know

- **Synchronous.** Hold the connection; there is no callback or polling.
  Set your client timeout to at least 75 seconds.
- **One environment for now.** Production, with its own credential and
  address, is issued only after the data-handling agreements are in place.
- **We record every call** in this test environment (including the
  synthetic request and response) so we can see what you see while you
  integrate. That recording is switched off before any real data is allowed.
- **Contract changes are versioned.** Fields are added, never repurposed;
  breaking changes get a new path. (1.1, 2026-09-15: the `jurisdiction`
  field was removed before any integration began — the service answers a
  clinical-appropriateness question, not a payer one. 1.2, same day:
  optional `medications`; the answer is a medical-necessity rationale with a
  fixed disclaimer. 1.3, same day: the contract's `medications` field was
  declared twice, and the stale copy said it had to be empty — corrected, so
  the schema now matches the worked example and the running service.
  2.0, same day: the request now mirrors the portal's order form —
  `test_requested` became `test_codes` (a list), `diagnosis_codes` accepts up
  to 10, and `test_category`, `specimen_type`, `clinical_purpose`,
  `patient_circumstance` and `patient_context` were added, all as controlled
  codes; the response echoes the purpose and circumstance and adds
  `methodology`, and citations carry `policy_number`.
  Responses also carry `wording_version`, which changes when we tune the
  rationale's emphasis or length; the guaranteed shape — inline citations,
  the closing disclaimer exactly once, and `human_review` — does not
  change with it.)
- **A fixture-only sandbox** (deterministic canned responses, no answering
  system behind it) also exists for pure transport tests. Ask if it would
  help; most teams will not need it now that the real test endpoint is open.

## Contact

Integration questions, new test codes, token rotation: reach us through the
channel this guide arrived by.
