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.
If you are currently calling the singular /session endpoints on reports.tiro.health, see Migrating from /session. Those endpoints are deprecated.
Session Lifecycle
A typical session follows this lifecycle:
- Session Creation: Your backend calls
POST /sessions, optionally including scopes, FHIR context (Patient, Encounter) and apost_submit_redirect. The session is created inactive and comes back with a single-usehandover_token - Browser Handover: The end-user's browser is sent to
/sessions/$handoverwith that token and anextLaunch URL. The session is activated, a secure cookie is set in the browser, and the user lands on the task - Completing the Report: The user fills in and submits the report, with access controlled by the session's scopes
- Return to Your System: On submit, Tiro.health sends the browser back to your
post_submit_redirect, with the resultingQuestionnaireResponseappended as a query parameter - 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.
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
responsequery parameter, e.g.?response=QuestionnaireResponse/456, so you know what was submitted. A Task can also carry its ownpost-submit-redirectinput; 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
falseon 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.
There is no equivalent redirect when the user abandons the report — only submitting sends the user back. Delete the session when you are done with it, rather than relying on a return trip.
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"
}
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_tokenfrom 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.
A session can only be handed over once, while it is still inactive. Replaying a consumed or expired token returns 401.
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
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 onTask.identifierwhen creating the task. The identifier must match exactly one task.
Task Status Handling
The task's status determines the destination:
| Task status | Destination |
|---|---|
draft | Form selection, so the user can pick a questionnaire for the task |
ready, in-progress, completed | The report editor, opened on the task's questionnaire response |
failed | Error — the task cannot be opened |
Patient and encounter context is read from the task itself, so you do not need to pass them.
Errors
| Status | Meaning |
|---|---|
400 | Neither task nor task:identifier was provided, or the value is malformed |
404 | No task matches the given task:identifier |
409 | More than one task matches the given task:identifier |
422 | The 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
When launching by task:identifier, remember to URL-encode the value: the
| separator becomes %7C, and the : in the system URL becomes %3A.
Get Session Information
Retrieves a session. There are two ways in:
GET /sessions/{id}— from your backend, authenticated with your API key. Returns404if 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 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 typessystem/<resourceType>.<action>- System-wide access to specific resource types
Examples:
patient/QuestionnaireResponse.write- Write access to reports (QuestionnaireResponses) in the patient's compartmentpatient/QuestionnaireResponse.read- Read access to reports in the patient's compartmentpatient/Patient.read- Read access to patient resources in the patient's compartmentpatient/Encounter.read- Read access to encounters in the patient's compartmentsystem/QuestionnaireResponse.read- System-wide read access to all reportspatient/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:
- The scopes are stored with the session
- All FHIR API requests during the session are checked against these scopes
- Requests for resources or actions not covered by the scopes are denied
- 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:
- Look up existing resources with matching identifiers
- Create new resources if no match is found
- 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.
| Deprecated | Current | |
|---|---|---|
| Host | reports.tiro.health | auth.tiro.health |
| Create | POST /session | POST /sessions |
| Handover | POST /session/$handover | GET or POST /sessions/$handover |
| Read | GET /session (cookie) | GET /sessions/{id} (API key) or GET /sessions/current (cookie) |
| Delete | DELETE /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 JavaScriptSecure- Only transmitted over HTTPSSameSite=Strict- Protection against CSRF attacks- Scoped to
tiro.healthdomain
Best Practices
- Minimize scope - Only grant the minimum permissions needed
- Short-lived tokens - Use the default 5-minute token expiration
- Secure token transmission - Pass tokens via secure form POST, not URL parameters
- Immediate handover - Complete the handover flow as quickly as possible
- Explicit logout - Always delete sessions when users log out
- Monitor sessions - Track active sessions and their expiration times