Session Management API

The Session Management API allows Capture API customers to programmatically create and manage user sessions with fine-grained access control through scopes.


Overview

The Session Management API is designed for EHR and LIS systems that integrate with Tiro.health using the Capture API. It enables you to:

  • Create sessions programmatically with specific scopes and FHIR context
  • Control access to resources throughout the session lifetime using scope-based permissions
  • Handover sessions to end-users in a browser environment
  • Terminate sessions when the user logs out or the session expires

This API is particularly useful when you need to:

  • Launch Tiro.health forms with pre-configured patient and encounter context
  • Restrict what data users can access during a session
  • Integrate Tiro.health into your application's authentication flow
  • Provide seamless user experience with session continuity

Base URL

The Session Management API is served by the Tiro.health Auth Service:

https://auth.tiro.health

Authentication

POST /sessions, GET /sessions/{id} and DELETE /sessions/{id} authenticate with your API key over HTTP Basic — the base64 encoding of client_id:client_secret. The two handover endpoints are unauthenticated by design: they are opened by the end-user's browser and carry a single-use token instead.


Session Lifecycle

A typical session follows this lifecycle:

  1. Session Creation: Your backend calls POST /sessions with the session user (the clinician the session acts as), plus scopes, FHIR context (Patient, Encounter) and a post_submit_redirect. The session is created inactive and comes back with a single-use handover_token
  2. Browser Handover: The end-user's browser is sent to /sessions/$handover with that token and a next Launch URL. The session is activated, a secure cookie is set in the browser, and the user lands on the task
  3. Completing the Report: The user fills in and submits the report, with access controlled by the session's scopes
  4. Return to Your System: On submit, Tiro.health sends the browser back to your post_submit_redirect, with the resulting QuestionnaireResponse and the Task appended as query parameters
  5. Session Termination: Your backend deletes the session via DELETE /sessions/{id} (equivalent to logout), or it expires automatically

The session only becomes usable at step 2 — the handover is what activates it and binds it to the end-user's browser. Your backend never holds the session cookie.

Mermaid diagram
POST/sessions

Create a Session

Creates a session for an end-user, optionally with FHIR context and scope restrictions. The session is created inactive: it does nothing until the handover activates it in the user's browser.

The session user is required: identify who the session acts as via the user field or a fhirContext entry with type: "Practitioner". Requests without a resolvable user fail with 422.

Request Body

  • Name
    scope
    Type
    string
    Description

    Space-separated list of requested scopes (e.g., "patient/Patient.read patient/QuestionnaireResponse.*"). These scopes control what data the user can access during the session. A list of strings is also accepted. See Scope-based Access Control.

  • Name
    patient
    Type
    integer
    Description

    FHIR server ID of the patient to associate with this session.

  • Name
    encounter
    Type
    integer
    Description

    FHIR server ID of the encounter to associate with this session.

  • Name
    user
    Type
    integer
    Description

    FHIR server ID of the user the session belongs to. Alternatively, identify the user with a fhirContext entry of type Practitioner — one of the two is required. When both are provided, the user field wins.

  • Name
    fhirContext
    Type
    array
    Description

    Array of FHIR context items using external identifiers to specify the Patient, Encounter, and Practitioner. Use this when integrating with systems that use different identifier namespaces. See FHIR Context for details.

  • Name
    deployment_mode
    Type
    string
    Description

    How Tiro.health renders for this session. "embedded" (the default) strips the surrounding application chrome — navigation, top bar — for use inside an iframe, WebView, or embedded browser of your system. "standalone" keeps the full application UI, for users working in a regular browser tab. This sets the session default; the Launch URL's viewMode parameter can override it per launch.

  • Name
    post_submit_redirect
    Type
    string
    Description

    URL in your own system to send the user back to once they submit the report. Tiro.health appends the resulting response and the task as query parameters, e.g. ?response=QuestionnaireResponse/456&task=Task/123, so you know what was submitted. A Task can also carry its own post-submit-redirect input; the session value takes precedence over it.

  • Name
    post_cancel_redirect
    Type
    string
    Description

    URL in your own system to send the user back to when they cancel out of the report instead of submitting it.

