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.

The API key must belong to a data tenant, otherwise session creation fails with 403.


Session Lifecycle

A typical session follows this lifecycle:

  1. Session Creation: Your backend calls POST /sessions, optionally including 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 appended as a query parameter
  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.

Request Body

  • Name
    scope
    Type
    string
    Description

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

  • 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.

  • Name
    fhirContext
    Type
    array
    Description

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

  • Name
    deployment_mode
    Type
    string
    Description

    Either "embedded" or "standalone". Defaults to "embedded".

  • 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 as a response query parameter, e.g. ?response=QuestionnaireResponse/456, so you know what was submitted. A Task can also carry its own post-submit-redirect input; the session value takes precedence over 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
    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
    expired_timestamp
    Type
    datetime
    Description

    When the session expires.

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.write",
    "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,
  "patient": { "reference": "Patient/123" },
  "encounter": null,
  "deployment_mode": "embedded",
  "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 to open a Task.

Session Handover

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

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/external/v1

Launch URL

/external/v1 is the entry point for opening the Tiro.health application from an external system. Point the next parameter of the handover at it, and it resolves the Task and sends the user to the right page for its current status.

Use this route rather than linking to an application page directly: the internal page URLs are an implementation detail and may change without notice.

Query Parameters

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.

Task Status Handling

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.

Errors

StatusMeaning
400Neither task nor task:identifier was provided, or the value is malformed
404No task matches the given task:identifier
409More than one task matches the given task:identifier
422The task cannot be opened — it has status failed, or a non-draft task has no questionnaire response yet

Launch URL

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

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

    User information including practitioner details.

  • Name
    data_tenant
    Type
    object
    Description

    Data tenant associated with this session.

  • 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

    FHIR server address and granted scopes for this session.

Get Session Request

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

Response (200 OK)

{
  "id": 12345,
  "user": {
    "id": 67890,
    "email": "doctor@hospital.org",
    "name": "Dr. Smith"
  },
  "data_tenant": {
    "id": 1,
    "name": "Hospital Name"
  },
  "active": true,
  "expired_timestamp": "2025-11-20T18:00:00Z",
  "created_timestamp": "2025-11-20T10:00:00Z",
  "last_modified_timestamp": "2025-11-20T10:00:00Z",
  "deployment_mode": "embedded",
  "fhir_server": {
    "address": "https://fhir.hospital.org/R4",
    "scope": ["patient/Patient.read", "patient/QuestionnaireResponse.write"]
  }
}

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: <context>/<resourceType>.<action>

Supported formats:

  • patient/<resourceType>.<action> - Patient-scoped access to specific resource types
  • system/<resourceType>.<action> - System-wide access to specific resource types

Examples:

  • patient/QuestionnaireResponse.write - Write access to reports (QuestionnaireResponses) in the patient's compartment
  • patient/QuestionnaireResponse.read - Read access to reports in the patient's compartment
  • patient/Patient.read - Read access to patient resources in the patient's compartment
  • patient/Encounter.read - Read access to encounters in the patient's compartment
  • system/QuestionnaireResponse.read - System-wide read access to all reports
  • patient/Patient.read patient/QuestionnaireResponse.write - Multiple scopes can be combined

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. Patient, Encounter, and Practitioner are the supporting context resources and are granted read-only.

How Scopes Work

When a session is created with scopes:

  1. The scopes are 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

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",
    "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.


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": "launch"
    },
    {
      "identifier": {
        "system": "http://hospital.org/encounter-ids",
        "value": "E-67890"
      },
      "type": "Encounter",
      "role": "launch"
    }
  ]
}

Key features:

  • identifier: Specifies the external system identifier (system + value)
  • type: Resource type (Patient or Encounter)
  • role: "launch": Indicates this is the primary launch context for the session

Identifier Resolution

When using external identifiers, Tiro.health will:

  1. Look up existing resources with matching identifiers
  2. Create new resources if no match is found
  3. Associate the session with the resolved or created resources

This enables seamless cross-system integration without requiring ID mapping or pre-registration of resources.


Migrating from /session

The singular /session endpoints are deprecated. They still work, but new integrations should use /sessions, and existing ones should move over.

DeprecatedCurrent
Hostreports.tiro.healthauth.tiro.health
CreatePOST /sessionPOST /sessions
HandoverPOST /session/$handoverGET or POST /sessions/$handover
ReadGET /session (cookie)GET /sessions/{id} (API key) or GET /sessions/current (cookie)
DeleteDELETE /session (cookie)DELETE /sessions/{id} (API key)

Authentication does not change. You keep authenticating with your API key over HTTP Basic, exactly as before.

Two things change beyond the path:

The handover token is returned in the response body. POST /sessions responds 201 with handover_token in the JSON. You no longer read it off a cookie.

Deleting a session needs its id. DELETE /sessions/{id} is called by your backend with your API key, rather than by the browser with a cookie. Keep the id from the create response.


Security Considerations

Token Expiration

  • Session tokens expire after 5 minutes and are single-use only
  • Active sessions have configurable expiration times
  • 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?