N NessyAPI docs
Status Changelog

Python SDK

Official Python SDK for NessyAPI — sync and async clients, dataclass response models, webhook signature verification with replay protection. Tested against Python 3.11+. PyPI: nessyapi-sdk.

Install

pip install nessyapi-sdk

Quickstart

import os
from nessyapi_sdk import NessyClient

nessy = NessyClient(api_key=os.environ["NESSY_API_KEY"])

session = nessy.create_session(chief_complaint="chest pain")
result = nessy.run_assessment(session.session_id)

for diff in result.differentials[:5]:
    print(f"{diff.diagnosis}: {diff.probability:.0%}")

Authentication

Pass your API key to the NessyClient constructor. Keys are nsy_live_* for production traffic and nsy_test_* for sandbox traffic (zero token cost). Manage keys from the dashboard's API Keys page.

nessy = NessyClient(
    api_key=os.environ["NESSY_API_KEY"],
    base_url="https://nessyapi.bravemeadow-4ea62cad.northeurope.azurecontainerapps.io",
    timeout=30,
)

Client constructor

NessyClient

NessyClient(api_key: 'str', base_url: 'str' = 'https://nessyapi.bravemeadow-4ea62cad.northeurope.azurecontainerapps.io', timeout: 'float' = 30.0, max_retries: 'int' = 3)

Construct a NessyAPI client.

  • api_key Bearer API key. Use nsy_test_* for sandbox traffic (zero token cost) or nsy_live_* for production. Manage keys from the dashboard's API Keys page.
  • base_url API root URL. Defaults to the Azure-hosted endpoint; override only when targeting staging or a self-hosted deployment.
  • timeout Per-request timeout in seconds. Default 30s; LLM-backed endpoints (answer, route) can run up to ~12s under load, so don't drop below 15s.
  • max_retries Number of automatic retries on transient failures (HTTP 429 / 5xx, network errors). Each retry uses exponential backoff with jitter and respects Retry-After.

Async client

AsyncNessyClient mirrors every method on the sync client. Use it from FastAPI, Starlette, aiohttp, or any asyncio-based server. The sync client is fine for scripts and Django views.

import asyncio
from nessyapi_sdk import AsyncNessyClient

async def main():
    async with AsyncNessyClient(api_key=...) as nessy:
        session = await nessy.create_session(chief_complaint="headache")
        result = await nessy.run_assessment(session.session_id)
        return result.differentials

asyncio.run(main())

Sessions

create_session 1 token

create_session(chief_complaint: 'str', age: 'int | None' = None, sex: 'str | None' = None, free_text: 'str | None' = None, patient_id: 'str | None' = None) → 'SessionResponse'

Create a new clinical session.

The session is the unit of clinical reasoning — every subsequent answer / route / finalize call references its session_id. Demographics provided here gate which questions the engine asks (e.g. gynaecological branches need sex='female').

  • chief_complaint Primary symptom or complaint, lowercase snake-style (e.g. "headache", "chest_pain"). Routing matches against the engine's complaint catalogue.
  • age Patient age in years. Influences risk stratification + age-gated branches.
  • sex Patient sex ("male" / "female" / "other").
  • free_text Optional natural-language description. When supplied, the engine runs NLP extraction to pre-fill demographics + flags before the first question.
  • patient_id Your internal stable identifier. When provided the session is linked to the patient profile, enabling chronic-conditions and medications to bias differentials.

SessionResponse with session_id, the first current_question, and triage-state metadata.

route 5 tokens (covers the LLM extraction call)

route(session_id: 'str', text: 'str') → 'AnswerResponse'

Route a free-text symptom description to the right clinical branch.

Use this when the user types a sentence ("I've had a sharp pain in my left chest for 30 minutes") instead of selecting a structured chief complaint. The engine runs NLP extraction, picks the matching branch, and returns the first question of that branch.

  • session_id Identifier returned by create_session.
  • text Free-text symptom narrative (Czech or English).

AnswerResponse whose current_question is the first structured question of the matched branch.

answer 8 tokens normally (covers NLP + reasoning); 0 tokens when skip=True

answer(session_id: 'str', question_id: 'str', *, raw_text: 'str | None' = None, extracted_fields: 'dict[str, Any] | None' = None, extracted_flags: 'list[str] | None' = None, skip: 'bool' = False) → 'AnswerResponse'