Response Fields

  • Name
    id
    Type
    integer
    Description

    Identifier of the session. Pass it to DELETE /sessions/{id} to end the session.

  • Name
    handover_token
    Type
    string
    Description

    Single-use token that activates the session. Hand it to the end-user's browser via the handover endpoint. It expires after 5 minutes.

  • Name
    active
    Type
    boolean
    Description

    false on creation. The session only becomes active once the handover completes.

  • Name
    user
    Type
    object
    Description

    Reference to the Practitioner the session acts as, e.g. {"type": "Practitioner", "reference": "Practitioner/456"}.

  • Name
    patient
    Type
    object
    Description

    Patient launch context resolved for this session, if any.

  • Name
    encounter
    Type
    object
    Description

    Encounter launch context resolved for this session, if any.

  • Name
    data_tenant
    Type
    object
    Description

    The data tenant this session belongs to.

  • Name
    deployment_mode
    Type
    string
    Description

    The deployment mode the session was created with.

  • Name
    fhir_server
    Type
    object
    Description

    The effective scope list granted to the session, after downscoping. The address field is null for API-created sessions.

  • Name
    expired_timestamp
    Type
    datetime
    Description

    When the session expires (7.5 days after creation).

Create Session Request

curl -X POST https://auth.tiro.health/sessions \
  -u "<client_id>:<client_secret>" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "patient/Patient.read patient/QuestionnaireResponse.* patient/Questionnaire.read",
    "user": 456,
    "patient": 123,
    "deployment_mode": "embedded",
    "post_submit_redirect": "https://ehr.example.com/reports/done"
  }'

Response (201 Created)

{
  "id": 789,
  "handover_token": "hDk3n...T9wQ",
  "active": false,
  "user": { "type": "Practitioner", "reference": "Practitioner/456" },
  "patient": { "type": "Patient", "reference": "Patient/123" },
  "encounter": null,
  "deployment_mode": "embedded",
  "fhir_server": {
    "address": null,
    "scope": [
      "patient/Patient.rs",
      "patient/QuestionnaireResponse.cruds",
      "patient/Questionnaire.rs"
    ]
  },
  "post_submit_redirect": "https://ehr.example.com/reports/done",
  "expired_timestamp": "2026-07-14T18:00:00Z"
}

GET/sessions/$handover

Session Handover

The handover endpoint activates the session: it consumes the single-use handover_token, sets the authentication cookie in the end-user's browser, and redirects to next. It must be reached by the end-user's browser — this is the step that binds the session to that browser. Calling it from your backend would set the cookie on your server, not on the user.

The endpoint accepts the same two parameters either way:

  • GET /sessions/$handover?token=…&next=… — query parameters. Simplest: just send the browser to the URL.
  • POST /sessions/$handover — form-encoded body. Use when you prefer a form submission, or already have one.

Both return 303 See Other with the session cookie set.

Parameters

  • Name
    token
    Type
    string
    Description

    The handover_token from the session creation step. Single-use, and expires after 5 minutes.

  • Name
    next
    Type
    string
    Description

    Where to send the user after activation. Use the Launch URL below to open a Task.

The next Launch URL

Point next at https://app.tiro.health/external/v1 — the entry point for opening the Tiro.health application from an external system. Note the host: this is a page on the application (app.tiro.health), not an endpoint on the auth server. It resolves the Task and sends the user to the right page for its current status. Use it rather than linking to an application page directly: the internal page URLs are an implementation detail and may change without notice.

