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.
Arguments
api_keyBearer API key. Usensy_test_*for sandbox traffic (zero token cost) ornsy_live_*for production. Manage keys from the dashboard's API Keys page.base_urlAPI root URL. Defaults to the Azure-hosted endpoint; override only when targeting staging or a self-hosted deployment.timeoutPer-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_retriesNumber of automatic retries on transient failures (HTTP 429 / 5xx, network errors). Each retry uses exponential backoff with jitter and respectsRetry-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').
Arguments
chief_complaintPrimary symptom or complaint, lowercase snake-style (e.g."headache","chest_pain"). Routing matches against the engine's complaint catalogue.agePatient age in years. Influences risk stratification + age-gated branches.sexPatient sex ("male"/"female"/"other").free_textOptional natural-language description. When supplied, the engine runs NLP extraction to pre-fill demographics + flags before the first question.patient_idYour internal stable identifier. When provided the session is linked to the patient profile, enabling chronic-conditions and medications to bias differentials.
Returns
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.
Arguments
session_idIdentifier returned bycreate_session.textFree-text symptom narrative (Czech or English).
Returns
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.
Arguments
session_idIdentifier returned bycreate_session.question_idcurrent_question.question_idfrom the previous response. Validates against the active question set.raw_textPatient's free-text answer. NLP extracts duration, severity, location, etc. Mutually exclusive with pre-extractedextracted_fields.extracted_fieldsAlready-parsed structured values ({"duration_hours": 3, "severity": 7}). Skips the LLM call — use when your client has its own NLP layer.extracted_flagsBoolean clinical flags (["smoker", "fever"]). Same skip-NLP behaviour.skipMark the question unanswered and advance. The engine records "no answer" rather than treating it as a negative.
Returns
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.
Arguments
session_idIdentifier returned bycreate_session.
Returns
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.
Arguments
session_idIdentifier returned bycreate_session.
Returns
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.
Arguments
session_idIdentifier returned bycreate_session.
Returns
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.
Arguments
session_idIdentifier returned bycreate_session.
Returns
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.
Arguments
chief_complaintSame ascreate_session.chief_complaint.agePatient age in years.sexPatient sex.answersOptional{question_id: free_text}map. Each entry is sent verbatim asraw_texttoanswer. Missing question_ids are skipped.max_questionsSafety cap on the loop length; default 15.
Returns
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.
Arguments
statusFilter by session status ("in_progress","finished","abandoned").Nonereturns all.patient_idRestrict to a single patient profile.limitPage size (max 200).offsetPagination offset.
Returns
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.
Arguments
session_idIdentifier returned bycreate_session.
Returns
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.
Arguments
session_idIdentifier returned bycreate_session.agePatient age in years. PassNoneto leave unchanged.sexPatient sex ("male"/"female"/"other").
Returns
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.
Arguments
accuracy"correct"/"incorrect"/"partially_correct". Required.actual_diagnosisFree-text label of the real diagnosis, when known (e.g."acute coronary syndrome").actual_icd10ICD-10 code (e.g."I21.4"); helps us match against differential entries that carried the same code.notesFree-text comments; surfaced in the audit log.
Returns
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.
Arguments
patient_idStable identifier you supplied previously. Returns 404 if no profile exists.
Returns
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.
Arguments
patient_idStable identifier. New IDs auto-create the profile.chronic_conditionsICD-10 labels or free-text condition names.current_medicationsGeneric or trade names.allergiesFree-text or coded allergens.risk_factorsLifestyle / occupational risk markers.family_historyConditions in first-degree relatives.smoking_statusOne of"never"/"former"/"current"/"unknown".alcohol_statusSame enum assmoking_status.
Returns
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.
Arguments
patient_idStable identifier you supplied previously.
Returns
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.
Arguments
patient_idStable identifier you supplied previously.
Returns
Raw dict with deleted_sessions count + status.
Admin & billing
get_balance Free
get_balance() → 'BalanceResponse'
Read the current token balance.
Returns
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.
Arguments
daysLookback window. Default 30; max 365.
Returns
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.
Returns
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.
Arguments
nameHuman label (shown in the dashboard, audit log).
Returns
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.
Arguments
key_idIdentifier fromlist_keys(not the secret).
Returns
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.
Returns
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.
Returns
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.
Arguments
limitPage size (max 200).offsetPagination offset.
Returns
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.
Fields
session_idstrstatusstrtriage_levelOptional[str] default:Nonecurrent_questionQuestion | None default:Nonetokens_chargedOptional[int] default:NonemetaOptional[MetaInfo] default:None
AnswerResponse
Typed wrapper for POST /v1/sessions/{id}/answer.
Fields
session_idstris_completeboolcurrent_questionQuestion | None default:Nonequestions_askedint default:0triage_levelOptional[str] default:Nonetokens_chargedOptional[int] default:Noneextracted_fieldsdict[str, Any] default:(empty)red_flags_detectedlist[str] default:(empty)warningslist[str] default:(empty)metaOptional[MetaInfo] default:None
ResultsResponse
Typed wrapper for POST /v1/sessions/{id}/finalize and GET /results.
Fields
session_idstrtriage_levelstrdifferentialslist[Differential]red_flagslist[str]primary_diagnosisstr | None default:Noneprimary_diagnosis_icdstr | None default:Noneprimary_probabilityfloat | None default:Nonerecommendationslist[str] default:(empty)questions_askedint default:0tokens_chargedint | None default:NonemetaMetaInfo | None default:None
BalanceResponse
BalanceResponse(balance: 'int', lifetime_used: 'int', tier: 'str', raw: 'dict[str, Any]' = <factory>)
Fields
balanceintlifetime_usedinttierstr
UsageResponse
Typed wrapper for GET /v1/admin/usage.
Fields
balanceint default:0lifetime_usedint default:0recordslist[UsageRecord] default:(empty)total_tokensint default:0daysint 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>)
Fields
partner_patient_idstrchronic_conditionslist[str] default:(empty)current_medicationslist[str] default:(empty)allergieslist[str] default:(empty)risk_factorslist[str] default:(empty)family_historylist[str] default:(empty)smoking_statusstr default:''alcohol_statusstr 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>)
Fields
session_idstrquestions_askedintquestions_remainingint | Nonequestions_totalintcurrent_question_idstr | Nonequestionslist[Question]
SessionListResponse
SessionListResponse(total: 'int', sessions: 'list[SessionListItem]', raw: 'dict[str, Any]' = <factory>)
Fields
totalintsessionslist[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')
Fields
session_idstrpatient_idstr | Nonechief_complaintstrtriage_levelstrprimary_diagnosisstr | Nonequestions_askedint | Nonecreated_atstr | Nonecompleted_atstr | 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>)
Fields
question_idstrtextstranswer_typestr default:'free_text'optionslist[str] default:(empty)branchstr default:''branch_displaystr default:''text_csstr | None default:Noneis_red_flagbool default:Falsetargetslist[str] default:(empty)
Differential
Differential(diagnosis: 'str', probability: 'float', icd10: 'str' = '')
Fields
diagnosisstrprobabilityfloaticd10str default:''
MetaInfo
Per-call metadata: token charges, processing time.
Fields
tokens_chargedint default:0processing_time_msint default:0
QuestionInfo
Slimmer question wrapper used by the typed Session/Answer responses.
Fields
question_idstrtextstrtext_csOptional[str] default:Nonetargetslist[str] default:(empty)is_red_flagbool default:False
UsageRecord
UsageRecord(date: 'str', action: 'str', tokens_charged: 'int', count: 'int', raw: 'dict[str, Any]' = <factory>)
Fields
datestractionstrtokens_chargedintcountint
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.
Arguments
payloadRaw request body bytessecretYour webhook signing secretsignatureValue of X-NessyAPI-Signature-256 headertimestampValue of X-NessyAPI-Timestamp header. Required for replay-safe verification — the server includes the timestamp in the signed content.tolerance_secondsMax age of timestamp in seconds (default 5 min). Set to 0 to disable age check.
Returns
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.
Attributes
status_codeHTTP status returned (e.g. 401, 422, 429).error_codeStable string from the error catalogue (e.g.rate_limited).request_idServer-side correlation id; include this when filing support tickets.detailOptional 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