Submit a patient's answer to the current question.

The engine NLP-extracts structured fields/flags from raw_text unless you pre-extract them yourself (faster + cheaper than running NLP for every answer). Skipping a question keeps the session moving when the patient cannot or will not answer; the engine flags that evidence as "missing" rather than absent.

  • session_id Identifier returned by create_session.
  • question_id current_question.question_id from the previous response. Validates against the active question set.
  • raw_text Patient's free-text answer. NLP extracts duration, severity, location, etc. Mutually exclusive with pre-extracted extracted_fields.
  • extracted_fields Already-parsed structured values ({"duration_hours": 3, "severity": 7}). Skips the LLM call — use when your client has its own NLP layer.
  • extracted_flags Boolean clinical flags (["smoker", "fever"]). Same skip-NLP behaviour.
  • skip Mark the question unanswered and advance. The engine records "no answer" rather than treating it as a negative.

AnswerResponse with the next current_question (or is_complete=True when finished), updated differentials, and any newly raised red flags.

get_results Free

get_results(session_id: 'str') → 'ResultsResponse'

Read the current differentials, triage level, and red flags.

Idempotent — call as often as needed without re-billing. Useful for showing live differential updates in your UI between answers.

  • session_id Identifier returned by create_session.

ResultsResponse with ranked differentials, triage label (urgent / routine / etc.), red-flag list, and meta.

get_state Free

get_state(session_id: 'str') → 'dict[str, Any]'

Read raw engine state for debugging and resuming sessions.

Returns the engine's internal state machine snapshot — the active branch, asked-question history, extracted evidence, and triage trajectory. Mostly used during development; production flows shouldn't need it.

  • session_id Identifier returned by create_session.

Raw dict; shape is intentionally not typed because it tracks the engine's evolving internals.

get_questions Free

get_questions(session_id: 'str') → 'QuestionsResponse'

List the question pool plus progress for the active session.

Useful for showing a "X of Y questions answered" progress bar. Returns every question the active branch may ask, with each marked as answered / skipped / pending.

  • session_id Identifier returned by create_session.

QuestionsResponse carrying the question list and progress counters.

finalize Free

finalize(session_id: 'str') → 'ResultsResponse'

Close the session and return the full assessment.

Marks the session finished; subsequent answer calls return 409. The response includes the final ranked differentials, triage level, recommended next steps (referral / observation / emergency), and a key-positive / key-negative findings summary.

  • session_id Identifier returned by create_session.

ResultsResponse with the finalized differential list + recommendations.

run_assessment 1 + N * 8 tokens, where N is the number of answered (non-skipped) questions

run_assessment(chief_complaint: 'str', age: 'int | None' = None, sex: 'str | None' = None, answers: 'dict[str, str] | None' = None, max_questions: 'int' = 15) → 'ResultsResponse'

Run a full assessment end-to-end (create → answer loop → finalize).

Convenience wrapper for non-interactive scripts and tests. Drives the engine question loop using a precomputed answer map; questions not in answers are skipped so the loop terminates predictably.

  • chief_complaint Same as create_session.chief_complaint.
  • age Patient age in years.
  • sex Patient sex.
  • answers Optional {question_id: free_text} map. Each entry is sent verbatim as raw_text to answer. Missing question_ids are skipped.
  • max_questions Safety cap on the loop length; default 15.

Final ResultsResponse from finalize.

Session management

list_sessions Free

list_sessions(*, status: 'str | None' = None, patient_id: 'str | None' = None, limit: 'int' = 50, offset: 'int' = 0) → 'SessionListResponse'

List sessions for the current tenant, newest first.

  • status Filter by session status ("in_progress", "finished", "abandoned"). None returns all.
  • patient_id Restrict to a single patient profile.
  • limit Page size (max 200).
  • offset Pagination offset.

SessionListResponse with items + total count for paging.

export_session Free

export_session(session_id: 'str') → 'dict[str, Any]'

Dump a complete session record for archival or audit.

Returns the full Q&A transcript, every triage level the session passed through, the final differential ranking, red flags, and recommendation. Format is JSON-serialisable; suitable for long-term storage or compliance review.

  • session_id Identifier returned by create_session.