Provide either task or task:identifier:

  • Name
    task
    Type
    string
    Description

    FHIR reference to the task to open, e.g. Task/123. Use this when you store the Tiro.health task id.

  • Name
    task:identifier
    Type
    string
    Description

    External identifier of the task, in {system}|{value} format. Use this when you only keep your own identifier — the same one you set on Task.identifier when creating the task. The identifier must match exactly one task. Remember to URL-encode the value: the | separator becomes %7C, and the : in the system URL becomes %3A.

  • Name
    viewMode
    Type
    string
    Description

    Optional. embedded renders the report without the surrounding application chrome (navigation, top bar) — use this inside iframes and embedded browsers. Defaults to the session's deployment_mode.

  • Name
    readonly
    Type
    string
    Description

    Optional. readonly=yes opens the report read-only. The value must be the literal yes. Only applies when the task opens in the report editor (non-draft statuses).

The task's status determines the destination:

Task statusDestination
draftForm selection, so the user can pick a questionnaire for the task
ready, in-progress, completedThe report editor, opened on the task's questionnaire response
failedError — the task cannot be opened

Patient and encounter context is read from the task itself, so you do not need to pass them.

The launch page itself can fail: 400 when neither task nor task:identifier is provided (or the value is malformed), 404 when no task matches the identifier, 409 when more than one matches, and 422 when the task cannot be opened (status failed, or a non-draft task without a questionnaire response yet).

Session Handover

https://auth.tiro.health/sessions/$handover
  ?token=<handover_token>
  &next=https%3A%2F%2Fapp.tiro.health%2Fexternal%2Fv1%3Ftask%3DTask%2F123

Launch URLs for next

https://app.tiro.health/external/v1?task=Task/123

Response (303 See Other)

// Redirects to the 'next' URL with session cookie set
Location: https://app.tiro.health/external/v1?task=Task/123
Set-Cookie: auth_session=<session_id>; HttpOnly; Secure; SameSite=Strict

GET/sessions/{id}

Get Session Information

Retrieves a session. There are two ways in:

  • GET /sessions/{id} — from your backend, authenticated with your API key. Returns 404 if the session belongs to another data tenant.
  • GET /sessions/current — from the end-user's browser, authenticated with the session cookie. Useful for a client to check its own session after handover.

Response Fields

  • Name
    id
    Type
    integer
    Description

    Unique identifier for the session.

  • Name
    user
    Type
    object
    Description

    Reference to the Practitioner the session acts as, e.g. {"type": "Practitioner", "reference": "Practitioner/456"}.

  • Name
    data_tenant
    Type
    object
    Description

    Data tenant associated with this session.

  • Name
    patient
    Type
    object
    Description

    Reference to the Patient launch context, or null.

  • Name
    encounter
    Type
    object
    Description

    Reference to the Encounter launch context, or null.

  • Name
    active
    Type
    boolean
    Description

    Whether the session is currently active.

  • Name
    expired_timestamp
    Type
    datetime
    Description

    When the session expires.

  • Name
    created_timestamp
    Type
    datetime
    Description

    When the session was created.

  • Name
    last_modified_timestamp
    Type
    datetime
    Description

    When the session was last modified.

  • Name
    deployment_mode
    Type
    string
    Description

    Either "embedded" or "standalone".

  • Name
    fhir_server
    Type
    object
    Description

    The effective scope list granted to this session. The address field is null for API-created sessions; the FHIR API base URL is https://reports.tiro.health/fhir/r5.

Get Session Request

curl https://auth.tiro.health/sessions/789 \
  -u "<client_id>:<client_secret>"

Response (200 OK)

{
  "id": 789,
  "user": { "type": "Practitioner", "reference": "Practitioner/456" },
  "data_tenant": {
    "id": 1,
    "name": "Hospital Name"
  },
  "patient": { "type": "Patient", "reference": "Patient/123" },
  "encounter": null,
  "active": true,
  "expired_timestamp": "2026-07-14T18:00:00Z",
  "created_timestamp": "2026-07-07T10:00:00Z",
  "last_modified_timestamp": "2026-07-07T10:00:00Z",
  "deployment_mode": "embedded",
  "fhir_server": {
    "address": null,
    "scope": [
      "patient/Patient.rs",
      "patient/QuestionnaireResponse.cruds",
      "patient/Questionnaire.rs"
    ]
  }
}

