Natzar Partner API Reference - v1.0.0
    Preparing search index...

    Errors & pagination

    Every non-2xx response carries a JSON body of one shape:

    {
    "error": {
    "code": "has_open_thread",
    "message": "Patient already has an open async consult",
    "details": {"asyncConsultId": "aB3xK9mQpZ4r"}
    }
    }

    Branch on code — it is machine-readable and stable. message is for logs (wording may change without notice), and details is optional structured context: validation issues for invalid_request, a conflicting resource id for some 409s. New codes may be added within /v1; treat an unknown code by its HTTP class (4xx = your request, 5xx = ours).

    The HTTP status is fully determined by the code (the server derives it from the same table, ERROR_HTTP_STATUS, so the two cannot disagree). Full per-code semantics live on ErrorCode.

    Code HTTP Meaning
    invalid_request 400 The body/query failed validation; details carries the per-field issues. Retrying unchanged will fail again.
    unauthorized 401 API key missing, malformed, revoked, wrong environment, or account suspended.
    embed_token_invalid 401 Embed only: session token failed verification (bad signature, suspended partner, invalidated by key rotation). Mint a fresh one.
    embed_token_expired 401 Embed only: session token exp passed. Mint a fresh one and call refresh(token).
    origin_not_allowed 403 Embed only: the embedding page's origin is not on your allowed-origins list.
    not_found 404 No such resource in your tenant (including ids that belong to other tenants — never a 403).
    feature_disabled 409 Your tenant does not have the product enabled (asyncEnabled / telehealthEnabled). Contact us; retrying won't help.
    phone_unavailable 409 POST /v1/patients: the phone is registered to a patient outside your tenant, or to a different patient inside it. No further detail is disclosed.
    email_in_use 409 POST /v1/physicians: a portal account with this email already exists (posted under a new externalId).
    external_id_conflict 409 POST /v1/physicians: this externalId is already registered under a different email. Repeats are idempotent only when identical; email changes are not supported over the API.
    has_open_thread 409 POST /v1/async-consults: the patient already has an open consult (details.asyncConsultId identifies it). One open thread per patient.
    thread_not_active 409 The consult isn't in an open, routable state for this operation (e.g. messaging a closed or still-invited thread).
    not_assigned 409 The acting physician is not the current assignee (assignment may have moved). Re-read; use claim/takeover.
    already_rated 409 Ratings are write-once.
    already_closed 409 The consult is already terminal; closedReason/status say how it ended.
    sla_not_overdue 409 takeover: the current assignee's response SLA hasn't elapsed yet.
    message_rejected 409 A posted message couldn't be accepted (thread closed between read and write, or empty after normalization). See the asynchronous sibling below.
    consult_not_cancellable 409 Telehealth cancel is only allowed from invited/waiting.
    rate_limited 429 Back off with jitter. The gateway may also 429 without a JSON body.
    internal_error 500 Our fault. Safe to retry idempotent requests.

    Every 409 (other than feature_disabled) is a state conflict: the consult moved between your last read and your write — the underlying store applies lifecycle transitions as conditional writes, so a stale operation fails loudly instead of double-applying. The recovery is always the same: re-read the resource, then retry only if the operation still makes sense. A retry that finds the work already done gets a descriptive 409 (already_closed, not_assigned, …), never a double-application.

    Operation Behavior on retry
    POST /v1/patients True upsert keyed on externalId — repeats update mutable fields, never duplicate. Always safe to retry.
    POST /v1/physicians Idempotent for an identical repeat (same externalId + same email → 200, names refreshed), so retries are safe. A repeat with a different email → 409 external_id_conflict; a new externalId with an in-use email → 409 email_in_use. Never a duplicate account.
    POST …/messages, POST …/replies Accepted with 202 and deduplicated inside the per-patient FIFO pipeline; a retry after a 5xx will not double-deliver.
    Lifecycle posts (claim/takeover/resolve/close/cancel) Conditional writes — a repeat gets a descriptive 409, never a second application.
    All GETs Naturally safe.

    POST /v1/async-consults/{id}/messages and …/replies return 202 Accepted: the message passed validation and was enqueued into the per-patient FIFO pipeline — it had not yet been processed when the response was sent, but it will appear on the transcript in order. The 202 body reports the messageId the message will carry on the transcript and in webhooks.

    To observe delivery:

    • Push: the async_consult.message webhook fires when the message lands on the transcript.
    • Pull: re-read GET /v1/async-consults/{id}/messages.

    One asynchronous edge: if the thread closes after your 202 but before the message processes, the message cannot be routed — you receive an async_consult.message_rejected webhook naming your messageId. An accepted patient message is never silently dropped; surface the rejection in your patient UI. (The synchronous sibling — the thread was already closed at post time — is the immediate 409 message_rejected / 409 thread_not_active instead.)

    Every list endpoint uses the same cursor contract ( Page):

    GET /v1/async-consults?status=active&limit=50
    → {"items": [...], "nextCursor": "eyJ..."}

    GET /v1/async-consults?status=active&limit=50&cursor=eyJ...
    → {"items": [...]} ← no nextCursor: last page

    Rules:

    • limit is 1–100; a server default applies when omitted.
    • nextCursor present ⇒ more results; pass it back verbatim as cursor. Absent ⇒ you've reached the end. (An empty items with no nextCursor is also a valid end.)
    • Cursors are opaque and endpoint-specific: don't parse them, don't store them long-term, and never reuse a cursor across endpoints or with different filter parameters.
    • Sort orders are fixed per endpoint: async consults by lastActivityAt descending, telehealth consults newest first, transcripts (…/messages) oldest first, events (GET /v1/events) oldest first.