This service is Beta for early access testing.

← portal API documentation

On this page

What the service does

The code-search service finds the right code (SNOMED CT, LOINC, etc) for clinical text, constrained by a FHIR context parameter (e.g. StructureDefinition#element) or a ValueSet URI. It layers intelligent matching over a FHIR terminology server's ValueSet/$expand: deterministic fast-path matching for common cases, with LLM evaluation and iterative search-term expansion for harder cases.

The intended use is to take clinical text whose meaning needs to be coded — for example, from a clinical note — and produce a code that satisfies the binding required by a FHIR resource element or your application's value set.

Meaning fidelity

The service picks the code whose meaning matches the input as closely as possible without adding meaning that isn't in the input. If the input says "thyroid scan" the service will not return a code that means "iodine-123 thyroid scan" — it would be inserting a method the clinician didn't write. If no exact-meaning code exists, the service falls back to the closest broader code that captures everything the input does say, and never to a narrower one. Returning a broader code is honest under-coding; returning a narrower code is fabrication.

Where a single code can't capture the full meaning, the response includes intersection_codes — secondary codes whose meanings, combined with the primary, encode what the text actually said. These are separate clinical concepts, not alternate codings of the same concept; under FHIR all Coding entries within a single CodeableConcept must represent the same concept (different terminology, same meaning). Callers should map intersection codes to the appropriate FHIR element for each — e.g. body site to Condition.bodySite, supporting evidence to Condition.evidence, secondary findings to a separate Condition resource — or, when the bound terminology supports it (SNOMED CT in particular), use a post-coordinated expression to encode the compound meaning in a single Coding.

FHIR binding awareness

The service understands FHIR's binding strength (required, extensible, preferred, example) and additional bindings on the bound element. It respects what each strength allows:

Concrete example. Given the AU eRequesting ServiceRequest.code element for imaging requests, which has a preferred binding to the RANZCR Radiology Referral ValueSet:

Why use this instead of $expand directly?

A naive client can call ValueSet/$expand?filter=... and pick the first result. That works for clean inputs against well-curated ValueSets. The cases this service handles that $expand on its own does not:

Authentication

All requests require a JWT bearer token issued by our authorization server.

Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

Since you're reading this you already have an account and are signed in. There are three routes to making authenticated calls, depending on the use case:

REST API

POST /api/v1/find-code

Find the best code for a clinical-text query.

Request body:

{
  "text": "type 2 diabetes",
  "context": "http://hl7.org.au/fhir/core/StructureDefinition/au-core-condition#Condition.code",
  "max_candidates": 3,
  "effort": "balanced"
}
FieldTypeRequiredDescription
textstringyesClinical text to encode
contextstringone of context/url requiredFHIR profile element with binding (e.g. StructureDefinition…#Condition.code)
urlstringone of context/url requiredValueSet canonical URL
systemstringnoCode system the result should be drawn from (e.g. http://snomed.info/sct, http://loinc.org). Default http://snomed.info/sct.
system_versionstringnoPin a specific code-system version, forwarded to the terminology server as system-version.
max_candidatesintnoMaximum number of candidates returned. Top-N by confidence, with ties at the boundary pulled in. Default 3.
effort"fast" | "balanced" | "best"noHow hard to try. fast = quick lookup, may bail early on hard cases. balanced (default) = full evaluation pipeline. best = more iterations on hard cases at the cost of latency.

Response:

{
  "matches": [
    {
      "code": "44054006",
      "system": "http://snomed.info/sct",
      "display": "Diabetes mellitus type 2",
      "confidence": 0.95,
      "reasoning": "exact match on preferred term"
    }
  ]
}
FieldDescription
matches[]Ranked candidates. matches[0] is the primary suggestion. May be empty if no plausible code exists.
matches[].confidence0.0 to 1.0. Values ≥ 0.9 are typically usable without human review; values below 0.7 should be treated as suggestions and confirmed.
matches[].reasoningHuman-readable explanation of why this code was selected.
intersection_codes[]When a single code can't capture the full meaning, additional codes whose intersection with matches[0] represents the complete meaning.

GET /api/v1/find-code

Same handler as POST, parameters in the query string. The endpoint accepts both forms because:

Behaviour is identical between the two; choose by use case, not by capability.

curl examples BETA URL

The hostname below is for early-access testing only and will change before general availability.

# Replace $TOKEN with your bearer token

curl -s "https://code-search.australiaeast.cloudapp.azure.com/api/v1/find-code" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "type 2 diabetes",
    "context": "http://hl7.org.au/fhir/core/StructureDefinition/au-core-condition#Condition.code"
  }' | jq .

# GET equivalent
curl -s -G "https://code-search.australiaeast.cloudapp.azure.com/api/v1/find-code" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "text=type 2 diabetes" \
  --data-urlencode "context=http://hl7.org.au/fhir/core/StructureDefinition/au-core-condition#Condition.code" | jq .

Reporting feedback

After automapping clinical text to a code, callers can report the outcome so it can become training and evaluation data. Three actions fall out of the same endpoint:

POST /api/v1/feedback

Authenticated (same bearer-token auth as find-code).

Request body:

{
  "text": "T2DM",
  "context": "http://hl7.org.au/fhir/core/StructureDefinition/au-core-condition#Condition.code",
  "system": "http://snomed.info/sct",
  "chosen_code": "44054006",
  "chosen_display": "Diabetes mellitus type 2",
  "supplied_code": "73211009",
  "supplied_display": "Diabetes mellitus"
}
FieldTypeRequiredDescription
textstringyesThe original search text that was mapped
contextstringone of context/url requiredFHIR profile element with binding (same as find-code)
urlstringone of context/url requiredValueSet canonical URL (same as find-code)
systemstringnoThe code system that was searched (the supplied code’s system). Optional — defaults to chosen_system if omitted, else http://snomed.info/sct. Send it explicitly (alongside chosen_system) to record a genuine cross-system correction.
chosen_codestringyesThe human-confirmed correct code
chosen_systemstringnoCode system of chosen_code; defaults to system. Lets a correction cross code systems.
chosen_displaystringnoDisplay term for the chosen code
supplied_codestringnoThe code the service originally returned. When present and different from chosen_code, recorded as a negative example.
supplied_displaystringnoDisplay term for the supplied code

Response201 Created:

{
  "id": "fb_01j8z...",
  "kind": "correction"
}

kind is one of confirmed (supplied == chosen), correction (supplied present but differs from chosen), or novel (no supplied code — human supplied where the service had nothing).

MCP (Model Context Protocol)

The service exposes an MCP streamable-HTTP endpoint at /mcp with one tool: find_code. Use it from any MCP-aware client (Claude Desktop, Claude Code, Cline, etc.).

Claude Desktop configuration BETA URL

Claude Desktop talks to MCP servers over stdio. To bridge that to our HTTP endpoint we use the mcp-remote npm package — it runs in-process, handles OAuth Protected Resource Metadata discovery, and pops a browser the first time you connect. Add to your claude_desktop_config.json:

{
  "mcpServers": {
    "code-search": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://code-search.australiaeast.cloudapp.azure.com/mcp",
        "--static-oauth-client-info",
        "{"client_id":"code-search-mcp"}"
      ]
    }
  }
}