DELETE/sessions/{id}

Delete a Session

Terminates a session by marking it inactive. This is equivalent to logging the user out: the session cookie in their browser stops working immediately.

Call this from your backend with your API key, using the id returned when you created the session. Deleting a session that belongs to another data tenant returns 404.

Delete Session Request

curl -X DELETE https://auth.tiro.health/sessions/789 \
  -u "<client_id>:<client_secret>"

Response (204 No Content)

// Empty response body

Scope-based Access Control

Scopes define what actions and resources a user can access during their session. When you create a session with specific scopes, those restrictions are enforced throughout the entire session lifetime.

Scope Format

Scopes follow the FHIR SMART on FHIR format, with an optional filter suffix: <context>/<resourceType>.<action>[?filters]

Filters are currently honored for Questionnaire scopes only, where they narrow which report templates the session can access — see Template Access and Filtering below.

Contexts:

  • patient/<resourceType>.<action> — access limited to the session's launched patient. Only meaningful when the session is created with a patient (or a Patient fhirContext item); without one, patient/ scopes grant nothing.
  • user/<resourceType>.<action> — access to resources the session user owns or authored. Use this when the session has no patient context.
  • system/<resourceType>.<action> — tenant-wide access. Only kept for sessions in EHR-managed tenants; in regular tenants it is downgraded to user/ at session creation.

Actions: read, write, or *. Note that write does not include read — a session that saves reports must also be able to read them back, so pair .read and .write, or use .*.

Recommended scopes for a patient-launched session:

patient/Patient.read patient/Encounter.read patient/QuestionnaireResponse.* patient/Questionnaire.read
  • patient/QuestionnaireResponse.* — create, read, and update the patient's reports
  • patient/Patient.read / patient/Encounter.read — read the launch context
  • patient/Questionnaire.read — read the report templates the form needs

Reports authored in Tiro.health are stored as FHIR QuestionnaireResponse resources, so QuestionnaireResponse is the resource type you scope to control read and write access to captured data.

SMART v2 permission syntax is also accepted: any combination of c (create), r (read), u (update), d (delete), s (search), e.g. patient/QuestionnaireResponse.cruds. The v1 forms are converted automatically (.read.rs, .write.cud, .*.cruds), and the session's stored scopes (fhir_server.scope) are always in v2 form.

How Scopes Work

When a session is created with scopes:

  1. The requested scopes are downscoped to what your data tenant allows (see below), then stored with the session
  2. All FHIR API requests during the session are checked against these scopes
  3. Requests for resources or actions not covered by the scopes are denied
  4. Scopes remain active until the session is deleted or expires

The effective scopes are returned in the create-session response under fhir_server.scope — check that field if a session gets unexpected 403s.

EHR-managed Data Tenants

In data tenants managed by an EHR integration, sessions are automatically downscoped at creation:

  • Patient, Encounter, Practitioner, and DocumentReference are capped to read-only by ID: no create, update, delete, or search — regardless of what was requested. End-users open these resources from the launch context; they never edit them in Tiro.health, since the EHR owns that data.
  • A * wildcard is expanded to the EHR-relevant set: full QuestionnaireResponse access, read+search Questionnaire, and read-only context resources.
  • Other resource scopes (e.g. QuestionnaireResponse) pass through with the requested permissions.

This downscoping applies to user sessions only. Machine-to-machine calls with your API key are governed by the key's own scopes, so background data imports keep working in EHR-managed tenants. Context resources can also be created at session creation via fhirContext role/new.

Example: Restricted Session

# Create a read-only session for patient data
curl -X POST https://auth.tiro.health/sessions \
  -u "<client_id>:<client_secret>" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "patient/Patient.read patient/Encounter.read patient/QuestionnaireResponse.read",
    "user": 456,
    "patient": 123
  }'

