NessyAPI (0.2.0)

Download OpenAPI specification:

NessyAPI — Clinical Decision Support API

A B2B clinical triage engine accessed via HTTPS and authenticated with Bearer API keys. Create sessions, drive Q&A flows, and receive structured triage and differential-diagnosis output. This document covers the cross-cutting conventions every integrator needs: authentication, pagination, errors, idempotency, rate limits, webhooks, and pricing. Per-endpoint schemas are in the reference below.

Authentication

All requests require an API key supplied in the Authorization header:

Authorization: Bearer nsy_live_XXXXXXXXXXXXXXXXXXXXXXXXXX

Two key prefixes are recognised:

Prefix Billing Intended use
nsy_test_ No billing, sandbox CI, staging, QA, local development
nsy_live_ Billable, production Production traffic from paying users

Each key carries a set of scopes enforced per request:

  • sessions:read — list, read, and export sessions
  • sessions:write — create, answer, route, and finalize sessions
  • patients:read — read patient profiles and per-patient history
  • patients:write — create, update, and delete patient profiles
  • webhooks:read — list webhook subscriptions and delivery history
  • webhooks:write — create, rotate, test, and delete webhook subscriptions
  • admin:read — read tenant stats, audit log, keys, tiers, usage
  • admin:write — create keys, change tier, update settings

Cookie-based auth (nessy_session) is also supported for the first-party dashboard only; all B2B integrations should use Bearer keys.

Pagination

List endpoints (sessions, audit log, keys, deliveries) accept limit and offset query parameters. Defaults are limit=50, offset=0; the maximum limit is 200. Responses include total, limit, offset, and the data array so clients can build stable pagers.

Error envelope

Every error response — regardless of status code — uses one consistent shape:

{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation",
    "request_id": "b3c4e8a1-2d1f-4e0c-9d6b-6a1d2e3f4a5b",
    "detail": { "field_errors": [ ... ] }
  }
}

The code field maps directly from HTTP status:

Status code
400 bad_request
401 unauthorized
403 forbidden
404 not_found
409 conflict
422 validation_error
429 rate_limited
500 internal_server_error
503 service_unavailable

request_id is echoed from the X-Request-ID request header (or generated server-side) and is the single correlation key you should quote when filing support tickets.

Idempotency

Mutating POST endpoints (notably POST /v1/sessions and its answer/route/ finalize children) accept an Idempotency-Key header. Keys are scoped to the authenticated tenant and cached for 24 hours. Replaying a request with the same key + tenant returns the cached response unchanged and sets the header Idempotency-Replayed: true on the response. Use a fresh UUIDv4 per logical action on the client side.

Rate limits

Per-API-key rate limits are enforced in a 1-minute rolling window and surfaced on every response via:

  • X-RateLimit-Limit — permitted requests per window
  • X-RateLimit-Remaining — requests left in the current window
  • X-RateLimit-Reset — unix epoch seconds at which the window resets
  • Retry-After (on 429 only) — seconds the client should wait

Exceeding the limit returns 429 Rate Limited with the unified error envelope:

{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit of 60 requests/minute exceeded.",
    "request_id": "...",
    "detail": { "retry_after": 42, "limit": 60 }
  }
}

Default limits by tier: free_trial 10 rpm, starter 30 rpm, growth 60 rpm, scale 120 rpm, enterprise 300 rpm. Per-key overrides may set stricter limits.

Webhooks

Webhook subscriptions deliver events over HTTPS POST with an HMAC SHA-256 signature computed over "{timestamp}.{body}" using the subscription's signing secret, encoded as lowercase hex. Two headers accompany every delivery:

  • X-NessyAPI-Signature-256: <hex>
  • X-NessyAPI-Timestamp: <unix epoch seconds>

Receivers should reject any delivery whose timestamp is more than 5 minutes skewed from now (replay protection), and must compute the expected signature with a constant-time comparison.

Available event types:

  • session.created
  • session.answered
  • session.finalized
  • assessment.completed
  • red_flag.detected
  • triage.escalated
  • session.timeout
  • balance.low_20
  • balance.low_10
  • balance.depleted

Use POST /v1/admin/webhooks/{id}/test to fire a synthetic delivery against the configured URL; the response includes status_code, latency_ms, and any error returned by the receiver.

Pricing tiers

Tier $/month Tokens included Concurrent Keys rpm SLA
free_trial $0 1 000 5 2 10
starter $300 10 000 20 5 30 99%
growth $1 300 50 000 50 10 60 99.5%
scale $5 750 250 000 200 25 120
enterprise $20 000 1 000 000 1 000 100 300 99.9%

Per-endpoint token cost