Raw dict; the shape mirrors what gets written to the audit DB (intentionally not typed so future fields don't require an SDK upgrade).

update_demographics Free

update_demographics(session_id: 'str', *, age: 'int | None' = None, sex: 'str | None' = None) → 'dict[str, Any]'

Patch demographics on an in-flight session.

Useful when create_session was called without demographics (e.g. anonymous chat) and the user later supplies them. The engine re-evaluates which branches/questions are eligible.

  • session_id Identifier returned by create_session.
  • age Patient age in years. Pass None to leave unchanged.
  • sex Patient sex ("male" / "female" / "other").

Raw dict with the updated demographics. The session keeps its current question pointer.

submit_feedback Free

submit_feedback(session_id: 'str', accuracy: 'str', *, actual_diagnosis: 'str | None' = None, actual_icd10: 'str | None' = None, notes: 'str | None' = None) → 'dict[str, Any]'

Submit ground-truth feedback for a finalized session.

Accuracy feedback feeds NessyAPI's continual evaluation set — we use it to track real-world precision and improve the engine. Only callable on finalized sessions.

  • accuracy "correct" / "incorrect" / "partially_correct". Required.
  • actual_diagnosis Free-text label of the real diagnosis, when known (e.g. "acute coronary syndrome").
  • actual_icd10 ICD-10 code (e.g. "I21.4"); helps us match against differential entries that carried the same code.
  • notes Free-text comments; surfaced in the audit log.

Raw dict with the feedback receipt.

Patients

get_patient Free

get_patient(patient_id: 'str') → 'PatientProfile'

Read a patient profile.

Profiles aggregate chronic conditions, medications, allergies, family history, and lifestyle flags. Sessions linked to the same patient_id inherit the profile so the engine can bias differentials accordingly.

  • patient_id Stable identifier you supplied previously. Returns 404 if no profile exists.

PatientProfile dataclass.

update_patient Free

update_patient(patient_id: 'str', *, chronic_conditions: 'list[str] | None' = None, current_medications: 'list[str] | None' = None, allergies: 'list[str] | None' = None, risk_factors: 'list[str] | None' = None, family_history: 'list[str] | None' = None, smoking_status: 'str | None' = None, alcohol_status: 'str | None' = None) → 'PatientProfile'

Create or update a patient profile (PUT semantics).

Each list-valued field (chronic conditions, medications, etc.) is **merge-append** — sending ["aspirin"] adds to an existing list rather than replacing it. To remove an item use the dashboard or contact support; the SDK doesn't expose a merge-delete primitive yet.

  • patient_id Stable identifier. New IDs auto-create the profile.
  • chronic_conditions ICD-10 labels or free-text condition names.
  • current_medications Generic or trade names.
  • allergies Free-text or coded allergens.
  • risk_factors Lifestyle / occupational risk markers.
  • family_history Conditions in first-degree relatives.
  • smoking_status One of "never" / "former" / "current" / "unknown".
  • alcohol_status Same enum as smoking_status.

PatientProfile reflecting the merged state.

list_patient_sessions Free

list_patient_sessions(patient_id: 'str') → 'list[dict[str, Any]]'

List every session linked to a patient profile.

  • patient_id Stable identifier you supplied previously.

List of session summary dicts (id, status, chief_complaint, triage_level, created_at). Empty list when the patient has no sessions.

delete_patient Free

delete_patient(patient_id: 'str') → 'dict[str, Any]'

Permanently erase a patient profile and all linked sessions.

Use to honour data-subject deletion requests. The operation is irreversible; archived audit-log records may retain hashed identifiers per retention policy.

  • patient_id Stable identifier you supplied previously.

Raw dict with deleted_sessions count + status.

Admin & billing

get_balance Free

get_balance() → 'BalanceResponse'

Read the current token balance.

BalanceResponse with balance (current), lifetime_used, and threshold flags (low_warning at 20 %, low_critical at 10 %).

get_usage Free

get_usage(days: 'int' = 30) → 'UsageResponse'

Aggregate token usage over a recent window.

  • days Lookback window. Default 30; max 365.

UsageResponse with daily buckets, per-route totals, and total spent.

list_keys Free

list_keys() → 'list[dict[str, Any]]'

List every API key on the tenant.

List of key dicts; each carries key_id, name, prefix (first 8 chars of the key for display only), created_at, and last_used_at. The full secret is **never** returned — only create_key ever exposes it.

create_key Free

create_key(name: 'str') → 'dict[str, Any]'

Mint a new API key.

The full key value (nsy_live_* / nsy_test_*) is included in this response **only**; subsequent list_keys calls return only the prefix. Store the secret immediately.

  • name Human label (shown in the dashboard, audit log).

Dict with key_id, name, key (full secret — store this!), and created_at.

revoke_key Free

revoke_key(key_id: 'str') → 'dict[str, Any]'

Revoke an API key.

Revocation is immediate at the API layer. In-flight requests already in transit may still complete, but the next request with the revoked key returns 401.

  • key_id Identifier from list_keys (not the secret).

Dict with revoked_at.

get_stats Free

get_stats() → 'dict[str, Any]'

Roll up tenant-wide session and usage stats.

Useful for embedding a "your numbers" panel in your own admin UI. Counts include sessions/day, triage distribution, and the most-used chief complaints.

Raw dict; shape mirrors what the dashboard's Overview page displays.

get_rate_limit Free

get_rate_limit() → 'dict[str, Any]'

Read current rate-limit usage for the calling key.

Dict with rpm_limit, rpm_used, rpm_remaining, and window_reset_seconds. Mirrors the X-RateLimit-* response headers but in JSON form so you can render it in a dashboard.

get_audit_log Free

get_audit_log(limit: 'int' = 50, offset: 'int' = 0) → 'dict[str, Any]'

Read the tenant audit log.

Records every privileged action — key rotation, team invites, webhook subscription changes, role changes. Visible to owner / admin / developer roles; viewer and billing get 403.

  • limit Page size (max 200).
  • offset Pagination offset.

Dict with events (list), total, next_cursor.

Lifecycle

close

close()

Close the underlying HTTP connection pool.

Use the context-manager form (with NessyClient(...) as nessy:) when possible — it closes the client automatically, even on exceptions. Manual close() is for long-lived processes that outlive a single with block.

Response models

Every response from the SDK is a typed dataclass. Each carries a .raw dict with the original JSON so new fields the API ships before you upgrade the SDK are still reachable (response.raw["new_field"]) — forward-compatible by construction.

SessionResponse

Typed wrapper for POST /v1/sessions and GET /v1/sessions/{id}/state.

  • session_id str
  • status str
  • triage_level Optional[str] default: None
  • current_question Question | None default: None
  • tokens_charged Optional[int] default: None
  • meta Optional[MetaInfo] default: None

AnswerResponse

Typed wrapper for POST /v1/sessions/{id}/answer.

  • session_id str
  • is_complete bool
  • current_question Question | None default: None
  • questions_asked int default: 0
  • triage_level Optional[str] default: None
  • tokens_charged Optional[int] default: None
  • extracted_fields dict[str, Any] default: (empty)
  • red_flags_detected list[str] default: (empty)
  • warnings list[str] default: (empty)
  • meta Optional[MetaInfo] default: None

ResultsResponse

Typed wrapper for POST /v1/sessions/{id}/finalize and GET /results.

  • session_id str
  • triage_level str
  • differentials list[Differential]
  • red_flags list[str]
  • primary_diagnosis str | None default: None
  • primary_diagnosis_icd str | None default: None
  • primary_probability float | None default: None
  • recommendations list[str] default: (empty)
  • questions_asked int default: 0
  • tokens_charged int | None default: None
  • meta MetaInfo | None default: None

BalanceResponse

BalanceResponse(balance: 'int', lifetime_used: 'int', tier: 'str', raw: 'dict[str, Any]' = <factory>)

  • balance int
  • lifetime_used int
  • tier str

UsageResponse

Typed wrapper for GET /v1/admin/usage.

  • balance int default: 0
  • lifetime_used int default: 0
  • records list[UsageRecord] default: (empty)
  • total_tokens int default: 0
  • days int default: 30

PatientProfile

PatientProfile(partner_patient_id: 'str', chronic_conditions: 'list[str]' = <factory>, current_medications: 'list[str]' = <factory>, allergies: 'list[str]' = <factory>, risk_factors: 'list[str]' = <factory>, family_history: 'list[str]' = <factory>, smoking_status: 'str' = '', alcohol_status: 'str' = '', raw: 'dict[str, Any]' = <factory>)

  • partner_patient_id str
  • chronic_conditions list[str] default: (empty)
  • current_medications list[str] default: (empty)
  • allergies list[str] default: (empty)
  • risk_factors list[str] default: (empty)
  • family_history list[str] default: (empty)
  • smoking_status str default: ''
  • alcohol_status str default: ''

QuestionsResponse

QuestionsResponse(session_id: 'str', questions_asked: 'int', questions_remaining: 'int | None', questions_total: 'int', current_question_id: 'str | None', questions: 'list[Question]', raw: 'dict[str, Any]' = <factory>)

  • session_id str
  • questions_asked int
  • questions_remaining int | None
  • questions_total int
  • current_question_id str | None
  • questions list[Question]

SessionListResponse

SessionListResponse(total: 'int', sessions: 'list[SessionListItem]', raw: 'dict[str, Any]' = <factory>)

  • total int
  • sessions list[SessionListItem]

SessionListItem

SessionListItem(session_id: 'str', patient_id: 'str | None', chief_complaint: 'str', triage_level: 'str', primary_diagnosis: 'str | None', questions_asked: 'int | None', created_at: 'str | None', completed_at: 'str | None')

  • session_id str
  • patient_id str | None
  • chief_complaint str
  • triage_level str
  • primary_diagnosis str | None
  • questions_asked int | None
  • created_at str | None
  • completed_at str | None

Question

Question(question_id: 'str', text: 'str', answer_type: 'str' = 'free_text', options: 'list[str]' = <factory>, branch: 'str' = '', branch_display: 'str' = '', text_cs: 'str | None' = None, is_red_flag: 'bool' = False, targets: 'list[str]' = <factory>)

  • question_id str
  • text str
  • answer_type str default: 'free_text'
  • options list[str] default: (empty)
  • branch str default: ''
  • branch_display str default: ''
  • text_cs str | None default: None
  • is_red_flag bool default: False
  • targets list[str] default: (empty)

Differential

Differential(diagnosis: 'str', probability: 'float', icd10: 'str' = '')

  • diagnosis str
  • probability float
  • icd10 str default: ''

MetaInfo

Per-call metadata: token charges, processing time.

  • tokens_charged int default: 0
  • processing_time_ms int default: 0

QuestionInfo

Slimmer question wrapper used by the typed Session/Answer responses.

  • question_id str
  • text str
  • text_cs Optional[str] default: None
  • targets list[str] default: (empty)
  • is_red_flag bool default: False

UsageRecord

UsageRecord(date: 'str', action: 'str', tokens_charged: 'int', count: 'int', raw: 'dict[str, Any]' = <factory>)

  • date str
  • action str
  • tokens_charged int
  • count int

Webhooks

Subscribe to webhook events from the dashboard's Webhooks page. Each delivery is signed with HMAC-SHA256 over the timestamp + body; verify with verify_webhook_signature:

verify_webhook_signature

verify_webhook_signature(payload: 'bytes', secret: 'str', signature: 'str', timestamp: 'str | None' = None, tolerance_seconds: 'int' = 300) → 'bool'

Verify a NessyAPI webhook signature.

  • payload Raw request body bytes
  • secret Your webhook signing secret
  • signature Value of X-NessyAPI-Signature-256 header
  • timestamp Value of X-NessyAPI-Timestamp header. Required for replay-safe verification — the server includes the timestamp in the signed content.
  • tolerance_seconds Max age of timestamp in seconds (default 5 min). Set to 0 to disable age check.

True if signature is valid (and timestamp is within tolerance).

Errors

Every non-2xx response is raised as NessyAPIError. Inspect .status_code and .error_code to dispatch — see the full code catalogue for every code with recovery steps.

NessyAPIError

class NessyAPIError(Exception)

Raised on HTTP 4xx/5xx responses from NessyAPI.

  • status_code HTTP status returned (e.g. 401, 422, 429).
  • error_code Stable string from the error catalogue (e.g. rate_limited).
  • request_id Server-side correlation id; include this when filing support tickets.
  • detail Optional structured payload, e.g. validation field errors.
try:
    nessy.create_session(chief_complaint="...")
except NessyAPIError as e:
    if e.error_code == "rate_limited":
        time.sleep(60)
    elif e.error_code == "payment_required":
        topup_balance()
    else:
        raise