Save, fully quit Claude Desktop (⌘Q on macOS, not just close the window) and reopen. The first time the model invokes find_code a browser tab opens against the code-search portal for sign-in; after that the token is cached by mcp-remote locally and refreshed automatically. No bearer token in the config file, ever.

The --static-oauth-client-info flag tells mcp-remote to use our pre-registered code-search-mcp public client instead of attempting Dynamic Client Registration (which the authorisation server doesn't expose to the public internet).

Other OAuth-aware MCP clients (Claude Code, Cursor, Cline) have native HTTP MCP support and can point directly at https://code-search.australiaeast.cloudapp.azure.com/mcp without the mcp-remote bridge — check each client's docs for the exact config shape.

Try it

After Claude Desktop has connected and you've logged in, paste this into a new chat:

Use the code-search find_code tool to find a code for the following clinical text,
in the context of an AU Core Condition resource:

  T2DM with diabetic retinopathy

The binding context for the resource element is
http://hl7.org.au/fhir/core/StructureDefinition/au-core-condition#Condition.code.

Show me the primary code, any intersection codes, and the reasoning.

Claude calls find_code, the service expands the abbreviation (T2DM → type 2 diabetes), recognises that "diabetic retinopathy" is a secondary clinical concept that doesn't fit in the same Condition.code, and returns it as an intersection code. You'll see Claude reflect both back to you with the SNOMED codes and a short explanation of how to model them on the FHIR resource.

The find_code tool

Same inputs as POST /api/v1/find-code: text, context/url, system, system_version, max_candidates, effort.

Output comes back in two parallel forms on the same MCP tool result. Clients pick whichever they prefer — there's no "mode" toggle.

Example tool result for "type 2 diabetes":

// content[0].text (what the LLM reads)
Found 44054006 — Diabetes mellitus type 2 (95% confidence)

Reasoning: exact match on preferred term

// structuredContent (programmatic access)
{
  "matches": [
    {
      "code": "44054006",
      "system": "http://snomed.info/sct",
      "display": "Diabetes mellitus type 2",
      "confidence": 0.95,
      "reasoning": "exact match on preferred term"
    }
  ]
}

Example for an empty-match case:

// content[0].text
No suitable code found for "wifi triggering seizures".

// structuredContent
{ "matches": [] }

The MCP endpoint advertises its protected-resource metadata at /.well-known/oauth-protected-resource per RFC 9728 — clients that support auth discovery will pick this up automatically.

For raw ValueSet/$expand browsing without LLM evaluation, use Ontoserver's own MCP tools — this service deliberately doesn't duplicate that surface.

Error responses

StatusBodyMeaning
200Result objectSuccess (matches may be empty if no plausible code exists)
400{"error":"validation_error","detail":...}Invalid request shape
401{"error":"Unauthorized"}Missing / invalid bearer token
403{"error":"forbidden_no_role"}Token is valid but doesn't grant access to this service
404{"error":"valueset_not_found"}The terminology server returned 404 for the resolved ValueSet URL — the ValueSet is genuinely unknown or unavailable. Distinct from 422 binding_not_resolved, where the problem is the context path not carrying a binding rather than the ValueSet itself being missing.
422 {"error":"binding_not_resolved","context":"…","cause":"…","detail":"…","suggestion":"…"}

A profile context (<StructureDefinition>#<elementPath>) could not be resolved to a ValueSet binding. The response includes:

  • context — the context string as supplied
  • cause — one of:
    • sd_not_found — the StructureDefinition URL could not be fetched
    • element_not_found — the element path does not exist in the StructureDefinition's snapshot (common for datatype sub-elements; see below)
    • no_binding_at_element — the element exists but carries no ValueSet binding
  • detail — human-readable explanation
  • suggestion — recommended fix

Common case — datatype sub-element path. An element such as MedicationRequest.dosageInstruction.route is a sub-element of the Dosage datatype; its binding lives on the datatype definition, not on MedicationRequest directly. The resolver returns cause: "element_not_found" because that path doesn't appear in the MedicationRequest StructureDefinition snapshot. Fix: pass the bound ValueSet URL directly via the url parameter, or use an element path that carries a binding on the profile you are targeting.

504{"error":"upstream_error"}Upstream terminology server unreachable / errored

Reporting issues

Email ontoserver-support@csiro.au. Include:

For unexpected codes specifically, include the FHIR context URL plus what you'd consider the correct code — that's the most useful form of feedback.