Endpoint Tokens charged
POST /v1/sessions (create) 1
POST /v1/sessions/{id}/route 5
POST /v1/sessions/{id}/answer 8
POST /v1/sessions/{id}/answer (skipped) 0
POST /v1/sessions/{id}/finalize 0
GET /v1/sessions/{id}/results 0
GET /v1/sessions/{id}/state 0
Any /v1/admin/* or /v1/patients/* endpoint 0

All charged responses also carry the header X-NessyAPI-Tokens-Charged: <int> so clients can reconcile cost per call without parsing the body.

Sessions

List Sessions

List sessions for this tenant — merges active (Redis) with finalized (PG).

query Parameters
Status (string) or Status (null) (Status)

Filter: active, finalized

Patient Id (string) or Patient Id (null) (Patient Id)

Filter by partner_patient_id

Chief Complaint (string) or Chief Complaint (null) (Chief Complaint)

Filter by chief complaint

Date From (string) or Date From (null) (Date From)

ISO date, e.g. 2026-01-01

Date To (string) or Date To (null) (Date To)

ISO date, e.g. 2026-12-31

limit
integer (Limit) [ 1 .. 200 ]
Default: 50
offset
integer (Offset) >= 0
Default: 0

Responses

Response samples

Content type
application/json
null

Create Session

Create a new clinical anamnesis session.

Request Body schema: application/json
required
chief_complaint
required
string (Chief Complaint) [ 1 .. 500 ] characters
Age (integer) or Age (null) (Age)
Sex (string) or Sex (null) (Sex)
Free Text (string) or Free Text (null) (Free Text)
Initial Fields (object) or Initial Fields (null) (Initial Fields)
Patient Id (string) or Patient Id (null) (Patient Id)

Responses

Request samples

Content type
application/json
{
  • "chief_complaint": "string",
  • "age": 120,
  • "sex": "string",
  • "free_text": "string",
  • "initial_fields": { },
  • "patient_id": "string"
}

Response samples

Content type
application/json
null

Export Session

Export a complete session record (decrypted) including all clinical data.

path Parameters
session_id
required
string (Session Id)

Responses

Response samples

Content type
application/json
null

Tenant Stats

Get aggregated statistics for this tenant.

Responses

Response samples

Content type
application/json
null

Bulk Export

Bulk export completed sessions as JSON for compliance/reporting.

Request Body schema: application/json
required
Date From (string) or Date From (null) (Date From)
Date To (string) or Date To (null) (Date To)
Patient Id (string) or Patient Id (null) (Patient Id)
limit
integer (Limit)
Default: 1000

Responses

Request samples

Content type
application/json
{
  • "date_from": "string",
  • "date_to": "string",
  • "patient_id": "string",
  • "limit": 1000
}

Response samples

Content type
application/json
null

Audit Log

List audit events for this tenant.

query Parameters
limit
integer (Limit) [ 1 .. 200 ]
Default: 50
offset
integer (Offset) >= 0
Default: 0
Action (string) or Action (null) (Action)

Filter by action, e.g. auth.login

Date From (string) or Date From (null) (Date From)
Date To (string) or Date To (null) (Date To)

Responses

Response samples

Content type
application/json
null

Rate Limit Status

Get current rate limit status for this tenant's API key.

T2.9 — No scope required. Any authenticated API key can query its own current bucket so partner UIs can show "requests remaining" indicators without giving their client-side code admin:read.

Responses

Response samples

Content type
application/json
null

Get Session State

Get current session state (lightweight, no differentials).

path Parameters
session_id
required
string (Session Id)

Responses

Response samples

Content type
application/json
{
  • "session_id": "string",
  • "status": "string",
  • "questions_asked": 0,
  • "is_complete": true,
  • "triage_level": "string",
  • "active_branches": [
    ],
  • "red_flags": [
    ],
  • "current_question": {
    },
  • "created_at": "2019-08-24T14:15:22Z"
}

Submit Answer

Submit a patient answer to the current question.

If raw_text is provided and extracted_fields is empty, server-side NLP extraction (Azure OpenAI) populates fields and flags automatically. Client-provided fields always take priority over NLP.

path Parameters
session_id
required
string (Session Id)
Request Body schema: application/json
required
question_id
required
string (Question Id) [ 1 .. 100 ] characters
Raw Text (string) or Raw Text (null) (Raw Text)
object (Extracted Fields)
extracted_flags
Array of strings (Extracted Flags)
Field Confidences (object) or Field Confidences (null) (Field Confidences)
skip
boolean (Skip)
Default: false

Responses

Request samples

Content type
application/json
{
  • "question_id": "string",
  • "raw_text": "string",
  • "extracted_fields": { },
  • "extracted_flags": [
    ],
  • "field_confidences": {
    },
  • "skip": false
}

Response samples

Content type
application/json
null

Get Results

Get current differential diagnoses, triage level, and red flags.

This endpoint is FREE (0 tokens) and can be called at any point during the session — not just after finalization.

path Parameters
session_id
required
string (Session Id)

Responses

Response samples

Content type
application/json
{
  • "session_id": "string",
  • "triage_level": "string",
  • "differentials": [
    ],
  • "red_flags": [
    ],
  • "primary_diagnosis": "string",
  • "primary_diagnosis_icd": "string",
  • "primary_probability": 0,
  • "questions_asked": 0,
  • "meta": {
    }
}

Finalize Session

Finalize the assessment session.

Triggers final scoring, generates complete clinical summary, and marks the session as read-only. Emits assessment.completed webhook.

path Parameters
session_id
required
string (Session Id)

Responses

Response samples

Content type
application/json
null

Delete Session

Delete a clinical session and all associated data (GDPR Art. 17).

  • Removes session from Redis (full state, tenant mapping, env)
  • Deletes from PostgreSQL clinical_sessions table
  • Anonymizes usage_records (NULLs session_id)
  • Removes from in-memory engine cache
path Parameters
session_id
required
string (Session Id)

Responses

Response samples

Content type
application/json
{ }

List Session Questions

List all questions for this session's active branches with progress info.

path Parameters
session_id
required
string (Session Id)

Responses

Response samples

Content type
application/json
null

Update Demographics

Update session demographics (age, sex) after creation.

path Parameters
session_id
required
string (Session Id)
Request Body schema: application/json
required
Age (integer) or Age (null) (Age)
Sex (string) or Sex (null) (Sex)

Responses

Request samples

Content type
application/json
{
  • "age": 0,
  • "sex": "string"
}

Response samples

Content type
application/json
null

Submit Feedback

Submit feedback on diagnosis accuracy for a completed session.

path Parameters
session_id
required
string (Session Id)
Request Body schema: application/json
required
accuracy
required
string (Accuracy)
Actual Diagnosis (string) or Actual Diagnosis (null) (Actual Diagnosis)
Actual Icd10 (string) or Actual Icd10 (null) (Actual Icd10)
Notes (string) or Notes (null) (Notes)

Responses

Request samples

Content type
application/json
{
  • "accuracy": "string",
  • "actual_diagnosis": "string",
  • "actual_icd10": "string",
  • "notes": "string"
}

Response samples

Content type
application/json
null

Rectify Session

Rectify a session's data (GDPR Art. 16 — Right to Rectification).

Only editable BEFORE finalization. Writes an audit event with the list of fields changed (never the values — those would leak PHI). Allowed edits: - Update age/sex demographics - Erase specific answers by question_id (removes from session state)

path Parameters
session_id
required
string (Session Id)
Request Body schema: application/json
required
Age (integer) or Age (null) (Age)
Sex (string) or Sex (null) (Sex)
Array of Clear Answers (strings) or Clear Answers (null) (Clear Answers)
Reason (string) or Reason (null) (Reason)

Responses

Request samples

Content type
application/json
{
  • "age": 0,
  • "sex": "string",
  • "clear_answers": [
    ],
  • "reason": "string"
}

Response samples

Content type
application/json
null

Admin

List Sessions

List sessions for this tenant — merges active (Redis) with finalized (PG).

query Parameters
Status (string) or Status (null) (Status)

Filter: active, finalized

Patient Id (string) or Patient Id (null) (Patient Id)

Filter by partner_patient_id

Chief Complaint (string) or Chief Complaint (null) (Chief Complaint)

Filter by chief complaint

Date From (string) or Date From (null) (Date From)

ISO date, e.g. 2026-01-01

Date To (string) or Date To (null) (Date To)

ISO date, e.g. 2026-12-31

limit
integer (Limit) [ 1 .. 200 ]
Default: 50
offset
integer (Offset) >= 0
Default: 0

Responses

Response samples

Content type
application/json
null

Export Session

Export a complete session record (decrypted) including all clinical data.

path Parameters
session_id
required
string (Session Id)

Responses

Response samples

Content type
application/json
null

Tenant Stats

Get aggregated statistics for this tenant.

Responses

Response samples

Content type
application/json
null

Bulk Export

Bulk export completed sessions as JSON for compliance/reporting.

Request Body schema: application/json
required
Date From (string) or Date From (null) (Date From)
Date To (string) or Date To (null) (Date To)
Patient Id (string) or Patient Id (null) (Patient Id)
limit
integer (Limit)
Default: 1000

Responses

Request samples

Content type
application/json
{
  • "date_from": "string",
  • "date_to": "string",
  • "patient_id": "string",
  • "limit": 1000
}

Response samples

Content type
application/json
null

Audit Log

List audit events for this tenant.

query Parameters
limit
integer (Limit) [ 1 .. 200 ]
Default: 50
offset
integer (Offset) >= 0
Default: 0
Action (string) or Action (null) (Action)

Filter by action, e.g. auth.login

Date From (string) or Date From (null) (Date From)
Date To (string) or Date To (null) (Date To)

Responses

Response samples

Content type
application/json
null

Rate Limit Status

Get current rate limit status for this tenant's API key.

T2.9 — No scope required. Any authenticated API key can query its own current bucket so partner UIs can show "requests remaining" indicators without giving their client-side code admin:read.

Responses

Response samples

Content type
application/json
null

Dashboard Login

Exchange API key for httpOnly JWT session cookie.

Hardened 2026-04-16 per code review — previously this endpoint:

  • had no rate limiting (brute-force oracle for API keys)
  • minted a JWT without jti so logout revocation silently no-op'd
  • set nessy_session cookie without the companion nessy_csrf, so the client's next mutating call hit _verify_csrf and failed

Fix: per-IP + per-key-hash rate limit, jti, CSRF cookie companion.

Responses

Response samples

Content type
application/json
null

Dashboard Logout

Clear httpOnly session cookie + CSRF cookie; blacklist JWT jti.

Responses

Response samples

Content type
application/json
null

List Keys

List all active API keys for this tenant.

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Create Key

Create a new API key for this tenant.

The raw key is returned ONCE in the response. Store it securely.

Supports Idempotency-Key header (24h TTL). On replay, the cached response — including the raw_key — is returned verbatim so clients safely retrying transient 5xxs don't end up with N keys they never saw. Non-2xx responses are never cached.

Request Body schema: application/json
required
name
required
string (Name) [ 1 .. 100 ] characters
Array of Scopes (strings) or Scopes (null) (Scopes)
rate_limit_rpm
integer (Rate Limit Rpm) [ 1 .. 10000 ]
Default: 60
test
boolean (Test)
Default: false

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "scopes": [
    ],
  • "rate_limit_rpm": 60,
  • "test": false
}

Response samples

Content type
application/json
{
  • "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5",
  • "key_prefix": "string",
  • "name": "string",
  • "scopes": [
    ],
  • "rate_limit_rpm": 0,
  • "is_active": true,
  • "last_used_at": "2019-08-24T14:15:22Z",
  • "expires_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z",
  • "key_suffix": "string",
  • "raw_key": "string"
}

Rotate Key

Rotate an API key — generates a new key with the same scopes/rate_limit, revokes the old one.

The new raw key is returned ONCE in the response. Store it securely.

Supports Idempotency-Key header (24h TTL). Retrying the same idempotency key returns the same response body (including raw_key) rather than minting a second new key and revoking the rotated one twice.

path Parameters
key_id
required
string <uuid> (Key Id)

Responses

Response samples

Content type
application/json
{
  • "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5",
  • "key_prefix": "string",
  • "name": "string",
  • "scopes": [
    ],
  • "rate_limit_rpm": 0,
  • "is_active": true,
  • "last_used_at": "2019-08-24T14:15:22Z",
  • "expires_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z",
  • "key_suffix": "string",
  • "raw_key": "string"
}

Update Key

Update mutable fields of an API key: name, scopes, rate_limit_rpm.

Immutable fields (key_prefix, test/live, the secret itself) require rotation instead. Scope escalation is prevented: the caller can only grant scopes they themselves hold.

path Parameters
key_id
required
string <uuid> (Key Id)
Request Body schema: application/json
required
Name (string) or Name (null) (Name)
Array of Scopes (strings) or Scopes (null) (Scopes)
Rate Limit Rpm (integer) or Rate Limit Rpm (null) (Rate Limit Rpm)

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "scopes": [
    ],
  • "rate_limit_rpm": 1
}

Response samples

Content type
application/json
{
  • "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5",
  • "key_prefix": "string",
  • "name": "string",
  • "scopes": [
    ],
  • "rate_limit_rpm": 0,
  • "is_active": true,
  • "last_used_at": "2019-08-24T14:15:22Z",
  • "expires_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z",
  • "key_suffix": "string"
}

Delete Key

Revoke an API key.

path Parameters
key_id
required
string <uuid> (Key Id)

Responses

Response samples

Content type
application/json
{ }

Get Balance Endpoint

Get current token balance.

Responses

Response samples

Content type
application/json
{
  • "tenant_id": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "balance": 0,
  • "lifetime_used": 0,
  • "tier": "string",
  • "burn_rate_7d": 0,
  • "estimated_days_remaining": 0
}

Get Usage

Get aggregated token usage for the last N days.

query Parameters
days
integer (Days)
Default: 30

Responses

Response samples

Content type
application/json
{
  • "tenant_id": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "balance": 0,
  • "lifetime_used": 0,
  • "period_start": "2019-08-24T14:15:22Z",
  • "period_end": "2019-08-24T14:15:22Z",
  • "records": [
    ]
}

Get Usage Details

Get individual (non-aggregated) usage records for audit/billing.

Returns up to limit records ordered by created_at DESC. Optional date filters: from_date/to_date in YYYY-MM-DD format.

query Parameters
From Date (string) or From Date (null) (From Date)
To Date (string) or To Date (null) (To Date)
limit
integer (Limit)
Default: 100

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Get Usage Summary Endpoint

Per-tenant usage rollup for the dashboard Usage page.

Returns totals, per-route breakdown with latency percentiles (derived from usage_records when available; otherwise reported as None), time-bucketed counts for charting, and a balance snapshot series.

Primary source: usage_records (append-only ledger, tenant-scoped via RLS). Latency percentiles come from the optional latency_ms column if present; when absent the endpoint still returns request/token counts so the chart renders.

Scope: admin:read.

query Parameters
range
string (Range) ^(1h|24h|7d|30d)$
Default: "24h"
group_by
string (Group By) ^(route|day|hour)$
Default: "route"

Responses

Response samples

Content type
application/json
{ }

Balance Status

Return current balance + threshold context for the low-balance UI.

Reads:

  • token_balances row (balance, initial_balance, lifetime_used, updated_at)
  • tenant.tier from the auth dependency

Does NOT consult Redis separately — the balance stored in Postgres is the source of truth; Redis is only a cache for the 402 pre-check hot path. If you need the cached value, call /v1/admin/balance.

Responses

Response samples

Content type
application/json
{
  • "balance": 0,
  • "initial_balance": 0,
  • "lifetime_used": 0,
  • "percentage_remaining": 0,
  • "current_threshold": "string",
  • "next_threshold": {
    },
  • "tier": "string",
  • "last_topup_at": "string"
}

List Webhooks

List all active webhook subscriptions.

Responses

Response samples

Content type
application/json
[
  • { }
]

Create Webhook

Create a new webhook subscription.

The signing secret is returned ONCE. Store it securely.

Request Body schema: application/json
required
url
required
string (Url) [ 10 .. 500 ] characters
events
required
Array of strings (Events) non-empty

Responses

Request samples

Content type
application/json
{
  • "url": "stringstri",
  • "events": [
    ]
}

Response samples

Content type
application/json
{ }

Delete Webhook

Delete a webhook subscription.

path Parameters
subscription_id
required
string <uuid> (Subscription Id)

Responses

Response samples

Content type
application/json
{ }

List Webhook Deliveries

List delivery history for a webhook subscription.

path Parameters
subscription_id
required
string <uuid> (Subscription Id)
query Parameters
limit
integer (Limit)
Default: 50

Responses

Response samples

Content type
application/json
[
  • { }
]

Rotate Webhook Secret

Rotate webhook signing secret. Old secret valid for 24h grace period.

The actual encryption + UPDATE lives in nessyapi.webhooks.dispatcher.rotate_webhook_secret — this handler is only a tenant-scoped gate before delegating.

path Parameters
subscription_id
required
string <uuid> (Subscription Id)

Responses

Response samples

Content type
application/json
{ }

Test Webhook

Send a test ping to the webhook URL.

path Parameters
subscription_id
required
string <uuid> (Subscription Id)

Responses

Response samples

Content type
application/json
{ }

List Deliveries

List recent webhook deliveries for the calling tenant.

Default filter surfaces the actionable failures (failed, dead-lettered, exhausted) — matching the dashboard's "show me what's broken" workflow. Pass ?status=delivered for the success log.

query Parameters
Subscription Id (string) or Subscription Id (null) (Subscription Id)
Status (string) or Status (null) (Status)

comma-separated status list

page
integer (Page) >= 1
Default: 1
per_page
integer (Per Page) [ 1 .. 200 ]
Default: 50

Responses

Response samples

Content type
application/json
{ }

Webhook Failures Summary

Lightweight counter endpoint for the sidebar badge + top banner.

Returns the number of deliveries in an actionable failure state plus the current DLQ length (shared across tenants, admin-only visibility; other tenants see 0).

Responses

Response samples

Content type
application/json
{ }

Get Delivery Detail

Full delivery detail including payload. Use for the detail modal.

path Parameters
delivery_id
required
string <uuid> (Delivery Id)

Responses

Response samples

Content type
application/json
{ }

Replay Delivery

Manually retry a failed / dead-lettered delivery.

Resets next_retry_at = now() so the periodic retry worker picks the row up on its next tick (≤ 30 s). Does NOT re-enqueue via XADD — that would bypass the per-subscription lock and produce a new delivery_id (Guardian invariant §10).

path Parameters
delivery_id
required
string <uuid> (Delivery Id)

Responses

Response samples

Content type
application/json
{ }

Resolve Delivery

Mark a permanently-failed delivery as acknowledged.

The row is preserved for audit. Body: {"reason": "..."} — the note is stored verbatim (trimmed to 500 chars) so operators can grep it later.

path Parameters
delivery_id
required
string <uuid> (Delivery Id)
Request Body schema: application/json
required
property name*
additional property
any

Responses

Request samples

Content type
application/json
{ }

Response samples

Content type
application/json
{ }

List Dlq

Read the DLQ stream via XRANGE. Admin scope required.

The DLQ is a shared (cross-tenant) Redis stream. Visibility is gated behind admin:write so only operators see other tenants' entries — no cross-tenant leak from the partner UI.

query Parameters
limit
integer (Limit) [ 1 .. 500 ]
Default: 50

Responses

Response samples

Content type
application/json
{ }

Replay All Dlq

Drain the DLQ stream back into the main stream.

Each entry is XADDed to nessy:webhooks with its original envelope (minus the DLQ metadata) so the webhook consumer picks it up on the next loop. The DLQ is cleared on success.

Requires {"confirm": true} in the body to reduce the risk of accidental drainage.

Request Body schema: application/json
required
property name*
additional property
any

Responses

Request samples

Content type
application/json
{ }

Response samples

Content type
application/json
{ }

Get Tenant Tier

Get current tier with all configuration details.

Responses

Response samples

Content type
application/json
{ }

Change Tier

Change tenant tier (upgrade or downgrade).

Rate limits for existing API keys stay at their per-key values. New keys will inherit the new tier's default rate limit.

DX-020 deprecation (2026-04-23): tier is accepted as a query-string parameter for backward-compat with the dashboard's initial PUT /v1/admin/tier?tier=<name> shape. This is an anti-pattern — PUT bodies belong in the JSON request body, not the URL. The query-string form will be removed on 2026-07-23 (90-day sunset per RFC 8594).

Response emits Deprecation, Sunset, Link, and Warning: 299 - "Use request body instead" headers. A new body-based alternative (ChangeTierRequest model) will ship in a future sprint; track via docs/code-review-2026-04-23/findings/features-dx.md DX-020.

query Parameters
tier
string (Tier)
Default: "starter"

Responses

Response samples

Content type
application/json
{ }

List Available Tiers

List all available tiers with their configuration.

Responses

Response samples

Content type
application/json
{ }

Get Nlp Settings

Get NLP fair-use settings for this tenant.

Responses

Response samples

Content type
application/json
{ }

Update Nlp Settings

Update NLP fair-use cutoff.

Only Growth, Scale, and Enterprise tiers can customize this. Set nlp_token_cutoff to 0 to always use NLP (until tokens run out).

Responses

Response samples

Content type
application/json
{ }

List Audit Events

List audit events for the calling tenant.

Max window: LIST_MAX_DAYS days (use the export endpoint for longer).

query Parameters
From (string) or From (null) (From)
To (string) or To (null) (To)
Array of Action (strings) or Action (null) (Action)

Filter by action (repeatable).

Actor Id (string) or Actor Id (null) (Actor Id)

Filter by user_id in metadata.

limit
integer (Limit) [ 1 .. 500 ]
Default: 100
Cursor (string) or Cursor (null) (Cursor)

Responses

Response samples

Content type
application/json
{ }

List Audit Actions

Return the distinct action names this tenant has emitted in the trailing days window. Drives the dashboard filter dropdown.

query Parameters
days
integer (Days) [ 1 .. 365 ]
Default: 90

Responses

Response samples

Content type
application/json
{ }

Export Audit Events

Stream the full audit trail for a (potentially long) window.

Requires admin:write scope (stronger than the list endpoint — this is considered a data-egress operation).

query Parameters
From (string) or From (null) (From)
To (string) or To (null) (To)
Array of Action (strings) or Action (null) (Action)
format
string (Format) ^(csv|ndjson)$
Default: "csv"

Responses

Response samples

Content type
application/json
null

Recent API requests for the current tenant (last 24h, max 100)

Return the most recent inbound requests for the tenant.

Lossy ring buffer with a 24h TTL — see request_inspector.py.

query Parameters
limit
integer (Limit) [ 1 .. 100 ]
Default: 50
cursor
integer (Cursor) >= 0
Default: 0

Responses

Response samples

Content type
application/json
{
  • "entries": [
    ],
  • "total": 0,
  • "next_cursor": 0,
  • "max_entries": 100
}

Get Volume Status

Return the tenant's current volume-eligibility snapshot.

Read-only. Requires admin:read. Computes lifetime spend on the fly (uses 30s balance cache) so the response always reflects current state.

Responses

Response samples

Content type
application/json
{ }

Set Volume Flag

Set or unset settings_json.volume_customer.

Body: {"volume_customer": true|false, "reason": "..." (optional)}.

Audit-logged. Triggers RL cache flush for this tenant so the change is visible within seconds.

Responses

Response samples

Content type
application/json
{ }

Set Rate Limit Override

Set or clear settings_json.rate_limit_rpm_override.

Body:

  • {"rate_limit_rpm": 1000} — set override
  • {"rate_limit_rpm": null} — clear override (use tier default)
  • Optional "reason" field for audit context.

Validation:

  • must be int between 0 and MAX_OVERRIDE_CEILING (5000)
  • 0 / null clears the override

Audit-logged. Triggers RL cache flush so change propagates immediately.

Responses

Response samples

Content type
application/json
{ }

Routing

Route Session

Route free-text description to clinical branch within an existing session.

Uses embedding pre-filter + GPT-4o structured routing. Enriches the session with diagnosis hints and initial fields.

path Parameters
session_id
required
string (Session Id)
Request Body schema: application/json
required
text
required
string (Text) [ 1 .. 2000 ] characters

Responses

Request samples

Content type
application/json
{
  • "text": "string"
}

Response samples

Content type
application/json
{
  • "chief_complaint": "string",
  • "confidence": 0,
  • "secondary_cc": "string",
  • "diagnosis_hints": [
    ],
  • "initial_fields": { },
  • "flags": [
    ],
  • "current_question": {
    },
  • "meta": {
    }
}

Schema

Get Chief Complaints

Get list of supported chief complaints and schema statistics.

Responses

Response samples

Content type
application/json
{
  • "chief_complaints": [
    ],
  • "total_branches": 0,
  • "total_differentials": 0,
  • "schema_version": "string"
}

Get Branches

List all clinical branches, optionally filtered by chief complaint.

query Parameters
Chief Complaint (string) or Chief Complaint (null) (Chief Complaint)

Responses

Response samples

Content type
application/json
null

Patients

List Patients

List patient profiles for the calling tenant.

Pagination is seek-based: pass the previous response's next_cursor to fetch the next page. Empty next_cursor means no more rows.

Filtering:

  • partner_patient_id=<exact> — exact match (takes precedence).
  • q=<substring> — case-insensitive substring on partner_patient_id.

Sort order is newest-updated first with profile_id as a stable tiebreaker, both used as the seek key.

Counts (session_count_total, session_count_active) come from clinical_sessions for finalized rows. session_count_active is a best-effort count of in-progress sessions (completed_at IS NULL) — active sessions that live only in Redis are NOT included here (that would require scanning Redis per-patient, which doesn't scale to thousands of patients). Use GET /patients/{id}/sessions for the full Redis-merged timeline.

Emits a single patient.list_accessed audit row per call (not per patient) — PHI access tracking without log spam.

query Parameters
limit
integer (Limit) [ 1 .. 200 ]
Default: 50

Page size (max 200).

Cursor (string) or Cursor (null) (Cursor)

Opaque cursor from the previous page's next_cursor.

Partner Patient Id (string) or Partner Patient Id (null) (Partner Patient Id)

Exact-match filter on partner_patient_id.

Q (string) or Q (null) (Q)

Case-insensitive substring search on partner_patient_id. Ignored when partner_patient_id is provided.

Responses

Response samples

Content type
application/json
{ }

Get Patient Profile

Retrieve patient profile by partner patient ID.

path Parameters
patient_id
required
string (Patient Id)

Responses

Response samples

Content type
application/json
{
  • "profile_id": "bfcb6779-b1f9-41fc-92d7-88f8bc1d12e8",
  • "partner_patient_id": "string",
  • "age": 0,
  • "sex": "string",
  • "chronic_conditions": [
    ],
  • "current_medications": [
    ],
  • "allergies": [
    ],
  • "risk_factors": [
    ],
  • "family_history": [
    ],
  • "smoking_status": "string",
  • "alcohol_status": "string",
  • "sessions_count": 0,
  • "last_session_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z"
}

Upsert Patient Profile

Create or update patient profile. Arrays are merge-appended and deduplicated.

path Parameters
patient_id
required
string (Patient Id)
Request Body schema: application/json
required
Array of Chronic Conditions (strings) or Chronic Conditions (null) (Chronic Conditions)
Array of Current Medications (strings) or Current Medications (null) (Current Medications)
Array of Allergies (strings) or Allergies (null) (Allergies)
Array of Risk Factors (strings) or Risk Factors (null) (Risk Factors)
Array of Family History (strings) or Family History (null) (Family History)
Smoking Status (string) or Smoking Status (null) (Smoking Status)
Alcohol Status (string) or Alcohol Status (null) (Alcohol Status)

Responses

Request samples

Content type
application/json
{
  • "chronic_conditions": [
    ],
  • "current_medications": [
    ],
  • "allergies": [
    ],
  • "risk_factors": [
    ],
  • "family_history": [
    ],
  • "smoking_status": "string",
  • "alcohol_status": "string"
}

Response samples

Content type
application/json
{
  • "profile_id": "bfcb6779-b1f9-41fc-92d7-88f8bc1d12e8",
  • "partner_patient_id": "string",
  • "age": 0,
  • "sex": "string",
  • "chronic_conditions": [
    ],
  • "current_medications": [
    ],
  • "allergies": [
    ],
  • "risk_factors": [
    ],
  • "family_history": [
    ],
  • "smoking_status": "string",
  • "alcohol_status": "string",
  • "sessions_count": 0,
  • "last_session_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z"
}

List Patient Sessions

List all sessions for a patient, most recent first.

Merges finalized sessions (from clinical_sessions, RLS-scoped via tenant_conn) with active sessions (from the Redis tenant index). This is the fix for T0.2: the Postgres table only ever held finalized rows, so in-progress sessions were invisible to the patient timeline.

path Parameters
patient_id
required
string (Patient Id)

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Delete Patient

GDPR full patient erasure — delete profile, sessions, anonymize usage.

path Parameters
patient_id
required
string (Patient Id)

Responses

Response samples

Content type
application/json
null

Patient Summary

Aggregated patient summary (epicrisis) from all sessions.

DX-003 consolidation (2026-04-23): previously a less-rich handler was defined first in this module and silently shadowed this one, breaking the PHI-access audit row and dropping red_flags_ever_seen. The richer response below is now the only registration. Legacy consumer fields (partner_patient_id, profile_exists, sessions_finalized, sessions_active, last_session_at) are preserved so partner integrations coded against the old shape keep working.

path Parameters
patient_id
required
string (Patient Id)

Responses

Response samples

Content type
application/json
null

Team

List Team Members

List all team members for this tenant (active and pending).

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Invite Team Member

Invite a new team member via email.

Creates an invite token in auth_tokens and sends an invite email. The invitee must accept the invite via POST /v1/auth/accept-invite to create their account with a password. Does NOT create a loginable user directly — that would leave an account with no password.

Supports Idempotency-Key header (24h TTL). Replays return the original 201 body without sending a second invite email or creating a duplicate token — important because the email-send side-effect is externally visible.

Request Body schema: application/json
required
email
required
string (Email) [ 3 .. 200 ] characters
name
string (Name) <= 100 characters
Default: ""
role
string (Role) ^(admin|developer|viewer|billing)$
Default: "developer"

Responses

Request samples

Content type
application/json
{
  • "email": "string",
  • "name": "",
  • "role": "developer"
}

Response samples

Content type
application/json
null

Change Member Role

Change a team member's role. Only owner can change roles.

Constraints:

  • Cannot change own role (future: when member auth exists)
  • Cannot change last owner to non-owner

DX-020 deprecation (2026-04-23): role is accepted via query string for backward-compat with the dashboard. REST convention is a JSON body. Query-string form will be removed on 2026-07-23 (90-day sunset per RFC 8594). Response emits Deprecation, Sunset, Link, and Warning: 299 - "Use request body instead" headers.

path Parameters
member_id
required
string <uuid> (Member Id)
query Parameters
role
string (Role)
Default: "viewer"

Responses

Response samples

Content type
application/json
{
  • "member_id": "435a4844-006a-4cfc-a644-e8eb2dd2ca43",
  • "email": "string",
  • "name": "string",
  • "role": "string",
  • "is_active": true,
  • "accepted_at": "2019-08-24T14:15:22Z",
  • "last_login_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z"
}

Remove Team Member

Remove a team member (soft delete: is_active=false).

Owner cannot be removed — must transfer ownership first.

path Parameters
member_id
required
string <uuid> (Member Id)

Responses

Response samples

Content type
application/json
{ }

Auth

Register

Register a new user + tenant (email/password).

Request Body schema: application/json
required
email
required
string (Email)
password
required
string (Password)
name
string (Name)
Default: ""
company
string (Company)
Default: ""

Responses

Request samples

Content type
application/json
{
  • "email": "string",
  • "password": "string",
  • "name": "",
  • "company": ""
}

Response samples

Content type
application/json
null

Login

Authenticate with email + password, set session cookie.

Request Body schema: application/json
required
email
required
string (Email)
password
required
string (Password)

Responses

Request samples

Content type
application/json
{
  • "email": "string",
  • "password": "string"
}

Response samples

Content type
application/json
null

Logout

Clear session + CSRF cookies and revoke the JWT.

Responses

Response samples

Content type
application/json
null

Verify Otp Endpoint

Verify a 6-digit OTP issued during register or login.

On success, mints the session JWT, sets the session + CSRF cookies and a 5-day signed nessy_trusted_device cookie so subsequent logins from the same browser skip the OTP step. Also flips dashboard_users.email_verified = true since holding the code proves email ownership.

Request Body schema: application/json
required
challenge_id
required
string (Challenge Id)
code
required
string (Code)

Responses

Request samples

Content type
application/json
{
  • "challenge_id": "string",
  • "code": "string"
}

Response samples

Content type
application/json
null

Resend Otp Endpoint

Mint a fresh code for an existing challenge and re-send the email.

Rate limit is intentionally aggressive (3/hour per IP) because each call enqueues an outbound email — a leaky resend is a spam vector. The Redis-backed challenge is mutated in place; the challenge_id stays the same so the dashboard can keep its OTP form state.

Request Body schema: application/json
required
challenge_id
required
string (Challenge Id)

Responses

Request samples

Content type
application/json
{
  • "challenge_id": "string"
}

Response samples

Content type
application/json
null

Verify Email

Verify email address using token.

Request Body schema: application/json
required
token
required
string (Token)

Responses

Request samples

Content type
application/json
{
  • "token": "string"
}

Response samples

Content type
application/json
null

Forgot Password

Send a password reset email. Always returns 200 to prevent enumeration.

Request Body schema: application/json
required
email
required
string (Email)

Responses

Request samples

Content type
application/json
{
  • "email": "string"
}

Response samples

Content type
application/json
null

Reset Password

Reset password using a valid reset token.

Request Body schema: application/json
required
token
required
string (Token)
new_password
required
string (New Password)

Responses

Request samples

Content type
application/json
{
  • "token": "string",
  • "new_password": "string"
}

Response samples

Content type
application/json
null

Invite Member

Invite a new team member (requires admin:write scope from cookie JWT).

Request Body schema: application/json
required
email
required
string (Email)
name
string (Name)
Default: ""
role
string (Role)
Default: "developer"

Responses

Request samples

Content type
application/json
{
  • "email": "string",
  • "name": "",
  • "role": "developer"
}

Response samples

Content type
application/json
null

Accept Invite

Accept an invite and create a user account.

Request Body schema: application/json
required
token
required
string (Token)
password
required
string (Password)
name
string (Name)
Default: ""

Responses

Request samples

Content type
application/json
{
  • "token": "string",
  • "password": "string",
  • "name": ""
}

Response samples

Content type
application/json
null

Get Me

Return current user info from JWT session cookie.

Responses

Response samples

Content type
application/json
null

System Status

Authenticated full-state probe for the dashboard Settings panel.

Returns engine + Postgres + Redis health alongside semver + build SHA. Public /health is intentionally slimmed in production (it leaks component state to unauthenticated callers); this endpoint is session- cookie-auth gated so the dashboard's System Status widget gets the detailed picture without re-exposing it on a public path.

Responses

Response samples

Content type
application/json
null

List Workspaces

List all workspaces (tenants) the current user belongs to.

Responses

Response samples

Content type
application/json
null

Switch Workspace

Switch to a different workspace. Sets new JWT cookie.

Responses

Response samples

Content type
application/json
null

Account

Get Me

Return the authenticated tenant's account snapshot.

No scope required. Any valid API key can call this endpoint to discover its own identity, balance, and configured scopes. Returns a compact document suitable for powering a "Hello, $tenant" UI header in partner dashboards or CLI tools.

Responses

Response samples

Content type
application/json
{ }

System

Get Changelog

Public changelog. Returns JSON by default; pass Accept: text/markdown to get the raw file. No auth required, no rate limit applied beyond the global per-IP limiter.

header Parameters
Accept (string) or Accept (null) (Accept)

Responses

Response samples

Content type
application/json
null

Get Changelog

Public changelog. Returns JSON by default; pass Accept: text/markdown to get the raw file. No auth required, no rate limit applied beyond the global per-IP limiter.

header Parameters
Accept (string) or Accept (null) (Accept)

Responses

Response samples

Content type
application/json
null

Public Status

Public, unauthenticated status JSON. See module docstring for shape.

Responses

Response samples

Content type
application/json
null

Liveness Check

Liveness probe — 200 if the process can serve a request.

Deliberately does NOT touch Postgres, Redis, or the engine. This keeps transient dependency flaps from restarting the container, which would drop in-flight sessions and cascade cold starts.

Responses

Response samples

Content type
application/json
null

Readiness Check

Readiness probe — 200 iff Postgres + Redis + engine are healthy.

Responses

Response samples

Content type
application/json
null

Health Check

Legacy alias for /readyz — kept for backward compat with the existing Azure Container Apps probes and monitoring. Will be removed once Terraform probes are flipped to /livez + /readyz and external callers are migrated.

Responses

Response samples

Content type
application/json
{
  • "status": "ok",
  • "version": "string",
  • "build": "string",
  • "engine_ready": false,
  • "postgres_connected": false,
  • "redis_connected": false
}

Status Check

Detailed component status for monitoring dashboards. Requires API key.

Responses

Response samples

Content type
application/json
null

Diagnostics

Responses

Response samples

Content type
application/json
null