Clinical Insights · Medical Justification API

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 URLhttps://api.clinicalinsightsco.com (the front page links this guide and the contract)
EndpointPOST /v1/medical-justifications
ContractGET /v1/openapi.yaml (OpenAPI 3.1); this guide at GET /v1/guide (Markdown source at /v1/guide.md)
LivenessGET /healthz
Credentiala bearer token, sent to you separately — never in email or tickets
Rate limit10 requests per minute per credential
Latencytypically 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.

{
  "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" }
  ]
}
fieldrequiredrules
request_idyesa UUID you generate per order event. It is the idempotency key (see below).
test_codesyes1–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_codesyes1–10 ICD-10-CM codes, e.g. F11.20, G89.29. Format is validated.
test_categorynoTOX. 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_typenourine, oral-fluid, blood, buccal-swab.
medicationsnoup 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_purposenoone 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_circumstancenothe "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_contextnoage_years (0–89), sex (female / male / unknown), pregnant (boolean). Nothing else about the person.
relevant_test_historynoup 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

{
  "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.)

fieldwhat to do with it
justificationThe 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.
disclaimerThe 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_circumstanceechoed 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.
methodologya 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.
citationsthe 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_citationsnormally empty. If not, the text cited a page we did not supply for this request — display the justification with a caution.
sourcesevery guideline page we consulted, in the order supplied. Useful for an "evidence considered" panel; safe to hide.
dispositionalways 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_versionstore 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.
timingfor your dashboards.

Idempotency and retries

Errors

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

{ "error": { "code": "invalid_request", "message": "diagnosis_codes must contain 1-5 ICD-10-CM codes", "retryable": false },
  "request_id": "3f1c9a2e-7b7d-4e0a-9c0f-2d5b8e6a1f10" }
statuscodemeaningretry?
400invalid_request, invalid_jsonthe body broke a contract ruleno — fix the request
401unauthorizedtoken missing or wrongno
409request_id_reusedsame id, different bodyno — new UUID
409in_progressearlier attempt still runningyes, after Retry-After
413 / 415payload_too_large, unsupported_media_typebody over 16 KB, or not JSONno
429rate_limitedmore than 10 requests in a minuteyes, after Retry-After
502no_passages, internal_errorcould not be answeredno — show to reviewer
503endpoint_disabled, throttled, upstream_unavailabletemporarily unavailableyes, after Retry-After
504timeoutdid not finish in 60 syes, 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 historytest_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 teststest_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

Contact

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