In this session, the user can read patient, encounter, and report data but cannot modify anything. Any attempt to create or update resources will be denied.

Template Access and Filtering

The Questionnaire scope controls which report templates the session user can pick. Template access is delegated from your API key: at session creation the scope is automatically constrained to the projects your integration's service account belongs to, so the session user needs no project membership of their own — including users created on the fly via fhirContext role/new.

You can narrow the delegation with filters on the scope (a Tiro.health extension to SMART on FHIR scopes):

patient/Questionnaire.read?project=53
patient/Questionnaire.read?url=https://templates.example.org/ct-scan
patient/Questionnaire.read?specialty=urology
  • project — one of your service account's projects; other projects are ignored
  • url — a single template by canonical URL, without a |version suffix
  • identifier, version, specialty — further narrowing, combined with the project filter

Without filters, the user can choose from all templates in your service account's projects. If your account belongs to no projects, the session gets no template access — contact Tiro.health to link your API key to the right project.


FHIR Context

Sessions can include FHIR context to associate them with specific patients and encounters. There are two ways to specify context:

Using Direct IDs

When you have the internal FHIR server IDs, use the direct patient and encounter parameters:

{
  "patient": 123,
  "encounter": 456
}

This is the simplest approach when your system already uses Tiro.health's FHIR server IDs.

Using External Identifiers with fhirContext

When integrating with external systems that use their own identifier namespaces, use the fhirContext array following the SMART on FHIR launch context specification:

{
  "fhirContext": [
    {
      "identifier": {
        "system": "http://hospital.org/patient-ids",
        "value": "P-12345"
      },
      "type": "Patient",
      "role": "https://tiro.health/fhir/role/new"
    },
    {
      "identifier": {
        "system": "http://hospital.org/encounter-ids",
        "value": "E-67890"
      },
      "type": "Encounter",
      "role": "https://tiro.health/fhir/role/new"
    },
    {
      "identifier": {
        "system": "http://hospital.org/user-ids",
        "value": "dr-smith"
      },
      "type": "Practitioner",
      "role": "https://tiro.health/fhir/role/new"
    }
  ]
}

Key features:

  • identifier: Specifies the external system identifier (system + value)
  • type: Resource type — Patient, Encounter, or Practitioner
  • role: Controls how the identifier is resolved (see below)

Identifier Resolution

The role of each context item determines what happens when no resource matches the identifier:

  • https://tiro.health/fhir/role/new — create if missing. When no resource matches, a new one is created from the identifier. This is how patient, encounter, and user records are materialised in your data tenant: you do not need to import them through the FHIR API first.
  • https://tiro.health/fhir/role/find — look up only. When no resource matches, session creation fails with 404 (e.g. "No patient found with identifier …").

If role is omitted it defaults to "launch", which behaves like find — it does not create missing resources.


Security Considerations

Token Expiration

  • Session tokens expire after 5 minutes and are single-use only
  • Sessions expire 7.5 days after creation
  • Expired tokens cannot be used for handover and will return a 401 error

Token Security

  • Session tokens should be treated as sensitive credentials
  • Tokens are deleted immediately after successful handover (one-time use)
  • Always use HTTPS when transmitting tokens
  • Never log or store session tokens

Cookie Security

Session cookies are configured with security best practices:

  • HttpOnly - Cannot be accessed via JavaScript
  • Secure - Only transmitted over HTTPS
  • SameSite=Strict - Protection against CSRF attacks
  • Scoped to tiro.health domain

Best Practices

  1. Minimize scope - Only grant the minimum permissions needed
  2. Short-lived tokens - Use the default 5-minute token expiration
  3. Secure token transmission - Pass tokens via secure form POST, not URL parameters
  4. Immediate handover - Complete the handover flow as quickly as possible
  5. Explicit logout - Always delete sessions when users log out
  6. Monitor sessions - Track active sessions and their expiration times

Was this page helpful?