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