Errors
When something goes wrong while you work with the Atticus FHIR API, the response tells you what happened in two ways: the HTTP status code and a machine-readable FHIR OperationOutcome resource in the body. Together they let you detect, classify, and debug a failure programmatically before reaching out to support.
You can tell whether a request succeeded from the HTTP status code. When a request fails, the OperationOutcome body carries one or more issues — each with a severity, a machine-readable code, and a human-readable details.text you can surface to your users or log.
Surface details.text to your users (or at least your logs) rather than a
generic "service unavailable". The API returns actionable messages — for
example, a misconfigured template version produces a 400 that explains the
exact URL and version at fault. Hiding it behind a generic error makes
incidents far harder to diagnose.
Status codes
The API uses conventional HTTP status codes to indicate the outcome of a request.
- Name
2xx- Description
Success. The request was accepted and processed.
- Name
4xx- Description
A client error — the request was rejected because of something in the input (a malformed body, a missing resource, insufficient permissions, or a configuration problem). The
OperationOutcomebody explains what to fix.
- Name
5xx- Description
A server error on our side. These are reported to our monitoring automatically; retry with backoff and contact support if they persist.
The OperationOutcome resource
Every error response on the FHIR API (/fhir/r5) returns a FHIR OperationOutcome resource. Its issue array always contains at least one entry.
- Name
resourceType*- Type
- 'OperationOutcome'
- Description
Always
'OperationOutcome'.
- Name
issue*- Type
- Issue[]
- Description
One or more issues describing what went wrong. Each issue has the fields below.
- Name
issue[].severity*- Type
- code
- Description
One of
fatal,error,warning,information. Errors that produce a 4xx/5xx areerror(orfatalfor aborted transactions).
- Name
issue[].code*- Type
- code
- Description
A machine-readable classification of the issue (e.g.
invalid,not-found,forbidden). See Issue codes. Branch on this — not ondetails.text.
- Name
issue[].details- Type
- CodeableConcept
- Description
Carries the human-readable message in
details.text. Error responses do not populate a codeddetails.coding— useissue.code(above) for any machine logic; treatdetails.textas a display/log string only.
- Name
issue[].diagnostics- Type
- string
- Description
Optional additional debugging information.
- Name
issue[].expression- Type
- string[]
- Description
Optional FHIRPath expression(s) pointing at the element(s) at fault — present on validation (
422) errors.
Example error response (400)
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "invalid",
"details": {
"text": "The specified Questionnaire with URL http://templates.tiro.health/templates/0123456789abcdef0123456789abcdef and version 1.1.0 is not an active template, or doesn't exist. Please contact your support team to check configuration."
}
}
]
}
Issue codes
The issue[].code value classifies the failure and maps to the HTTP status below. Always branch on code rather than parsing details.text.
code | HTTP status | Meaning |
|---|---|---|
invalid | 400 Bad Request | The request is syntactically valid but references something that cannot be used — e.g. a questionnaire canonical that does not resolve to an active template. |
multiple-matches | 400 Bad Request | A search expected to match a single resource matched several. Narrow your search parameters. |
not-supported | 400 Bad Request | The requested operation or combination of parameters is not supported. |
forbidden | 403 Forbidden | You are authenticated but not allowed to access this resource (includes row-level access denials). |
not-found | 404 Not Found | The referenced resource does not exist or is not visible to you. |
conflict | 409 Conflict | The request conflicts with the current state of the resource. |
duplicate | 409 Conflict | A resource with the same unique identity already exists. The Location header points at the existing resource. |
conflict | 412 Precondition Failed | A conditional request failed because the resource changed since you last read it (If-Match/ETag). |
required / business-rule | 422 Unprocessable Content | The resource failed validation. expression points at the offending element(s); for questionnaire responses this is a FHIRPath into the submitted resource. |
not-supported | 501 Not Implemented | The operation is recognised but not yet implemented. |
exception | 500 Internal Server Error | An unexpected error on our side. Reported to monitoring automatically. |
Common errors
Questionnaire (template) is not active or doesn't exist
When you create or $initialize a Complete Questionnaire Task, you reference the template through a questionnaire canonical — the template URL, optionally pinned to a version with a |<version> suffix (for example …/0123456789abcdef0123456789abcdef|1.1.0).
If that exact version is no longer active — because it was retired or superseded by a newer publication — the canonical resolves to nothing and the API returns a 400 with code: invalid and a details.text naming the URL and version at fault (see the example above).
Why it happens. A version-pinned canonical only resolves while that precise version is active. Publishing a new version of the template retires the old one, so any integration still configured with the old pinned version starts failing — typically surfacing to the end user as "the application can't be opened".
How to fix it. Configure a version-independent canonical by dropping the |<version> suffix; it always resolves to the latest active version of the template:
- Avoid — version-pinned:
http://templates.tiro.health/templates/0123456789abcdef0123456789abcdef|1.1.0
Brittle: breaks as soon as1.1.0is retired or superseded. - Prefer — version-independent:
http://templates.tiro.health/templates/0123456789abcdef0123456789abcdef
Robust: always resolves to the latest active version.
Pin a version only when you deliberately need a fixed historical template and you keep that version published.
Validation failures (422)
When a submitted resource (for example a QuestionnaireResponse) fails validation, the API returns 422 Unprocessable Content. Each issue carries an expression with the FHIRPath to the offending element, so you can map the error back to the exact field:
Example validation error (422)
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "required",
"details": { "text": "Field required" },
"expression": ["QuestionnaireResponse.item[0].answer[0].valueCoding.system"]
}
]
}
Resource not found (404)
A 404 with code: not-found means the resource either does not exist or is not visible to your account. Because access is scoped per data tenant, a resource that exists for one tenant returns 404 (not 403) for another — confirm you are operating in the correct data tenant.
Handling errors in code
Handle errors in three layers, from coarse to fine. Each layer refines the previous one, so your handling keeps working even when the finer signal is absent:
- HTTP status — coarse control flow: authentication, retry,
4xxvs5xx. issue.code— the standard FHIR issue type; always present. This is your primary semantic branch.issue.details.coding— when a coding from the Tiro detail code system (http://fhir.tiro.health/CodeSystem/operation-outcome-issue-detail) is present, branch on itscodefor a precise condition. Treat it as a refinement: most errors expose onlyissue.code+details.text, so never depend on a coding being present.
The examples below use the Firely .NET SDK (Hl7.Fhir.R5). FhirClient raises a FhirOperationException that carries the parsed OperationOutcome (ex.Outcome) and the HTTP status (ex.Status). The Tiro detail codes are modelled as a strongly-typed enum decorated with [EnumLiteral] and parsed with EnumUtility.ParseLiteral<T>, which returns null for unknown/future codes — keeping the (open) binding forward-compatible.
Handle an OperationOutcome error
using System;
using System.Linq;
using Hl7.Fhir.Introspection; // FhirEnumeration
using Hl7.Fhir.Model;
using Hl7.Fhir.Rest;
using Hl7.Fhir.Utility; // EnumLiteral, EnumUtility
public static class TiroSystems
{
public const string IssueDetail = "http://fhir.tiro.health/CodeSystem/operation-outcome-issue-detail";
}
// Strongly-typed view of the Tiro detail code system. The binding is open, so
// EnumUtility.ParseLiteral returns null for any code not listed here (forward-compatible).
[FhirEnumeration("TiroIssueDetail")]
public enum TiroIssueDetail
{
[EnumLiteral("TEMPLATE_NOT_ACTIVE", TiroSystems.IssueDetail)] TemplateNotActive,
[EnumLiteral("TEMPLATE_EXPERIMENTAL_VERSION_REQUIRED", TiroSystems.IssueDetail)] ExperimentalVersionRequired,
[EnumLiteral("INITIAL_RESPONSE_PATIENT_MISMATCH", TiroSystems.IssueDetail)] InitialResponsePatientMismatch,
[EnumLiteral("INITIAL_RESPONSE_CANONICAL_MISMATCH", TiroSystems.IssueDetail)] InitialResponseCanonicalMismatch,
}
// First recognized Tiro detail code, or null when none is present / the code is unknown.
// Parse the raw Coding.Code string — never wrap an unmapped code in Code<T> (its .Value throws).
static TiroIssueDetail? GetTiroDetail(OperationOutcome? outcome) =>
outcome?.Issue
.SelectMany(i => i.Details?.Coding ?? Enumerable.Empty<Coding>())
.Where(c => c.System == TiroSystems.IssueDetail)
.Select(c => EnumUtility.ParseLiteral<TiroIssueDetail>(c.Code))
.FirstOrDefault(v => v is not null);
try
{
await client.TypeOperationAsync("$initialize", "Task", input);
}
catch (FhirOperationException ex)
{
// 1. HTTP status — retry/escalate on server errors.
if ((int)ex.Status >= 500) throw;
var outcome = ex.Outcome; // parsed OperationOutcome (null for non-FHIR bodies)
var issue = outcome?.Issue.FirstOrDefault();
var message = issue?.Details?.Text ?? ex.Message; // safe to display / log
// 2. Tenant detail code — precise and stable (populated as codes roll out).
switch (GetTiroDetail(outcome))
{
case TiroIssueDetail.TemplateNotActive:
// The pinned template version is retired — prompt to fix the canonical.
return;
// null / unknown code: fall through to issue.code below.
}
// 3. Standard FHIR issue type — always present; your primary branch.
switch (issue?.Code)
{
case OperationOutcome.IssueType.Forbidden: /* access denied */ break;
case OperationOutcome.IssueType.NotFound: /* unknown id or wrong data tenant */ break;
case OperationOutcome.IssueType.Conflict:
case OperationOutcome.IssueType.Duplicate: /* state conflict */ break;
default: /* show `message` */ break;
}
}
Branch on issue.code (and the coded issue.details.coding when present) —
never on details.text. Message wording can change; treat details.text as a
display/log string only.