Introduction
Welcome to the Open HealthHub Open API Hub! You can use our APIs to access the Open HealthHub Open API endpoints based on HL7 FHIR, which can interact with the Open HealthHub and SMART on FHIR applications for questionnaires, information and remote patient monitoring.
Open API overview
Welcome to the Open HealthHub! You can use our APIs to access the Open HealthHub Open API endpoints based on HL7 FHIR, which can interact with the Open HealthHub and SMART on FHIR applications for questionnaires, information and remote patient monitoring. This way you can integrate our solution for digital forms and remote patient monitoring in your own platform, portal or application. This saves both healthcare professionals and patients a lot of time. You can also become an Open HealthHub partner and get access to our technical expertise, training program and more to support your integration.
Client libraries
We have language bindings in Shell, .NET, Java, JavaScript, PHP, Python and Ruby! You can view code examples in the dark area to the right, and you can switch the programming language of the examples with the tabs in the top right.
For complete implementation please have a look at our FHIR Client Example Repository
Clients | |||||||
---|---|---|---|---|---|---|---|
Shell | Ruby | Python | PHP | JS | Java | C# | Go |
Client types
We support 2 different type of clients to access our FHIR APIs:
- Data centered client: This client can be used to invite patients to Improve programs and retrieve (encrypted) patient responses.
- Person centered client: This client can be used to display the available programs for a user, show the questionnaires to the user and capture the patient responses.
Data centered clients will need to upload a public key, so the patient data can be encrypted and retrieved by this client.
In the following sections we'll show the following icons for the different types of client that could use that API:
- Data centered:
- Patient centered:
Data Centered Client
Data centered clients are mainly used to invite patients for a program created in the Improve Designer and to retrieve the (encrypted) responses to the questionnaires.
After setting-up accounts and clients the standard flow for a data centered clients is:
The data centered client starts with inviting a patient by creating a CarePlan. The response of this request will contain the code the patient needs to register for this program (for Enterprise customers this can be automated, see CarePlan creation for more information). Once the patient is registered for this program he can fill out the questionnaires in the Improve App or in another app that acts as Person centered client.
The next step is retrieving the QuestionnaireResponse for a patient. This can be done by polling for new responses, but the preferred way is using Subscriptions to get notified about new responses.
Once the responses are retrieved they need to be decrypted to read the actual answers given by the patient.
Person Centered Client
Person centered clients are used to present the programs and questionnaires to the patient and capture the responses of the patient.
After setting-up accounts and clients the standard flow for a person centered clients is:
The person centered client might start with retrieving all CarePlans for a patient. The CarePlan contains all tasks the patient should complete for this specific program. For every task there is a Questionnaire that the patient should fill out.
After the patient has filled out the Questionnaire the person centered client should have a complete QuestionnaireResponse with all the given answers. To provide this response to the Open HealthHub APIs the response should be encrypted. For encrypting the client will need the public keys of the participants in the program. These can be retrieved as part of the CarePlan, as part of a CareTeam, or as a list of Practitioners.
When the QuestionnaireResponse is encrypted the client needs to send a Bundle of QuestionnaireResponses (one response per public key/participant), to the Open HealthHub FHIR API. This will fulfill the task of the CarePlan, optionally triggering new tasks.
Get started
Welcome to the Open HealthHub Open API Hub! You can use our API's to access the Open HealthHub Open API endpoints based on FHIR, which can interact with the Open HealthHub hub for questionnaires, information and remote patient monitoring.
Step 1. Play and learn
Get familiar with our FHIR REST APIs, FHIR Client Example Repository and our example code here on this website. You can play with our sandbox with the listed credentials.
Learn more about FHIR via the following links:
Our Postman Collection offers a great start at discovering our FHIR REST APIs (sandbox environment). It is pre-configured with the sandbox credentials (api-key, openid credentials) so you can start right away:
- click on the collection "Open HealthHub FHIR APIs" on the left.
- click on the tab "Authorization"
- click on the button "Get New Access Token" at the bottom of the page
Now you can start making requests and view the results in the console.
Step 2. Get Credentials
To generate FHIR API credentials for the live environment:
- Request for an Open HealthHub API developer account.
- We will send your credentials.
- You can start using your credentials.
- Replace the sandbox credentials with your credentials.
- Replace the sandbox URL with the live URL.
For more information about authentication see our documentation
Step 3. Upload your public key
If you want to create a data centered client, you'll need to upload your public key.
- Download and install the latest version of GPG command line tools for your operating system.
- Generate a GPG key pair.
- in your terminal/command prompt enter
gpg --expert --full-generate-key
- choose
ECC and ECC
for the kind of key,Curve 25519
for the kind of curve, choose default 0 (does not expire) for keys validity, choose a user ID to match your usecase, when choosing a passphrase remember that it will need to be provided every time the private key is used. - use
gpg --list-secret-keys --keyid-format=long
to view all keys - for the key you want to use, find the line that looks like
sec ed25519/9EB63F8FEEC4DEB8 2021-09-23 [SC]
take the key ID, in this case: 9EB63F8FEEC4DEB8 - run
gpg --armor --export <KEYID>
to export your key. You want to upload your public key that looks like:-----BEGIN PGP PUBLIC KEY BLOCK----- ..... -----END PGP PUBLIC KEY BLOCK-----
- run
gpg --armor --export-secret-key <KEYID>
to export your private key that looks like-----BEGIN PGP PRIVATE KEY BLOCK----- ..... -----END PGP PRIVATE KEY BLOCK-----
. Store this in a password manager/secret available only to your software for decrypting.
- in your terminal/command prompt enter
- Using our Binary resource upload your public key
Step 4. Get an Improve Designer account
You can design your own questionnaires and patient instructions which you can use for your app project using the Improve Designer.
- Register your own Improve Designer account or deliver your API key to one of your colleagues who has an Improve Designer account.
- Use the program credentials for your app project.
Step 5. Start your integration
Start building your own App project using our documentation and our example FHIR Client Example Repository.
Authentication
Retrieve the access token:
// Create Interceptor class that implements IClientInterceptor
@Override
public void interceptRequest(IHttpRequest theRequest) {
String token = getToken();
theRequest.addHeader(Constants.HEADER_AUTHORIZATION, (Constants.HEADER_AUTHORIZATION_VALPREFIX_BEARER + token));
}
private String getToken() {
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://auth.openhealthhub.com/auth/realms/OpenHealthHub/protocol/openid-connect/token"))
.POST(ofString("client_id=api-sandbox&client_secret=95810e52-4307-41f5-99a4-d873ab63b536&grant_type=client_credentials"))
.header("Content-Type", "application/x-www-form-urlencoded")
.build();
String response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()).body();
JsonObject respAsJson = JsonParser.parseString(response).getAsJsonObject();
String token = respAsJson.getAsJsonPrimitive("access_token").getAsString();
}
const headers = new Headers();
headers.append('Content-Type', 'application/x-www-form-urlencoded');
headers.append('Accept', 'application/json');
const urlencoded = new URLSearchParams();
urlencoded.append('client_id', 'api-sandbox');
urlencoded.append('client_secret', '95810e52-4307-41f5-99a4-d873ab63b536');
urlencoded.append('grant_type', 'client_credentials');
const requestOptions = {
method: 'POST',
headers: headers,
body: urlencoded,
redirect: 'follow'
};
const token = await fetch('https://auth.openhealthhub.com/auth/realms/OpenHealthHub/protocol/openid-connect/token', requestOptions)
.then(response => response.json())
.then(result => result.access_token);
$ch = curl_init('https://auth.openhealthhub.com/auth/realms/OpenHealthHub/protocol/openid-connect/token');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => 'client_id=api-sandbox&client_secret=95810e52-4307-41f5-99a4-d873ab63b536&grant_type=client_credentials',
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded'
],
]);
$res = curl_exec($ch);
curl_close($ch);
$decoded_json = json_decode($res, true);
$token = $decoded_json['access_token'];
type TokenResponse struct {
Access_token string
}
func authenticate() string {
url := "https://auth.openhealthhub.com/auth/realms/OpenHealthHub/protocol/openid-connect/token"
method := "POST"
payload := strings.NewReader("client_id=api-sandbox&client_secret=95810e52-4307-41f5-99a4-d873ab63b536&grant_type=client_credentials")
client := &http.Client{}
req, err := http.NewRequest(method, url, payload)
if err != nil {
fmt.Println(err)
return ""
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return ""
}
defer res.Body.Close()
data := new(TokenResponse)
err = json.NewDecoder(res.Body).Decode(&data)
if err != nil {
fmt.Println(err)
return ""
}
return data.Access_token
}
# The ruby fhir client takes care of retrieving the token, if configured correctly as shown below
token=$(curl --location --request POST 'https://auth.openhealthhub.com/auth/realms/OpenHealthHub/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=api-sandbox' \
--data-urlencode 'client_secret=95810e52-4307-41f5-99a4-d873ab63b536' \
--data-urlencode 'grant_type=client_credentials' | jq -r '.access_token')
def get_token():
url = 'https://auth.openhealthhub.com/auth/realms/OpenHealthHub/protocol/openid-connect/token'
response = requests.post(url,
f'client_id={api-sandbox}&client_secret={95810e52-4307-41f5-99a4-d873ab63b536}&grant_type=client_credentials',
headers={'Content-Type': 'application/x-www-form-urlencoded'}).json()
return response['access_token']
const string authUrl =
"https://auth.openhealthhub.com/auth/realms/OpenHealthHub/protocol/openid-connect/token";
IEnumerable<KeyValuePair<string, string>> auth = new[]
{
new KeyValuePair<string, string>("client_id", "api-sandbox"),
new KeyValuePair<string, string>("client_secret", "95810e52-4307-41f5-99a4-d873ab63b536"),
new KeyValuePair<string, string>("grant_type", "client_credentials")
};
HttpClient http = new HttpClient();
HttpContent content = new FormUrlEncodedContent(auth);
HttpResponseMessage response = http.PostAsync(authUrl, content).Result;
return JsonConvert.DeserializeObject<Dictionary<string, string>>(
response.Content.ReadAsStringAsync().Result);
The
client_id
andclient_secret
need to be replaced with the id and secret for your clientAdding the API key and token to the request:
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
client.additional_headers = {'X-API-KEY': 'ad880601-b7e6-4d86-901d-b6fca96fc725'}
client.set_oauth2_auth('client_id', 'client_secret', 'https://auth.openhealthhub.com/auth/realms/OpenHealthHub/protocol/openid-connect/auth', 'https://auth.openhealthhub.com/auth/realms/OpenHealthHub/protocol/openid-connect/token')
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4',
authorization=f'Bearer {get_token()}',
extra_headers={'x-api-key': 'ad880601-b7e6-4d86-901d-b6fca96fc725'}
)
# With shell, you can just pass the correct header with each request
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4" \
-H "X-API-KEY: ad880601-b7e6-4d86-901d-b6fca96fc725" \
-H "Authorization: Bearer $token"
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const requestObj = typeof request === 'string' ? {url: request} : request;
const parameterSeparator = requestObj.url.indexOf('?') === -1 ? '?' : '&';
requestObj.url = `${requestObj.url}${parameterSeparator}apikey=ad880601-b7e6-4d86-901d-b6fca96fc725`;
requestObj.headers = {
Authorization: `Bearer ${this.token}`
};
client.request(requestObj);
$ch = curl_init('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => array(
'X-API-Key: ' . self::API_KEY
),
CURLOPT_XOAUTH2_BEARER => $token,
CURLOPT_HTTPAUTH => CURLAUTH_BEARER
]);
req := http.NewRequest("GET", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4", nil)
req.Header.Add("X-API-KEY", "ad880601-b7e6-4d86-901d-b6fca96fc725")
token := authenticate()
req.Header.Add("Authorization", "Bearer " + token)
@Interceptor
public class ApiKeyInterceptor {
@Hook(Pointcut.CLIENT_REQUEST)
public void addApiKey(IHttpRequest request) {
request.addHeader("X-API-KEY", "ad880601-b7e6-4d86-901d-b6fca96fc725");
}
}
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
client.registerInterceptor(new ApiKeyInterceptor());
client.registerInterceptor(new AuthorizationInterceptor());
public class ApiKeyMessageHandler : HttpClientHandler
{
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Add("x-api-key", "ad880601-b7e6-4d86-901d-b6fca96fc725");
request.Headers.Add("Authorization", $"Bearer {_token}");
return await base.SendAsync(request, cancellationToken);
}
}
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4", settings, new ApiKeyMessageHandler());
Make sure to replace
api-key
with your personal API key.
Improve uses API keys and OpenID Connect to allow access to the API. You can register a new Improve API key at our developer portal.
Improve expects the API key and OpenID Connect access token to be included in all API requests to the server in a header, or as request parameter on the url.
For the sandbox environment this looks as follows:
- API Key:
- A header:
X-API-KEY: ad880601-b7e6-4d86-901d-b6fca96fc725
- As url parameter:
https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Appointment/1?apikey=ad880601-b7e6-4d86-901d-b6fca96fc725
- A header:
- Access token header:
Authorization: Bearer <access_token_retrieved_from_oidc_server>
You can retrieve the access token header from the token endpoint using the fields below. In some cases the language's FHIR library will take care of this. You'll need to set these values in your code then (see the examples to the right).
- Access token endpoint:
https://auth.openhealthhub.com/auth/realms/OpenHealthHub/protocol/openid-connect/token
- OpenID Connect client_id:
api-sandbox
- OpenID Connect client_secret:
95810e52-4307-41f5-99a4-d873ab63b536
- OpenID Connect grant_type:
client_credentials
FHIR 4 API Reference
Overview
FHIR Resource | Open HealthHub entity |
---|---|
PlanDefinition | Program |
Questionnaire | Module |
CarePlan | Invited patient |
CareTeam | Program participants for specific invited patient (including public key) |
Practitioner | Program participant (including public key) |
Questionnaire | Module |
QuestionnaireResponse | Module Response |
Times
Before going into detail with the exposed FHIR API's it's important to note that all dates and timestamps mentioned in these API's are in UTC. This means that any localisation of these dates and timestamps will need to happen on the client side. For exact details on how to fill these, we recommend looking into the FHIR documentation
Binary (Upload Key)
Upload public key
String publicKey = Base64.getEncoder().encodeToString(IOUtils.resourceToByteArray("sandbox.pub"));
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Binary"))
.POST(ofString(publicKey))
.header("Content-Type", "text/plain")
.header("Authorization", "Bearer " + getToken())
.header("x-api-key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
.build();
HttpResponse<Void> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.discarding());
HttpClient http = new HttpClient();
var publicKey = File.ReadAllBytes(Path.Combine(Directory.GetCurrentDirectory(), "sandbox.pub"));
var encodedKey = System.Convert.ToBase64String(publicKey);
var content = new ByteArrayContent(Encoding.ASCII.GetBytes(encodedKey));
content.Headers.ContentType = MediaTypeHeaderValue.Parse("text/plain");
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AuthenticationUtil.authenticate());
http.DefaultRequestHeaders.Add("x-api-key", "ad880601-b7e6-4d86-901d-b6fca96fc725");
HttpResponseMessage response = http.PostAsync("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Binary", content).Result;
Console.Out.WriteLine(response.StatusCode);
publicKey, err := ioutil.ReadFile("sandbox.pub")
if err != nil {
return 0, err
}
pubKeysBytes := []byte(publicKey)
encodedBytes := []byte(b64.StdEncoding.EncodeToString(pubKeysBytes))
req, err := http.NewRequest("POST", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Binary", bytes.NewBuffer(encodedBytes))
if err != nil {
return 0, err
}
token := client.Authenticate()
req.Header.Add("Authorization", "Bearer "+token)
req.Header.Add("Content-Type", "text/plain")
req.Header.Add("X-API-KEY", "ad880601-b7e6-4d86-901d-b6fca96fc725")
resp, err := http.DefaultClient.Do(req)
const client = new Client();
const token = await client.getToken();
const request = {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'Authorization': `Bearer ${token}`,
'X-API-KEY': 'ad880601-b7e6-4d86-901d-b6fca96fc725'
},
body: btoa(publicKey)
};
const promise = fetch('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Binary', request);
return promise.then(resp => {
return {responseStatus: resp.status, request};
});
$client = new FhirClient();
$keyASCII = file_get_contents(dirname(__FILE__) . 'sandbox.pub');
$encodedPubKey = base64_encode($keyASCII);
return $client->createRaw("Binary", $encodedPubKey, 'text/plain');
with open('sandbox.pub') as file:
key = file.read()
key_bytes = key.encode('ascii')
base64_bytes = base64.b64encode(key_bytes)
encoded_key = base64_bytes.decode('ascii')
token = get_token()
resp = requests.post(url='https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Binary',
data=encoded_key,
headers={'Content-Type': 'text/plain', 'Authorization': f'Bearer {token}', 'X-API-KEY': 'ad880601-b7e6-4d86-901d-b6fca96fc725'})
return resp
uri = URI('https://auth.openhealthhub.com/auth/realms/OpenHealthHub/protocol/openid-connect/token')
params = { 'client_id' => 'api-sandbox',
'client_secret' => '95810e52-4307-41f5-99a4-d873ab63b536',
'grant_type' => 'client_credentials' }
res = Net::HTTP.post_form(uri, params)
token = JSON.parse(res.body)['access_token']
file = open('sandbox.pub')
key = file.read
encoded_key = Base64.strict_encode64(key)
uri = URI('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Binary')
Net::HTTP.post(uri, encoded_key, { 'Content-Type' => 'text/plain',
'X-API-KEY' => 'ad880601-b7e6-4d86-901d-b6fca96fc725',
'Authorization' => 'Bearer ' + token })
base64Key=$(base64 -w 0 sandbox.pub)
curl -X POST "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Binary" -H "Content-Type: text/plain" -H "X-API-KEY: ad880601-b7e6-4d86-901d-b6fca96fc725" -H "Authorization: Bearer $token" --data-raw "$base64Key"
This endpoint can be used to upload your public key to Open HealthHub. This key will be used to encrypt patient data so you/your application can decrypt the patient data using your private key.
HTTP Request
POST https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Binary
The body of the post should be the base64 encoded version of the public key.
Request headers
Header | Value | Description |
---|---|---|
Content-Type | text/plain | the Content-Type should be text/plain |
CarePlan
Create CarePlan
curl -X POST "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan" -H "Content-Type: application/json" --data-binary "@careplan.json"
CarePlan carePlan = new CarePlan();
carePlan.setPeriod(new Period().setStart(Date.from(Instant.now())));
Patient patient = new Patient();
patient.setId("patient");
HumanName humanName = new HumanName();
humanName.setText("Test Patient");
patient.setName(Collections.singletonList(humanName));
ContactPoint email = new ContactPoint();
email.setSystem(ContactPoint.ContactPointSystem.EMAIL);
email.setValue("test@patient.ohh");
patient.addTelecom(email);
Identifier programPatientId = new Identifier();
programPatientId.setSystem("urn:oid:2.16.840.1.113883.2.4.99");
programPatientId.setValue("1234");
patient.addIdentifier(programPatientId);
carePlan.addContained(patient);
carePlan.setSubject(new Reference("#patient"));
carePlan.setInstantiatesCanonical(List.of(new CanonicalType("PlanDefinition/cca2eaf3-03a9-46c0-88c6-e0287917cea6")));
return client.create().resource(carePlan).execute();
var patient = new Patient
{
Id = "patient",
Identifier = new List<Identifier>
{
new Identifier
{
System = "urn:oid:2.16.840.1.113883.2.4.99",
Value = "1234"
}
},
Name = new List<HumanName> {new HumanName {Text = "Test Patient"}},
Telecom = new List<ContactPoint>
{
new ContactPoint
{
System = ContactPoint.ContactPointSystem.Email,
Value = "test@patient.ohh"
}
}
};
var carePlan = new CarePlan
{
Period = new Period(FhirDateTime.Now(), null),
Contained = new List<Resource>{patient},
Subject = new ResourceReference("#patient"),
InstantiatesCanonical = new List<string> { "PlanDefinition/cca2eaf3-03a9-46c0-88c6-e0287917cea6" }
};
var plan = client.Create(carePlan);
Console.Out.WriteLine(plan.InstantiatesCanonical.First());
}
const client = new Client();
return client.create({
resourceType: "CarePlan",
contained: [
{
resourceType: "Patient",
id: "patient",
identifier: [
{
system: "urn:oid:2.16.840.1.113883.2.4.99",
value: "1234"
}
],
name: [
{
text: "Test Patient"
}
],
telecom: [
{
system: "email",
value: "test@patient.ohh"
}
]
},
{
resourceType: "Task",
id: "2",
status: "requested",
intent: "original-order",
input: [
{
type: {
coding: [
{
system: "http://openhealthhub.com/fhir/StructureDefinition/care-plan-task",
code: "create-user"
}
]
},
valueBoolean: true
},
{
type: {
coding: [
{
system: "http://openhealthhub.com/fhir/StructureDefinition/care-plan-task",
code: "register-program"
}
]
},
valueBoolean: true
},
{
type: {
coding: [
{
system: "http://openhealthhub.com/fhir/StructureDefinition/care-plan-task",
code: "send-invitation-mail"
}
]
},
valueBoolean: true
}
]
}
],
instantiatesCanonical: [
"https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition/cca2eaf3-03a9-46c0-88c6-e0287917cea6"
],
status: "active",
intent: "proposal",
subject: {
reference: "#patient"
},
period: {
start: "2021-03-16"
},
activity: [
{
reference: {
reference: "#2"
}
}
]
}
);
FhirClient.new
careplan = FHIR::CarePlan.new
careplan.period = FHIR::Period.new
careplan.period.start = Date.new(2021, 7, 9)
patient = FHIR::Patient.new
patient.id = 'patient'
patient_identifier = FHIR::Identifier.new
patient_identifier.system = 'urn:oid:2.16.840.1.113883.2.4.99'
patient_identifier.value = '1234'
patient.identifier = [patient_identifier]
patient.name = 'Test Patient'
patient_email = FHIR::ContactPoint.new
patient_email.system = 'email'
patient_email.value = 'test@patient.ohh'
patient.telecom = [patient_email]
careplan.contained = [patient]
careplan.instantiatesCanonical = 'PlanDefinition/cca2eaf3-03a9-46c0-88c6-e0287917cea6'
patient_reference = FHIR::Reference.new
patient_reference.reference = '#patient'
careplan.subject = patient_reference
FHIR::CarePlan.create(careplan)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
with open('careplan.json', 'r') as file:
careplan_json = json.load(file)
create_response = await client.execute ('CarePlan', method='post', data=careplan_json)
print(create_response)
file, err := os.Open("careplan/careplan.json")
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan", file)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
if method == "POST" {
req.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(req)
$client = new FhirClient();
$careplan = new FHIRCarePlan();
$patient = new FHIRPatient();
$patientName = new FHIRHumanName();
$patientName->setText('Test Patient');
$patientEmail = new FHIRContactPoint();
$telecomType = new FHIRContactPointSystem();
$telecomType->setValue('email');
$patientEmail->setSystem($telecomType)->setValue('test@patient.ohh');
$patientIdentifier = new FHIRIdentifier();
$patientIdentifier->setSystem('urn:oid:2.16.840.1.113883.2.4.99')->setValue('1234');
$patient->setId('patient')->addName($patientName)->addTelecom($patientEmail)->addIdentifier($patientIdentifier);
$careplan->addContained($patient);
$careplan->setSubject(new FHIRReference(['#patient']));
$careplan->setInstantiatesCanonical([new FHIRCanonical('PlanDefinition/cca2eaf3-03a9-46c0-88c6-e0287917cea6')]);
$period = new FHIRPeriod();
$period->setStart([new FHIRInstant('2021-03-16')]);
$careplan->setPeriod($period);
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($careplan)
]);
$res = curl_exec($ch);
curl_close($ch);
$createdCarePlan = new FHIRCarePlan(json_decode($res, true));
Update CarePlan
curl -X PUT "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/1" -H "Content-Type: application/json" --data-binary "@careplan.json"
CarePlan carePlan = new CarePlan();
carePlan.setId("1");
carePlan.setPeriod(new Period().setStart(Date.from(Instant.now())));
Patient patient = new Patient();
patient.setId("patient");
HumanName humanName = new HumanName();
humanName.setText("Test Patient");
patient.setName(Collections.singletonList(humanName));
ContactPoint email = new ContactPoint();
email.setSystem(ContactPoint.ContactPointSystem.EMAIL);
email.setValue("test@patient.ohh");
patient.addTelecom(email);
Identifier programPatientId = new Identifier();
programPatientId.setSystem("urn:oid:2.16.840.1.113883.2.4.99");
programPatientId.setValue("1234");
patient.addIdentifier(programPatientId);
carePlan.addContained(patient);
carePlan.setSubject(new Reference("#patient"));
carePlan.setInstantiatesCanonical(List.of(new CanonicalType("PlanDefinition/cca2eaf3-03a9-46c0-88c6-e0287917cea6")));
return client.update().resource(carePlan).execute();
var patient = new Patient
{
Id = "patient",
Identifier = new List<Identifier>
{
new Identifier
{
System = "urn:oid:2.16.840.1.113883.2.4.99",
Value = "1234"
}
},
Name = new List<HumanName> {new HumanName {Text = "Test Patient"}},
Telecom = new List<ContactPoint>
{
new ContactPoint
{
System = ContactPoint.ContactPointSystem.Email,
Value = "test@patient.ohh"
}
}
};
var carePlan = new CarePlan
{
Id = "1",
Period = new Period(FhirDateTime.Now(), null),
Contained = new List<Resource>{patient},
Subject = new ResourceReference("#patient"),
InstantiatesCanonical = new List<string> { "PlanDefinition/cca2eaf3-03a9-46c0-88c6-e0287917cea6" }
};
var plan = client.Update(carePlan);
Console.Out.WriteLine(plan.InstantiatesCanonical.First());
}
const client = new Client();
return client.update({
resourceType: "CarePlan",
id: "1",
contained: [
{
resourceType: "Patient",
id: "patient",
identifier: [
{
system: "urn:oid:2.16.840.1.113883.2.4.99",
value: "1234"
}
],
name: [
{
text: "Test Patient"
}
],
telecom: [
{
system: "email",
value: "test@patient.ohh"
}
]
}
],
instantiatesCanonical: [
"https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition/cca2eaf3-03a9-46c0-88c6-e0287917cea6"
],
status: "active",
intent: "proposal",
subject: {
reference: "#patient"
},
period: {
start: "2021-03-16"
}
}
);
FhirClient.new
careplan = FHIR::CarePlan.new
careplan.period = FHIR::Period.new
careplan.period.start = Date.new(2021, 7, 9)
patient = FHIR::Patient.new
patient.id = 'patient'
patient_identifier = FHIR::Identifier.new
patient_identifier.system = 'urn:oid:2.16.840.1.113883.2.4.99'
patient_identifier.value = '1234'
patient.identifier = [patient_identifier]
patient.name = 'Test Patient'
patient_email = FHIR::ContactPoint.new
patient_email.system = 'email'
patient_email.value = 'test@patient.ohh'
patient.telecom = [patient_email]
careplan.contained = [patient]
careplan.instantiatesCanonical = 'PlanDefinition/cca2eaf3-03a9-46c0-88c6-e0287917cea6'
patient_reference = FHIR::Reference.new
patient_reference.reference = '#patient'
careplan.subject = patient_reference
FHIR::CarePlan.partial_update(1, careplan)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
with open('careplan.json', 'r') as file:
careplan_json = json.load(file)
update_response = await client.execute ('CarePlan', method='put', data=careplan_json)
print(update_response)
file, err := os.Open("careplan/careplan.json")
if err != nil {
return nil, err
}
req, err := http.NewRequest("PUT", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/1", file)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
if method == "POST" {
req.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(req)
$client = new FhirClient();
$careplan = new FHIRCarePlan();
$careplan->setId("1");
$patient = new FHIRPatient();
$patientName = new FHIRHumanName();
$patientName->setText('Test Patient');
$patientEmail = new FHIRContactPoint();
$telecomType = new FHIRContactPointSystem();
$telecomType->setValue('email');
$patientEmail->setSystem($telecomType)->setValue('test@patient.ohh');
$patientIdentifier = new FHIRIdentifier();
$patientIdentifier->setSystem('urn:oid:2.16.840.1.113883.2.4.99')->setValue('1234');
$patient->setId('patient')->addName($patientName)->addTelecom($patientEmail)->addIdentifier($patientIdentifier);
$careplan->addContained($patient);
$careplan->setSubject(new FHIRReference(['#patient']));
$careplan->setInstantiatesCanonical([new FHIRCanonical('PlanDefinition/cca2eaf3-03a9-46c0-88c6-e0287917cea6')]);
$period = new FHIRPeriod();
$period->setStart([new FHIRInstant('2021-03-16')]);
$careplan->setPeriod($period);
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/1';
$ch = curl_init($url);
$body = json_encode($careplan);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_POSTFIELDS => $body
]);
$res = curl_exec($ch);
curl_close($ch);
$updatedCarePlan = new FHIRCarePlan(json_decode($res, true));
Get CarePlan
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/3"
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
CarePlan carePlan = client.read()
.resource(CarePlan.class)
.withId(3L)
.execute();
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
var app = client.Read<CarePlan>("CarePlan/1");
Console.Out.WriteLine(app.Description);
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const carePlan = await client.request('CarePlan/1');
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
FHIR::CarePlan.read(3)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
carePlan = await client.resource('CarePlan').execute('1', 'GET')
req, err := http.NewRequest("GET", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/1", nil)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
if method == "POST" {
req.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(req)
unmarshaller, err := jsonformat.NewUnmarshaller(config.TimeZone, config.FhirVersion)
if err != nil {
return nil, err
}
bytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
unmarshal, err := unmarshaller.Unmarshal(bytes)
if err != nil {
return nil, err
}
unmarshal.(*r4pb.ContainedResource).GetCarePlan()
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/4?_format=json';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$carePlan = new FHIRCarePlan(json_decode($res, true));
Search CarePlan
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/?instantiates-canonical=PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806%7C1&patient.identifier=1234"
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
CarePlan carePlan = client.search()
.forResource(CarePlan.class)
.where(CarePlan.INSTANTIATES_CANONICAL.hasId("PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806|1"))
.and(CarePlan.PATIENT.hasChainedProperty(Patient.IDENTIFIER.exactly().identifier("1234")))
.returnBundle(Bundle.class)
.execute();
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
var searchPlan = client.Search<CarePlan>(new[]
{"instantiates-canonical=PlanDefinition/4944e73f-e447-49ba-a64c-a246b9ef4bdd|1", "patient.identifier=1234"});
searchPlan.Entry.ForEach(cp => Console.WriteLine(cp.FullUrl));
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const carePlan = await client.request('CarePlan/?patient.identifier=1234');
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
params = {
'instantiates-canonical': 'PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806|1',
'patient.identifier': '1234'
}
FHIR::CarePlan.search(params)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
response = await client.resources('CarePlan').search(instantiates_canonical='PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806|1').fetch()
req, err := http.NewRequest("GET", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan?instantiates-canonical=PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806|1&patient.identifier=1234", nil)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
response, err := http.DefaultClient.Do(req)
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/?_format=json&instantiates-canonical=PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806|1&patient.identifier=1234';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$bundle = new FHIRBundle(json_decode($res, true));
Search CarePlan by ID with Practitioners
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/?_id=3&_include=CarePlan:care-team&_include=CareTeam:participant"
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/?_format=json&_id=4&_include=CarePlan:care-team&_include=CareTeam:participant';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$bundle = new FHIRBundle(json_decode($res, true));
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
CarePlan carePlan = client.search()
.forResource(CarePlan.class)
.where(IAnyResource.RES_ID.exactly().identifier("4"))
.include(new Include("CarePlan:" + CarePlan.SP_CARE_TEAM))
.include(new Include("CareTeam:" + CareTeam.SP_PARTICIPANT))
.returnBundle(Bundle.class)
.execute();
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
params = {
'_id': id,
'_include': %w[CarePlan:care-team CareTeam:participant]
}
FHIR::CarePlan.search(params)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
response = await client.resources('CarePlan').search(_id='4').include('CarePlan', 'care-team').include('CareTeam', 'participant').fetch()
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const carePlan = await client.request('CarePlan/?_id=1&_include=CarePlan:care-team&_include=CareTeam:participant');
req, err := http.NewRequest("GET", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan?_id=1&_include=CarePlan:care-team&_include=CareTeam:participant", nil)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
response, err := http.DefaultClient.Do(req)
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
var searchPlan = client.Search<CarePlan>(new[]
{"_id=1", "_include=CarePlan:care-team", "_include=CareTeam:participant"});
searchPlan.Entry.ForEach(cp => Console.WriteLine(cp.FullUrl));
Delete CarePlan
curl -X DELETE "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/3"
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
CarePlan carePlan = client.delete().resourceById("CarePlan", "1").execute();
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
client.Delete("CarePlan/1");
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
await client.delete('CarePlan', 1);
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
FHIR::CarePlan.destroy(3)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
await client.resource('CarePlan').execute('1', 'DELETE')
req, err := http.NewRequest("DELETE", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/1", nil)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
if method == "POST" {
req.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(req)
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/4';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CUSTOMREQUEST => 'DELETE'
]);
$res = curl_exec($ch);
curl_close($ch);
When a care plan is scheduled by a doctor via Open HealthHub the patient will be notified by email. They will receive a link and pin code to
access the program.
When a care plan is scheduled through a connected EHR system the link (with pin code) will be given in the CarePlan response and can then be
integrated where needed in the requesting application.
The link will be specified in an extension with url https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/program-link
.
The pin code is part of the link but is also specified separately in an extension with
the url https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/program-pin
.
The APIs can be used to connect Open HealthHub to, for example, your EHR or scheduling software.
Retrieving a CarePlan
A CarePlan can be retrieved by its ID using a GET
request. The CarePlan will contain the following information:
- patient information (
subject
) - the PlanDefinition it has instantiated (
instantiatesCanonical
) status
, eitheractive
ordraft
, depending on whether the patient has accepted the invitation- Start and end date of the CarePlan (
period.start
andperiod.end
) - An
extension
containing the URL the patient needs to open the program as described above - An
extension
containing the PIN which the patient needs to register the program - Two
extensions
that contain the Customized Invite Mail text and image when set in the Improve Designer- The image uses extension url
https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/custom-invite-image
and is of type Attachment - The text uses extension url
https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/custom-invite-text
and is of type String
- The image uses extension url
- An
extension
with a link to the embedded form filler program timeline. This will only be present when the patient is already registered to the program. With this link the patient can open the embedded form filler and see an overview of the tasks in the program. The extension url ishttps://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/form-filler-program-timeline
. - A
careTeam
reference. The CareTeam contains a list of Practitioner references. The Practitioners have a public key that should be used for encrypting the QuestionnaireResponses that fulfill activities in this CarePlan. ( See Including referenced resources for information on retrieving these resources in a single HTTP request) - An
activity
list. Every activity represents a questionnaire for the patient. Questionnaires are calculated dynamically based on input from previous questionnaires, so the list is not final and can grow over time. The activities contain the following information:outcomeReference
a logical reference to a QuestionnaireResponse which holds the answers from the patient to this questionaire. The QuestionnaireResponse might not exist yet when the activity isin-progress
detail.instantiatesCanonical
a reference to the Questionnaire it representsdetail.status
can be either of the followingin-progress
,completed
,cancelled
,entered-in-error
.detail.scheduledPeriod.start
the date the questionnaire started to be available for the patient.detail.scheduledPeriod.end
represents the due date for unfinished questionnaires, and the date of completion for completed questionnaire.detail.description
a human-readable title of the questionnaire.- Two
extensions
that contain information to open the specific questionnaire directly in the embedded form filler: - The extension with url
https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/form-filler-program-task
contains the direct link to open the embedded form filler - The extension with url
https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/task_uuid
contains the task with the UUID that can be used to compose the URL to the embedded form filler yourself
Using with the Embedded Form Filler
Besides using the embedded form filler links in the extensions, you can also build the links to the embedded form filler yourself:
The URL has the following format: https://formfiller.openhealthhub.com/user/<user-uuid>/program/<program-uuid>-<invite-id>/module/<task-uuid>?signature=<signature>&key=<key>
The values to use in the link can be retrieved from the following fields in the CarePlan:
URL placeholder | CarePlan location | Description |
---|---|---|
user-uuid | 'contained subject'.identifier |
with system urn:ietf:rfc:3986 , without the urn:uuid: prefix |
program-uuid | instantiatesCanonical |
only the UUID part after PlanDefinition, this is the program UUID used when creating the CarePlan |
invite-id | id |
the id of the careplan |
task-uuid | activity.detail.extension |
optional when you want to open this questionnaire directly. As described above the value from the task_uuid extension |
signature | 'contained subject'.extension |
extension with url https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/patient-signature identifying the user-uuid, used to prevent changing the url |
key | see note below |
Searching CarePlans
CarePlans can be retrieved by searching on (a combination) of the following fields, using a GET
request. The content of the CarePlan will
be the same as for Retrieving a CarePlan.
_id
: The id of the CarePlanpatient.identifier
: One of the patient identifierspatient.email
: The email address of the patientinstantiates-canonical
: The canonical URL, reference or the UUID of the PlanDefinition for which the CarePlan is created. Optional you can add|
and version to search for a specific version of the PlanDefinition.
When no parameters are given all CarePlans for the client will be returned.
Including referenced resources
When searching CarePlans you can include referenced resources by using the _include
parameter.
The following include parameters are supported for CarePlan:
CarePlan:care-team
: include the CareTeam resource.CareTeam:participant
: include the participants of a CareTeam. Only applicable whenCarePlan:care-team
is also included.
The included participants will be part of the returned Bundle. For included resources the search.mode
of the Bundle entry will be set
to include
.
If you want to include these resource for a specific CarePlan (by id) you can use the request below. Ofcourse you can also combine the include parameters with other search parameters.
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan?_id=<ID>&_include=CarePlan:care-team&_include=CareTeam:participant
Creating a CarePlan
For creating new CarePlan the following fields are required (on top of the FHIR requirements):
Field | Description | Example Value |
---|---|---|
subject.actor | There should be exactly one patient subject | |
subject.actor.name.text | The patient subject should have a name specified in the text field, other properties will be ignored. If no name present the email address of the patient will be used |
Test Patient |
subject.actor.telecom | The patient subject should have a telecom element of type 'email', if multiple email addresses are found the first one will be used | test@patient.ohh |
subject.actor.identifier.value | The patient should have a patient number, this should be identifiable by the system creating the care plan. | 564372819 |
subject.actor.identifier.system | The patient identifier, this should be the system of the healthcare institution. | urn:oid:2.16.840.1.113883.2.4.99 |
subject.actor.identifier.use | The patient identifier use. Only required when supplying more than 1 identifier. If you list more than 1 identifier there should be one and only one with use official . |
official, usual, secondary |
instantiatesCanonical | The instantiatesCanonical should hold a canonical URI according to FHIR specifications, Open Health Hub is a bit more lenient and accepts a reference to the PlanDefinition (i.e. Improve Program) for which the patient is invited | PlanDefinition/cca2eaf3-03a9-46c0-88c6-e0287917cea6 |
period.start * | A start date (for details regarding the format refer to Times) should be entered when the patient can start the program. When retrieving a care plan we will always fill the end date. If no end date is known, the start date will be used. | 2021-03-16 |
activity | For our enterprise customers we support adding an activity to specify whether:
|
Activities
As described above we also offer the possibility to automatically create a user for the patient (if it doesn't exist yet), register the
patient to the program and send an invitation email to the patient.
To achieve this you should add an activity
element to the CarePlan. The activity should contain a reference to a contained Task
resource.
This Task can have input
items that look as follows:
{ "type": { "coding": [ { "system": "http://openhealthhub.com/fhir/StructureDefinition/care-plan-task", "code": "<action>" } ] }, "valueBoolean": true }
Here action
should be replaced with the actual action:
- To create a user:
create-user
- To register the program to the patient:
register-patient
- To send an email to the patient:
send-invitation-mail
Note that you can't use register-patient
without also enabling create-user
.
Please see the create CarePlan JavaScript snippet on the left for an example what this looks like in a complete CarePlan request.
For more information about the returned CarePlan see Retrieving a CarePlan.
Updating a CarePlan
A CarePlan can be updated by sending a PUT
request with the updated CarePlan.
The CarePlan to update can be specified in 2 ways. The first is by setting the ID on the URL.
The second is by adding a patient.identifier
query parameter with one of the identifiers of the patient on the url.
There are some restrictions when updating a CarePlan:
- The CarePlan can only be updated when the patient has not completed any of the tasks in the program yet.
- When updating the patient's email address, the CarePlan will only be updated when the email address is not shared with other patients and when the email address does not belong to another user.
- When using the
patient.identifier
query:- If no CarePlan can be found using the query, a new CarePlan will be created
- If multiple CarePlans are found an error response with status
412
will be returned
- When updating by setting the ID on the URL, the ID should be set in the CarePlan resource body, and it should match the ID on the URL. (as per FHIR specification)
Deleting a CarePlan
A CarePlan can be deleted by sending a DELETE
request.
The CarePlan to delete can be specified in 2 ways. The first is by setting the ID on the URL.
The second is by adding patient.identifier
and instantiates-canonical
query parameters with on the url. The patient.identifier
should
be one of the identifiers of the patient; the instantiates-canonical
should be a reference to the PlanDefinition as a complete URL (
e.g. https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition/<ID>
) or a resource reference (e.g. PlanDefinition/<ID>
). If the query returns
multiple
results an error response with status 412
will be returned.
HTTP Request
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/<ID>
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan?_id=<ID>&_include=CarePlan:care-team&_include=CareTeam:participant
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/?patient.identifier=<PatientIdentifierToken>&patient.email=<PatientEmail>&instantiates-canonical=<PlanDefinitionReference>
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan?patient.identifier=<PatientIdentifierToken>&patient.email=<PatientEmail>&instantiates-canonical=<PlanDefinitionReference>&_include=CarePlan:care-team&_include=CareTeam:participant
POST https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan
PUT https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/<ID>
PUT https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/?patient.identifier=<PatientIdentifierToken>
DELETE https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/<ID>
DELETE https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CarePlan/?patient.identifier=<PatientIdentifierToken>&instantiates-canonical=<PlanDefinitionReference>
URL Parameters
Parameter | Description |
---|---|
ID | The ID of the CarePlan to retrieve |
PatientIdentifierToken | A token representing a patient identifier |
PlanDefinitionReference | A reference to the PlanDefinition (specified as: https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition/<PlanDefinitionId> or PlanDefinition/<PlanDefinitionId> |
PatientEmail | The patient email address |
CareTeam
Retrieve CareTeam including Practitioners
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CareTeam/?_id=3&_include=CareTeam:participant"
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CareTeam/?_format=json&_id=4&_include=CareTeam:participant';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$bundle = new FHIRBundle(json_decode($res, true));
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
CarePlan carePlan = client.search()
.forResource(CareTeam.class)
.where(IAnyResource.RES_ID.exactly().identifier("4"))
.include(new Include("CareTeam:" + CareTeam.SP_PARTICIPANT))
.returnBundle(Bundle.class)
.execute();
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
params = {
'_id': id,
'_include': 'CareTeam:participant'
}
FHIR::CareTeam.search(params)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
response = await client.resources('CareTeam').search(_id='4').include('CareTeam', 'participant').fetch()
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const careTeam = await client.request('CareTeam/?_id=1&_include=CareTeam:participant');
req, err := http.NewRequest("GET", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CareTeam?_id=1&_include=CareTeam:participant", nil)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
response, err := http.DefaultClient.Do(req)
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
var searchTeam = client.Search<CareTeam>(new[] {"_id=1", "_include=CareTeam:participant"});
searchTeam.Entry.ForEach(cp => Console.WriteLine(cp.FullUrl));
The CareTeam resembles the participants in the program as defined in Improve Designer's Add colleagues
step. Since the actual participants
allowed to see the patient data differs per patient, the CareTeam is linked to a specific CarePlan for a patient.
The ID of the CareTeam will match with the ID of the CarePlan.
When retrieving a CareTeam by ID it will contain references to the Practitioners in this CareTeam. To retrieve a Bundle with both the CareTeam and all participating Practitioners you can use the following request:
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CareTeam?_id=<ID>&_include=CareTeam:participant
The included Practitioner resource entries will have the search.mode
set to include
.
HTTP Request
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CareTeam/<ID>
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/CareTeam/?_id=<ID>&_include=CareTeam:participant
URL Parameters
Parameter | Description |
---|---|
ID | The ID of the CareTeam to retrieve |
Practitioner
Retrieve Practitioners from CareTeam
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Practitioner/?_has:CareTeam:_id=3"
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Practitioner/?_format=json&_has:CareTeam:_id=4';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$bundle = new FHIRBundle(json_decode($res, true));
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
CarePlan carePlan = client.search()
.byUrl(String.format("Practitioner?_has:CareTeam:_id=%s", 4))
.returnBundle(Bundle.class)
.execute();
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
params = {
'_has:CareTeam:_id': id
}
FHIR::Practitioner.search(params)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
response = await client.resources('Practitioner').search(**{'_has:CareTeam:_id':1}).fetch()
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const practitioner = await client.request('Practitioner/?_has:CareTeam:_id=1');
req, err := http.NewRequest("GET", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Practitioner/?_has:CareTeam:_id=1", nil)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
response, err := http.DefaultClient.Do(req)
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
var practitioners = client.Search<Practitioner>(new[] {"_has:CareTeam:_id=1"});
practitioners.Entry.ForEach(cp => Console.WriteLine(cp.FullUrl));
The Practitioner resembles a user in the Improve Platform and can be used to retrieve a public key used for encrypting a QuestionnaireResponse.
For the Practitioner content we use a custom
profile (https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/PractitionerWithKey
). The profile contains an extension
with the public key of the participant. The url for this extension
is https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/participant-program-public-key
.
Besides the public key the Practitioners with this profile also have 1 identifier which contains the UUID of the public key. The use of this
identifier is set to secondary
. This information is needed for creating an encrypted QuestionnaireResponse.
Retrieving a Practitioner by ID is currently only possible for the client itself. To search for other Practitioners you'll need to search them by CareTeam id. To accomplish this you can use the following request:
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Practitioner?_has=CareTeam:_id=<CareTeam ID>
This will return a Bundle with all Practitioners that are part of that CareTeam.
HTTP Request
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Practitioner/<ID>
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Practitioner/?_has=CareTeam:_id=<CareTeam ID>
URL Parameters
Parameter | Description |
---|---|
ID | The ID of the Practitioner to retrieve |
CareTeam ID | The ID of the CareTeam for which you want to retrieve the practitioners |
VitalSigns
Get Observation
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Observation/1"
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
Observation observation = client.read()
.resource(Observation.class)
.withId(1)
.execute()
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
var obs = client.Read<Observation>("Observation/1");
Console.Out.WriteLine(obs.Category[0].Text);
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const observation = await client.request("Observation/1");
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
FHIR::Observation.read(1)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
obs = await client.resource('Observation').execute('1', 'GET')
print(obs.resource_type, obs.id)
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Observation/4?_format=json';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$observation = new FHIRObservation(json_decode($res, true));
Search Observation
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Observation?identifier=identifier&device-name=devicename"
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
Bundle observationBundle = client.search()
.forResource(Observation.class)
.where(Patient.IDENTIFIER.exactly().identifier("1234"))
.where(Device.DEVICE_NAME.matches().value("deviceName"))
.returnBundle(Bundle.class)
.execute();
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
var obs = client.Read<Observation>("Observation/1");
Console.Out.WriteLine(obs.Category[0].Text);
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const bundle = await client.request("Observation?identifier=1234&device-name=testDevice");
const observations = bundle.entry.map(entry => entry.resource);
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
params = {
'identifier': '1234',
'device-name': 'testDevice'
}
FHIR::Observation.search(params)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
observations = await client.resources('Observation').search(identifier='identifier', device_name='devicename').fetch()
for o in observations:
print(o.as_json())
req, err := http.NewRequest("GET", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Observation/1", nil)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
if method == "POST" {
req.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(req)
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Observation?device-name=deviceName&identifier=1234?_format=json';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$bundle = new FHIRBundle(json_decode($res, true));
Having accurate patient’s vital signs available whenever and wherever needed is crucial for medical professionals to provide the best care possible. Vital signs acquired via Improve Connected Health can be monitored on Improve Dashboard, but also added to, for example, the patient’s EHR file.
The APIs can be used to (automatically) add vital signs retrieved via Open HealthHub in any of the supported formats to your EHR system(s), or to integrate or create a different dashboard.
HTTP Request
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Observation/<ID>
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Observation/search?identifier=<PATIENT-NUMBER>&device-name=<DEVICE-NAME>
URL Parameters
Parameter | Description |
---|---|
ID | The ID of the Observation to retrieve |
PATIENT-NUMBER | the patient-number to retrieve measurements Observations for |
DEVICE-NAME | name of the device that the measurements were made with |
PlanDefinition
Get PlanDefinition
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition/57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca"
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
PlanDefinition planDefinition = client.read()
.resource(PlanDefinition.class)
.withId("57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca")
.execute();
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/", settings);
var qr = client.Read<PlanDefinition>("PlanDefinition/57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca");
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const resp = await client.request("PlanDefinition/57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca");
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
FHIR::PlanDefinition.read('57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca')
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
response = await client.resource('PlanDefinition').execute('57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca', 'GET')
print(response)
req, err := http.NewRequest("GET", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition/57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca", nil)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
if method == "POST" {
req.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(req)
unmarshaller, err := jsonformat.NewUnmarshaller(config.TimeZone, config.FhirVersion)
if err != nil {
return nil, err
}
bytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
unmarshal, err := unmarshaller.Unmarshal(bytes)
if err != nil {
return nil, err
}
unmarshal.(*r4pb.ContainedResource).GetPlanDefinition()
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition/57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca?_format=json';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$planDefinition = new FHIRPlanDefinition(json_decode($res, true));
Search PlanDefinition
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition?definition=Questionnaire/97f680b9-e397-4298-8c53-de62a284c806"
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
Bundle bundle = client.search()
.forResource(PlanDefinition.class)
.where(PlanDefinition.DEFINITION.hasId("Questionnaire/97f680b9-e397-4298-8c53-de62a284c806"))
.returnBundle(Bundle.class)
.execute();
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
var qr = client.Search<PlanDefinition>(new []{"definition=Questionnaire/97f680b9-e397-4298-8c53-de62a284c806"});
qr.Entry.ForEach(component => Console.WriteLine(component.FullUrl));
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const bundle = await client.request("PlanDefinition?definition=Questionnaire/97f680b9-e397-4298-8c53-de62a284c806");
const responses = bundle.entry.map(entry => entry.resource);
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
params = {
'definition': 'Questionnaire/97f680b9-e397-4298-8c53-de62a284c806'
}
FHIR::PlanDefinition.search(params)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
response = await client.resources('PlanDefinition').search(definition='Questionnaire/97f680b9-e397-4298-8c53-de62a284c806').fetch()
print(response)
req, err := http.NewRequest("GET", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition?definition=Questionnaire/97f680b9-e397-4298-8c53-de62a284c806", nil)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
if method == "POST" {
req.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(req)
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition?definition=Questionnaire/97f680b9-e397-4298-8c53-de62a284c806?_format=json';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$bundle = new FHIRBundle(json_decode($res, true));
The program as created in Improve Designer.
Programs and therefore plan definitions are versioned. If you get the plan definition by id you get the latest version. You can also request a specific version. It is possible that some patients used an old version where new patients are already on the latest version of the plan definition. When referencing the plan definition in the care plan we therefore reference a specific version of the plan definition.
This endpoint can be used to retrieve a PlanDefinition by id, by id and version or search using a small set of parameters.
Getting by ID returns the latest version of the plan if you append | after the Id followed by the version you get the specific version of the plan. Make sure to use the url encoding %7C for |
HTTP Request
Get by ID
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition/<ID>
Get by ID and Version
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition/<ID>|<VERSION>
Search
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/PlanDefinition?definition=Questionnaire/<MODULE_UUID>|<MODULE_VERSION>&publisher=<PUBLISHER_NAME>
URL Parameters
Note that in case of search all parameters are optional, a search without parameters will return all resources that are accessible for your client.
Parameter | Description |
---|---|
ID | UUID of the PlanDefinition to retrieve |
VERSION | (optional) The version of the PlanDefinition to retrieve |
MODULE_UUID | (optional) UUID of the Questionnaire that is part of this PlanDefinition |
MODULE_VERSION | (optional) Version of the questionnaire, can only be used in combination with MODULE_UUID |
PUBLISHER_NAME | (optional) name of the user that published the PlanDefinition |
Questionnaire
Get Questionnaire
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Questionnaire/781dc338-a932-4c0e-8c02-8d419fad4264"
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
Questionnaire questionnaire = client.read()
.resource(Questionnaire.class)
.withId("781dc338-a932-4c0e-8c02-8d419fad4264")
.execute();
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
var questionnaire = client.Read<Questionnaire>("Questionnaire/781dc338-a932-4c0e-8c02-8d419fad4264");
Console.Out.WriteLine(questionnaire.Description);
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const questionnaire = client.request("Questionnaire/781dc338-a932-4c0e-8c02-8d419fad4264");
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
FHIR::Questionnaire.read("781dc338-a932-4c0e-8c02-8d419fad4264")
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
questionnaire = await client.resource('Questionnaire').execute('781dc338-a932-4c0e-8c02-8d419fad4264', 'GET')
print(questionnaire.description)
req, err := http.NewRequest("GET", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Questionnaire/781dc338-a932-4c0e-8c02-8d419fad4264", nil)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
if method == "POST" {
req.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(req)
unmarshaller, err := jsonformat.NewUnmarshaller(config.TimeZone, config.FhirVersion)
if err != nil {
return nil, err
}
bytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
unmarshal, err := unmarshaller.Unmarshal(bytes)
if err != nil {
return nil, err
}
unmarshal.(*r4pb.ContainedResource).GetQuestionnaire()
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Questionnaire/781dc338-a932-4c0e-8c02-8d419fad4264?_format=json';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$questionnaire = new FHIRQuestionnaire(json_decode($res, true));
Patients interact with healthcare professionals through questionnaires on Improve Mobile App. A Questionnaire can either be selected from Improve Library or custom-made in Improve Designer. Improve Designer allows the user to specify custom codings for questions. These codings can be used to map data in this resource to existing data. The unfilled-out questionnaire is made available in FHIR format through this end-point.
Questionnaires are versioned. If you get the questionnaire by id you get the latest version which is used in one of your plan definitions. You can also request a specific version. It is possible that patients answered an old version of the questionnaire in the past, therefore references to the questionnaire, for example in the questionnaire response are always to a specific version.
Generally the Questionnaire item components retrieved through this endpoint correspond to questions created in the Improve Designer. An exception to this is the readonly text items that are generated for the formulas defined in the Improve Designer. These are made so that even if a result slide is not defined (where the formula is mentioned as a sub item), the formula will still be in the questionnaire.
Questionnaires contain a list of item
elements which represents the specific questions in the questionnaire.
Question types
Users can make and design their own questionnaire in the designer, native FHIR does not contain any elements which are focused on design so we added some extensions to make it clear how the question should be displayed.
There are some general principles to keep in mind
Textual items like additional labels or the text for the next question button are added as items of type display to a question. The linkId then indicates where the label belongs to for example
vslide_4_below
indicates that the label should go below the question with linkIdvslide_4
. We also use thehttp://hl7.org/fhir/StructureDefinition/questionnaire-itemControl
extension to indicate the context of the text, we currently use:inline
,prompt
,upper
, andlower
.We added
extension
indicating the display type with the urlhttps://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/display-type"
this can have the valuesvertical-slider
,horizontal-slider
andtoggle
. The other display types can be determined from the FHIR question type.Not only questions can be linked to a code, but also answer options can be linked to a code this is not natively supported in FHIR, so we use an
extension
with the urlhttp://openhealthhub.com/fhir/StructureDefinition/question-answer-option-coding
.Most of our questions have a footer indicating the progress of a patient, the content of the footer is static and can not be customized. For result and info questions it is possible to indicate in the designer that the footer should be hidden. Question which do have a footer have a child item, with an extension of type
http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl
and valuefooter
.Any text item can contain HTML tags, even including base64 encoded images.
Below is an example of every question type with how they are displayed in the designer and how it is represented in FHIR.
Date Question
A date question is of type date
{ "id": "1", "linkId": "date_1", "prefix": "Date question", "text": "<p> This is an example of a date question<\p>", "type": "date", "repeats": false, "item": [ { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "footer", "display": "Footer" } ] } } ], "linkId": "date_1_footer", "type": "display" } ] }
Multiple Select Question
A multi select question is of type choice
and the value of repeats
is set to true
indicating multiple answers are allowed.
In this example every answer option has a label, a value and a code attached to it.
{ "id": "2", "linkId": "mselect_2", "prefix": "Multiple select question", "text": "<p>This is an example of a multiple select question in which every answers is linked to a SNOMED code. The question is about different symptoms and you can select multiple</p>", "type": "choice", "repeats": true, "answerOption": [ { "id": "mselect_2_answer-option_0", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "Sneezing" }, { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/question-answer-option-coding", "valueCoding": { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "162367006", "display": "Sneezing" } } ], "valueString": "0" }, { "id": "mselect_2_answer-option_1", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "Coughing" }, { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/question-answer-option-coding", "valueCoding": { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "162367006", "display": "Coughing" } } ], "valueString": "1" }, { "id": "mselect_2_answer-option_2", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "Fever" }, { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/question-answer-option-coding", "valueCoding": { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "386661006", "display": "Fever" } } ], "valueString": "2" }, { "id": "mselect_2_answer-option_3", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "Diarrhea" }, { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/question-answer-option-coding", "valueCoding": { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "62315008", "display": "Diarrhea" } } ], "valueString": "3" } ], "item": [ { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "footer", "display": "Footer" } ] } } ], "linkId": "mselect_2_footer", "type": "display" } ] }
Horizontal Slider
Horizontal slider questions can be recognised because they have an extension
https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/display-type
with the value horizontal-slider
. We also use the FHIR extension
http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl
with value slider
but the custom extension
is needed to indicate the direction of the slider. Sliders have answer options in which every answer option represents a tick on the slider, with labels.
Sliders also use the extension
https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/slider-step-decimal-value
to indicates the step size on
the slider. It is possible that there is no label and therefore no answer option for every allowed step value. We use a custom extension because step values
can be decimal.
The order of the answers options matter and should be represented in the slider.
Horizontal sliders also have optional labels which should be presented left, right or in the middle of the slider
In the following example some ticks are linked to codes but not all
{ "id": "3", "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/display-type", "valueString": "horizontal-slider" } { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/slider-step-decimal-value", "valueDecimal": 10 }, { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "slider", "display": "Slider" } ] } } ], "linkId": "hslide_3", "prefix": "Horizontal slide", "text": "<p>This is an example of a horizontal slider question. Asking about blood pressure, the low and high blood pressure values are mapped to the corresponding SNOMED codes</p>", "type": "decimal", "repeats": false, "answerOption": [ { "id": "hslide_3_answer-option_70", "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/question-answer-option-coding", "valueCoding": { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "45007003", "display": "Low Blood pressure" } }, { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "low" } ], "valueInteger": 70 }, { "id": "hslide_3_answer-option_80", "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/question-answer-option-coding", "valueCoding": { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "45007003", "display": "Low Blood pressure" } }, { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "80" } ], "valueInteger": 80 }, { "id": "hslide_3_answer-option_90", "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/question-answer-option-coding", "valueCoding": { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "45007003", "display": "Low Blood pressure" } }, { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "90" } ], "valueInteger": 90 }, { "id": "hslide_3_answer-option_100", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "100" } ], "valueInteger": 100 }, { "id": "hslide_3_answer-option_110", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "110" } ], "valueInteger": 110 }, { "id": "hslide_3_answer-option_120", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "120" } ], "valueInteger": 120 }, { "id": "hslide_3_answer-option_130", "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/question-answer-option-coding", "valueCoding": { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "194757006", "display": "High Blood pressure" } }, { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "130" } ], "valueInteger": 130 }, { "id": "hslide_3_answer-option_140", "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/question-answer-option-coding", "valueCoding": { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "194757006", "display": "High Blood pressure" } }, { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "140" } ], "valueInteger": 140 }, { "id": "hslide_3_answer-option_150", "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/question-answer-option-coding", "valueCoding": { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "194757006", "display": "High Blood pressure" } }, { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "high" } ], "valueInteger": 150 } ], "item": [ { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "lower", "display": "Lower-bound" } ] } } ], "linkId": "hslide_3_before", "text": "left", "type": "display" }, { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "upper", "display": "Upper-bound" } ] } } ], "linkId": "hslide_3_after", "text": "right", "type": "display" }, { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "prompt", "display": "Prompt" } ] } } ], "linkId": "hslide_3_center", "text": "center", "type": "display" }, { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "footer", "display": "Footer" } ] } } ], "linkId": "hslide_3_footer", "type": "display" } ] }
Vertical Slider
Vertical slider questions can be recognised because they have an extension
https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/display-type
with the value vertical-slider
. We also use the FHIR extension
http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl
with value slider
but the custom extension
is needed to indicate the direction of the slider. Sliders have answer options in which every answer option represents a tick on the slider.
Sliders also use the extension
https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/slider-step-decimal-value
to indicates the step size on
the slider. It is possible that there is no label and therefore no answer option for every allowed step value. We use a custom extension because step values
can be decimal.
The order of the answers options matter and should be represented in the slider.
Vertical sliders also have optional labels which can be represented above, or below the slider
In the example below the slider has answer options for the values 0, 10, 20, 30 and 40, but all answers between 0 and 40
rounded to 0.5 are valid, as indicated by the slider-step-decimal-value
{ "id": "4", "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/display-type", "valueString": "vertical-slider" }, { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/slider-step-decimal-value", "valueDecimal": 0.5 }, { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "slider", "display": "Slider" } ] } } ], "linkId": "vslide_4", "type": "decimal", "repeats": false, "answerOption": [ { "id": "vslide_4_answer-option_40", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "40" } ], "valueInteger": 40 }, { "id": "vslide_4_answer-option_30", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "30" } ], "valueInteger": 30 }, { "id": "vslide_4_answer-option_20", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "20" } ], "valueInteger": 20 }, { "id": "vslide_4_answer-option_10", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "10" } ], "valueInteger": 10 }, { "id": "vslide_4_answer-option_0", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "0" } ], "valueInteger": 0 } ], "item": [ { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "lower", "display": "Lower-bound" } ] } } ], "linkId": "vslide_4_below", "text": "Text below slider", "type": "display" }, { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "upper", "display": "Upper-bound" } ] } } ], "linkId": "vslide_4_above", "text": "Text above slider", "type": "display" }, { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "footer", "display": "Footer" } ] } } ], "linkId": "vslide_4_footer", "type": "display" } ] }
Number question
Number questions are of type decimal
they can be restricted in range, we use the FHIR extensions for minValue
and maxValue
. We also added our own extension
with the url https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/slider-step-decimal-value
.
This indicates the steps numbers can be increased with. If it is zero any number in the range is allowed, if its 2 and the range starts with 0 only even numbers are allowed.
In the example below the question is linked to a SNOMED code
{ "id": "5", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/minValue", "valueDecimal": 0 }, { "url": "http://hl7.org/fhir/StructureDefinition/maxValue", "valueDecimal": 300 }, { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/slider-step-decimal-value", "valueDecimal": 0 } ], "linkId": "410668003", "code": [ { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "410668003", "display": "Length" } ], "prefix": "Numbers", "text": "<p>An example of a numerical question, asking for the length of the patient, linked to a SNOMED code </>", "type": "decimal", "repeats": false, "item": [ { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "prompt", "display": "Prompt" } ] } } ], "linkId": "410668003_before", "text": "How tall are you", "type": "display" }, { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "prompt", "display": "Prompt" } ] } } ], "linkId": "410668003_after", "text": "centimeters", "type": "display" }, { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "footer", "display": "Footer" } ] } } ], "linkId": "410668003_footer", "type": "display" } ] }
Result Question
A result question is used to display the result of a formula to the patient. It can be based on questions earlier in the
questionnaire. The result question is of type group
. Note that it has the ability to hide footers, and add custom
text for the button to move to the next question. Result questions are only part of the questionnaire if the result of a
formula is displayed to a patient. It is also possible to use a formula without displaying it, in that case only a formula
is added to the questionnaire.
An explanation of how to deal with the formula itself can be found in the explanation of formula types at the bottom of this section.
The example below is linked to a SNOMED code and hides the footer, this is not the case for every result question. The formula in the example refers to input questions which are not part of these examples, see the sandbox to see a complete questionnaire.
{ "id": "7", "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/hide-footer", "valueBoolean": true } ], "linkId": "result_7", "code": [ { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "60621009", "display": "Body mass index" } ], "prefix": "Result", "text": "<p>This is an example of a result question, it displays the result of applying the formula to calculate BMI, the answers to the questions about length and mass are used as input for the calculation. The result is linked to the SNOMED code for BMI</p>", "type": "group", "repeats": false, "item": [ { "linkId": "result_7_formula_1", "code": [ { "system": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/CodeSystem/question-formula-result", "code": "formula_result", "display": "Formula Result" } ], "text": "($410668003$/(($4147007$)/100)^(2))", "type": "decimal", "item": [ { "linkId": "result_7_formula_1_display_name", "text": "BMI", "type": "display" } ] }, { "linkId": "result_7_next-button-label", "text": "Continue", "type": "display" } ] }
Yes No Question
A yes no question is essentially a boolean question with just two answer options, but there is a possibility that other options
then true
or false
are used, for example yes
or no
. So we map it to a question of type choice
and add an extension
with url https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/display-type"
and the value toggle
to indicate that
this should be displayed different from regular multiple choice questions. Like other questions with answer options we have the
possibility to add custom labels or codes to an answer option.
{ "id": "8", "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/display-type", "valueString": "toggle" } ], "linkId": "yesno_8", "prefix": "Yes/No question", "text": "<p> Example of a boolean question with custom labels and the yes answers is linked to the SNOMED code for pregnancy. Question are you pregnant </p>", "type": "choice", "repeats": false, "answerOption": [ { "id": "yesno_8_answer-option_0", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "No, or not certain" } ], "valueString": "0" }, { "id": "yesno_8_answer-option_1", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "Yes definitely" }, { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/question-answer-option-coding", "valueCoding": { "system": "urn:uuid:13f4adda-718c-11ed-a1eb-0242ac120002", "version": "2022-09", "code": "77386006", "display": "Pregnant" } } ], "valueString": "1" } ], "item": [ { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "footer", "display": "Footer" } ] } } ], "linkId": "yesno_8_footer", "type": "display" } ] }
Open Question
An open question is a question of type text
. We use the FHIR extension
http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl
with the value text-box
to indicate that it is a free field.
{ "id": "9", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "text-box", "display": "Text Box" } ] } } ], "linkId": "open_9", "prefix": "Example of an open question", "text": "<p>Example of an open questionlt/p>", "type": "text", "repeats": false, "item": [ { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "footer", "display": "Footer" } ] } } ], "linkId": "open_9_footer", "type": "display" } ] }
Multiple choice Question
A multiple choice question is a question in which only one answer option can be chosen. The type of the question is choice
.
You can distinguish it from the multiple select question because the value of repeats
is set to false
.
In the example below there are custom labels, but no codes attached to the different answer options.
{ "id": "10", "linkId": "mchoice_10", "prefix": "Multiple choice question", "text": "<p>Example of a multiple choice question, the patient can only select one option for the following question: On what type of device are you responding to this questionnaire</p>", "type": "choice", "repeats": false, "answerOption": [ { "id": "mchoice_10_answer-option_0", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "Laptop" } ], "valueString": "0" }, { "id": "mchoice_10_answer-option_1", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "Mobile phone" } ], "valueString": "1" }, { "id": "mchoice_10_answer-option_2", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix", "valueString": "Tablet" } ], "valueString": "2" } ], "item": [ { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "footer", "display": "Footer" } ] } } ], "linkId": "mchoice_10_footer", "type": "display" } ] }
Picture Question
A picture question is a question of type attachement
. If repeats
is true the patient is allowed to upload multiple pictures.
In that case we use the maxOccurs
extension from FHIR to indicate how many pictures can be uploaded.
{ "id": "11", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-maxOccurs", "valueInteger": 3 } ], "linkId": "image_11", "prefix": "Picture question", "text": "<p> Example of a picture question, asking patient to make a picture of an injury for example</p>", "type": "attachment", "repeats": true, "item": [ { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "inline", "display": "In-line" } ] } } ], "linkId": "image_11_below", "text": "<p>Text below picture</p>", "type": "display" }, { "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", "code": "footer", "display": "Footer" } ] } } ], "linkId": "image_11_footer", "type": "display" } ] }
Info Question
An info question is either of type display
or of type group
. Info questions are meant to display information to the patient.
The text element will contain html and can include a picture, table, list or other html content. There are also questionnaires which
only contain information questions. The starting and final questions of a questionnaire are always of type information.
{ "id": "12", "linkId": "info_12", "prefix": "Info question", "text": "<p>An example of an info question, containing only information.</p> <p>The final and starting question of a questionnaire are always of type info</p> <h5> </h5>", "type": "group", "repeats": false, "item": [ { "linkId": "info_12_next-button-label", "text": "Send answers to server, and don't forget to encrypt them", "type": "display" } ] }
Formula
We use formulas to calculate results based on previous inputs. We have the result type question to show the result of a formula to the user. But it is also possible to have a formula question without the result being shown to the user. In that scenario you'll still need to use the formula to calculate a result and send it to the server as part of the questionnaire response. For this reason we also add the formula as a questionnaire item, but it should not be displayed and only used for calculation. Formula questions can be recognised because they will contain the word 'formula' in the linkId and are added to end of the questionnaire.
We use the FHIR extension http://hl7.org/fhir/StructureDefinition/maxDecimalPlaces
to indicate the amount of decimal places to expres the result of the formula in.
Formulas mostly follow mathjs syntax. There is exactly one exception to this, which is
the variables. To clarify which QuestionnaireItem result is used in a formula,
the variables used in the formulas will be replaced by the linkId, surrounded by $
, of the QuestionnaireItem result that should replace
the variable.
This linkId may be specified by using the Export label
field in Improve Designer and does not necessarily follow mathjs's syntax.
For example, given answers {"some-long-link-id": 2, "something-else": 3}
and formula $some-long-link-id$ * $something-else$
, the
result should be 2 * 3
or when evaluated 6.
An example of the fhir code can be found below
{ "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/maxDecimalPlaces", "valueInteger": 2 } ], "linkId": "formula_2", "prefix": "BMI", "text": "($410668003$/(($4147007$)/100)^(2))", "type": "decimal", "readOnly": true }
Enable when behavior
All questions can contain a list of enableWhen
elements. If this list is not empty it means that the specific question should be hidden by
default and only switched on if the conditions in the list are met.
We follow the FHIR specifications, but we added two custom extensions to allow for some behavior which is allowed in our platform but is not
currently supported by default by FHIR.
Both extensions have to do with the fact that some questions allow for multiple answers.
The default implementation of the FHIR
enableWhen
element's=
is defined as 'true if whether at least one answer has a value that is equal to the enableWhen answer.'. If ourextension
with the urlhttps://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/use-strict-equal
is defined then this=
should be interpreted more strictly, meaning that in the case of multiple answers to the same question the enableWhen condition should only be considered true if only and only if that question is answered with the specific answer. For example consider a multi-select question asking about symptoms where a user can select "Coughing", "Sneezing", "Headache" or a combination of those answers. And consider a follow-up question asking about the specifics of the type of coughing, with the followingenableWhen
:"enableWhen": [{ "question": "symptoms_question", "operator": "=", "answerString": "Coughing" }]
This means that the question to which the enableWhen condition is applied should be shown when the answer to the symptoms question was either, "Coughing" or "Sneezing and Coughing" or "Coughing and Headache" or "Coughing and Headache and Sneezing" options where ticked by the user. If our extension is added as in the example below, the meaning changes, and the question should be shown when the user answered just " Coughing", but not in any other scenario."enableWhen": [ { "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/use-strict-equal", "valueBoolean": true } ], "question": "symptoms_question", "operator": "=", "answerString": "Coughing" }, ... ]
The second
extension
with the urlhttps://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/multi-select-answer
is used to enable questions based on multiple answers. If it's defined it will have at least two sub extensions with the urlhttps://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/multi-select-answer-value
, which contain the values that should all be selected to enable the question. If a question is shown based on a single value of a multi select value we will not use the regular enableWhen condition as in the example above. The two extension can be used together, but this is not needed. Consider the following example:"enableWhen": [ { "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/multi-select-answer", "extension": [ { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/multi-select-answer-value", "valueString": "Coughing" }, { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/multi-select-answer-value", "valueString": "Sneezing" } ] }, { "url": "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/use-strict-equal", "valueBoolean": true } ],
This means that the question to which this enableWhen conditions is applied should be shown when the answer to the "symptoms_question" was "Sneezing and Coughing" but not when the answer was only "Sneezing", or only "Coughing" and also not when the answer was "Sneezing and Coughing, and Headache". If the"question": "symptoms_question", "operator": "=", }, ... ]
https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/use-strict-equal
was not defined, the question should still not be enabled if only "Sneezing" or only "Coughing" was selected. But this time the question should be enabled if the patient answers "Sneezing and Coughing, and Headache" to the "symptoms_question".
Be aware that this logic adds on top of the default FHIR definitions of dealing with enableWhen conditions, and FHIR allows for multiple enableWhen conditions on a single question. The final example below shows an example of a question which should be enabled when the answer to "symptoms_question" contained either " Sneezing" or "Coughing". It does not use any of our custom extensions. This implementation is less strict, meaning that the question is shown in any scenario, except if only "Headache" was answered.
"enableWhen": [ { "question": "symptoms_question", "operator": "=", "answerString": "Sneezing" }, { "question": "symptoms_question", "operator": "=", "answerString": "Coughing" } ], "enableBehavior": "any",
Available endpoints
The endpoint can be used to retrieve a Questionnaire by id, and by id and version.
Getting by ID returns the latest version used in any planDefinition if you append | after the Id followed by the version you get the specific version of the plan. Make sure to use the url encoding %7C for |
HTTP Request
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Questionnaire/<ID>
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Questionnaire/<ID>|<VERSION>
URL Parameters
Parameter | Description |
---|---|
ID | The ID of the Questionnaire to retrieve |
VERSION | The version of the Questionnaire to retrieve |
QuestionnaireResponse
Get QuestionnaireResponse
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/QuestionnaireResponse/57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca"
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
QuestionnaireResponse questionnaireResponse = client.read()
.resource(QuestionnaireResponse.class)
.withId("57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca")
.execute();
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/", settings);
var qr = client.Read<QuestionnaireResponse>("QuestionnaireResponse/57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca");
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const resp = await client.request("QuestionnaireResponse/57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca");
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
FHIR::QuestionnaireResponse.read('57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca')
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
response = await client.resource('QuestionnaireResponse').execute('57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca', 'GET')
print(response)
req, err := http.NewRequest("GET", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/QuestionnaireResponse/57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca", nil)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
if method == "POST" {
req.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(req)
unmarshaller, err := jsonformat.NewUnmarshaller(config.TimeZone, config.FhirVersion)
if err != nil {
return nil, err
}
bytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
unmarshal, err := unmarshaller.Unmarshal(bytes)
if err != nil {
return nil, err
}
unmarshal.(*r4pb.ContainedResource).GetQuestionnaireResponse()
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/QuestionnaireResponse/57a1f708-d9cf-4d8c-9f25-b5a450e7f0ca?_format=json';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$questionnaireResponse = new FHIRQuestionnaireResponse(json_decode($res, true));
Search QuestionnaireResponse
curl "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/QuestionnaireResponse?patient.identifier=6226217e&based-on.instantiates-canonical=PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806"
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
Bundle bundle = client.search()
.forResource(QuestionnaireResponse.class)
.where(QuestionnaireResponse.BASED_ON.hasChainedProperty(
CarePlan.INSTANTIATES_CANONICAL.hasId("PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806")))
.and(QuestionnaireResponse.PATIENT.hasChainedProperty(Patient.IDENTIFIER.exactly().identifier("6226217e")))
.returnBundle(Bundle.class)
.execute();
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
var qr = client.Search<QuestionnaireResponse>(new []{"patient.identifier=6226217e", "based-on.instantiates-canonical=97f680b9-e397-4298-8c53-de62a284c806"});
qr.Entry.ForEach(component => Console.WriteLine(component.FullUrl));
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const bundle = await client.request("QuestionnaireResponse?based-on.instantiates-canonical=PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806&patient.identifier=6226217e");
const responses = bundle.entry.map(entry => entry.resource);
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
params = {
'based-on.instantiates-canonical': 'PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806',
'patient.identifier': '6226217e'
}
FHIR::QuestionnaireResponse.search(params)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
response = await client.resources('QuestionnaireResponse').search(based_on.instantiates_canonical='PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806', patient__identifier='6226217e').fetch()
print(response)
req, err := http.NewRequest("GET", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/QuestionnaireResponse?based-on.instantiates-canonical=PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806&patient.identifier=6226217e", nil)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
if method == "POST" {
req.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(req)
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/QuestionnaireResponse?based-on.instantiates-canonical=PlanDefinition/97f680b9-e397-4298-8c53-de62a284c806&patient.identifier=6226217e&_format=json';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$res = curl_exec($ch);
curl_close($ch);
$bundle = new FHIRBundle(json_decode($res, true));
The patient’s filled out questionnaire is immediately visible for the medical professional on Improve applications.
The APIs can be used to add responses to, for example, the patient’s EHR file, or can be used to integrate or create a different dashboard.
Retrieving & Searching QuestionnaireResponse
This endpoint can be used to retrieve a QuestionnaireResponse by id, or search using a small set of parameters. Note when using the search endpoint a Bundle containing the QuestionnaireResponses that match the search criteria will be returned.
The answers of the questionnaire will be encrypted using OpenPGP. Details on decrypting the response can be found in the Decrypting QuestionnaireResponse section.
Creating QuestionnaireResponse(s)
Create QuestionnaireResponses as Bundle
curl -X POST "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Bundle" -H "Content-Type: application/json" --data-binary "@response-bundle.json"
MethodOutcome post(QuestionnaireResponse response) {
var bundle = new Bundle();
bundle.addEntry().setResource(response);
return client.create().resource(bundle).execute();
}
Bundle CreateQuestionnaireResponse(QuestionnaireResponse response)
{
var bundle = new Bundle
{
Entry = new List<Bundle.EntryComponent>
{
new Bundle.EntryComponent
{
Resource = response
}
}
};
return client.Create(bundle);
}
const bundle = await encryptQuestionnaireResponse(questionnaireResponse, practitioners)
const client = new Client();
return client.create(bundle);
FhirClient.new
def createQuestionnaireResponse(response)
bundle = FHIR::Bundle.new
bundle_entry = FHIR::Bundle::Entry.new
bundle_entry.resource = response
bundle.entry = [bundle_entry]
FHIR::Bundle.create(bundle)
end
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
with open('bundle.json', 'r') as file:
bundle_json = json.load(file)
await client.resource('Bundle').save(bundle_json)
file, err := os.Open("bundle.json")
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Bundle", file)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
if method == "POST" {
req.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(req)
$bundle = new FHIRBundle();
$entry = new FHIRBundleEntry();
$entry->setResource($response);
$bundle->setEntry([$entry]);
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Bundle';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($bundle)
]);
$res = curl_exec($ch);
curl_close($ch);
$responseBundle = new FHIRBundle(json_decode($res, true));
Person centered clients need to provide encrypted QuestionnaireResponses to the Open HealthHub platform. There are a couple of requirements for these QuestionnaireResponses when posting them:
meta.profile
should be set tohttps://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/EncryptedQuestionnaireResponse
.identifier
should be set to the identifier as set in the QuestionnaireResponse in theCarePlan.activity
this response fulfills.basedOn
should be set to a CarePlan reference with the correct ID (e.g.CarePlan/123
).source
should reference the Patient (as contained resource). The patient should have one identifier with systemurn:ietf:rfc:3986
. ( can be taken from the originating CarePlan)author
should reference the Practitioner (as contained resource). The Practitioner should contain an identifier with systemurn:ietf:rfc:3986
and usesecondary
.- All answers should be encrypted and the
extension
https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/encryptedAnswers
should be present with an encrypted JSON object containing all answers. Details on encrypting the response can be found in the Encrypting QuestionnaireResponse section.
HTTP Request
Get by ID
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/QuestionnaireResponse/<ID>
Search
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/QuestionnaireResponse?based-on.instantiates-canonical=PlanDefinition/<PROGRAM_UUID>|<PROGRAM_VERSION>&questionnaire=Questionnaire/<MODULE_UUID>|<MODULE_VERSION>&patient.identifier=<PATIENT_ID>&_since=<SINCE_DATE>
Creating single response
POST https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/QuestionnaireResponse
Creating multiple responses
POST https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4
also see transaction requests
URL Parameters
Note that in case of search all parameters are optional, a search without parameters will return all resources that are accessible for your client.
Parameter | Description |
---|---|
ID | UUID of the QuestionnaireResponse to retrieve |
PROGRAM_UUID | (optional) UUID of the program this Questionnaire response belongs to |
PROGRAM_VERSION | (optional) Version of the program this Questionnaire response belongs to, can only be used in combination with PROGRAM_UUID |
MODULE_UUID | (optional) UUID of the Questionnaire that was answered |
MODULE_VERSION | (optional) Version of the Questionnaire that was answered, can only be used in combination with MODULE_UUID |
PATIENT_ID | (optional) the id of the patient that answered the questionnaire in FHIR Token format |
SINCE_DATE | (optional) QuestionnaireResponses that were entered after this date, in ISO 8601 format. For details refer to Times |
Decrypting QuestionnaireResponse
The decrypted JSON with all answers
{
"date_1": [
{
"value": "1981-07-29",
"codes": []
}
],
"photo_1": [
{
"value": "base64EncodedImage",
"codes": []
}
],
"hslider_1": [
{
"value": "5.5",
"codes": []
}
],
"vslider_1": [
{
"value": "-9",
"codes": []
}
],
"formula_1": [
{
"value": "-3.5",
"codes": []
}
],
"number_1": [
{
"value": "42",
"codes": []
}
],
"mchoice_1": [
{
"value": "Raspberry",
"codes": [{
"code": "7658-8",
"system": "https://fhir.loinc.org/CodeSystem/loinc",
"display": "Raspberry BasoBnd Ab Qn",
"version": "2.71"
}]
}
],
"mselect_1": [
{
"value": "Laptop",
"codes": []
},
{
"value": "Smartphone",
"codes": []
}
],
"boolean_1": [
{
"value": "1",
"codes": []
}
],
"open_2": [
{
"value": "It's the best questionnaire I've ever seen!",
"codes": []
}
]
}
The private key used to decrypt the examples from the sandbox
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: OpenPGP.js v4.10.10
Comment: https://openpgpjs.org
xYYEYQeklhYJKwYBBAHaRw8BAQdA7zkG2GrTERRlKqZRhbHEYARkzWjXt4Bj
RBx9PFEfwwb+CQMIwF4UP4mQC0bgQkxklm3EDi6SFHw1kfMzQ+gqpfaepGau
EPjpSqLN48gZJMznHK4UBe0xzu67OP6Y9UyT8SL2dj/gSVcku1MZvPZCEP2Y
Is0uU2FuZGJveCBBUEkyIDxlZGR5K3NhbmRib3gyQG9wZW5oZWFsdGhodWIu
Y29tPsKPBBAWCgAgBQJhB6SWBgsJBwgDAgQVCAoCBBYCAQACGQECGwMCHgEA
IQkQAyVBKg8H7ZgWIQSmxMhcFAXUQBd0uOYDJUEqDwftmLCiAQDVYGQ9eI+J
WL0PYwgrY7StV590qZar9S1dJvmGfkcGAgD/e/EBG7C5+Kd5KO2uCaS2q42B
HKzBUGVRYBjFQ8/SUQDHiwRhB6SWEgorBgEEAZdVAQUBAQdAum8OjYljCvYq
GCKW84Fub4/e/fp3l8dE/oZsrwArMwUDAQgH/gkDCJRAihNl5syQ4C/UKaHt
gM94KCfG48hvgIiOFrBp7ZBkNOl30H0r/mOLD6+BrvMFLHUG3H+OTIwsjDbc
Q5sHAE9tPVDXA98luvBQC/KNu2HCeAQYFggACQUCYQeklgIbDAAhCRADJUEq
DwftmBYhBKbEyFwUBdRAF3S45gMlQSoPB+2Y9IYBAMCPkUQO9r7Ot3nff45i
vThPkehhDk+MDb4zWW3xXVe7AP0ZXv6yBNGlsm2xFyGtVQH30nckhyBeKGcT
CEykt3SCBQ==
=X1eg
-----END PGP PRIVATE KEY BLOCK-----
Load private key:
gpg --import "../../sandbox.key"
privateKey = PGPainless.readKeyRing().secretKeyRing(ClientPerAnswerApplication.class.getResourceAsStream(PRIVATE_KEY_FILE));
keyRingProtector = SecretKeyRingProtector.unlockAllKeysWith(Passphrase.fromPassword(PRIVATE_KEY_PASSPHRASE), privateKey);
var privateKey = File.ReadAllBytes(Path.Combine(Directory.GetCurrentDirectory(), "sandbox.key"));
var pgp = new Openpgp();
pgp.Keys.Add(new Key(privateKey));
pgp.Keys[0].Passphrase = PgpPassphrase;
const keys = await openpgp.key.readArmored(PRIVATE_KEY_FILE);
const key = keys.keys[0];
await key.decrypt(PRIVATE_KEY_PASSPHRASE);
this.decryptedPrivateKey = key;
GPGME::Key.import(File.open('../../sandbox.key'))
gpg = gnupg.GPG()
with open('../../sandbox.key') as file:
gpg.import_keys(file.read(), passphrase='api-sandbox')
privateKey, err := ioutil.ReadFile("../sandbox.key")
$keyASCII = file_get_contents(dirname(__FILE__) . '/../../../../sandbox.key');
$gnupg = new gnupg();
$importKeyResult = $gnupg->import($keyASCII);
$fingerPrint = $importKeyResult['fingerprint'];
$gnupg -> adddecryptkey($fingerPrint, 'api-sandbox');
return $gnupg;
Determine if QuestionnaireResponse is encrypted
jq -r '.extension[] | select(.url="https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/encryptedAnswers") | .valueString'
boolean isEncryptedResponse(QuestionnaireResponse questionnaireResponse) {
return questionnaireResponse.getMeta()
.getProfile()
.stream()
.anyMatch(profile -> "https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/EncryptedQuestionnaireResponse".equals(profile.getValue()));
}
questionnaireResponse.Meta.Profile.Any(profile =>
"https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/EncryptedQuestionnaireResponse".Equals(profile));
isEncryptedResponse(resp) {
return resp.meta.profile.some(profile => profile === 'https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/EncryptedQuestionnaireResponse');
}
response.meta.profile.any? { |profile| profile == 'https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/EncryptedQuestionnaireResponse' }
encryptedProfile = 'https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/EncryptedQuestionnaireResponse'
def is_encrypted(resource: Resource):
encProfile = list(filter(lambda p: encryptedProfile == p, resource.meta.profile))
return len(encProfile) > 0
func isEncrypted(qr *questionnaire_response_go_proto.QuestionnaireResponse) bool {
for _, p := range qr.Meta.Profile {
if p.GetValue() == encryptedProfileUrl {
return true
}
}
return false
}
foreach ($resp->getMeta()->getProfile() as $profile) {
if ($profile->getValue() == 'https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/EncryptedQuestionnaireResponse') {
return true;
}
}
return false;
Decrypt a String value
gpg -d
String decryptValue(String value, String extensionUrl) {
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
.onInputStream(new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)))
.decryptWith(keyRingProtector, privateKey)
.doNotVerify()
.build();
byte[] bytes = decryptionStream.readAllBytes();
decryptionStream.close();
return new String(bytes, StandardCharsets.UTF_8);
}
pgp.Decrypt();
var decrypted = pgp.OutputMessage;
async decryptValue(value) {
const messageObj = await openpgp.message.readArmored(value);
const result = await openpgp.decrypt({message: messageObj, privateKeys: [this.decryptedPrivateKey]});
return result.data;
}
crypto = GPGME::Crypto.new
crypto.decrypt(value, password: private_key_passphrase).to_s
dec = gpg.decrypt(encryptedAnswers[0].valueString)
print(dec)
message, err := helper.DecryptMessageArmored(string(privateKey), []byte("api-sandbox"), armored)
$gnupg->decrypt($value);
All answers in our QuestionnaireResponse are encrypted, so they can't be read unless you have the correct private key to decrypt the responses. The answers are encrypted in 2 separate ways.
Every answer is encrypted in the item -> answer field, using an extension with the prefix
https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/encrypted-
followed by the type of the answer (e.g. decimalType
, stringType
, attachment
).
Because decrypting all answers one-by-one can have a big impact on performance, we also emit all answers in an encrypted JSON object. This
is emitted in an extension at the top level of the QuestionnaireResponse with
url https://api.openhealthhub.com/OpenHealthhub/fhir/4/StructureDefinition/encryptedAnswers
.
All answers are emitted as a JSON object with a value
property that contains the actual value of the answer. If the answer has codes
linked to it (as entered in Improve Designer), they will be put in a codes
array (see the mchoice_1
answer on the right). Most
questionnaire
items will have a one-to-one match between their linkids and the ids for the answer value objects. For result questionnaire items
result_<result_index>_formula_<formula_index>
will be created to indicate first which item (result_<result_index>
) and
second (formula_<formula_index>
)
which result sub-item the answer is for.
Finally, all formula results will also be emitted as formula_<formula_id>
.
For decrypting, the private key shown on the right can be used (passphrase: api-sandbox
).
Some helper functions are displayed here to help you load the private key, determine if a QuestionnaireResponse is encrypted, and decrypt a value.
For a complete example please have a look at the FHIR QuestionnaireResponse Client Examples:
Language |
---|
C# |
Go |
Java |
JavaScript |
PHP |
Python |
Ruby |
shell |
Encrypting QuestionnaireResponse
All answers in our QuestionnaireResponse should be encrypted, so they can't be read unless you have the correct private key to decrypt the responses. The answers should be encrypted in 2 separate ways.
Using our javascript SDK
import {encryptQuestionnaireResponse} from '@openhealthhub/fhir-encrypter';
const questionnaireResponse = {}; // your QuestionnaireResponse
const practitioners = []; // your Practitioners
encryptQuestionnaireResponse(questionnaireResponse, practitioners)
.then(bundle => {
// post('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Bundle', bundle)
})
.catch(error => {
// handle error
});
To help you with creating an encrypted QuestionnaireResponse we offer a npm package with a single function with inputs the QuestionnaireResponse and a list of Practitioners which will output a Bundle that can be posted to our Bundle endpoint.
The following fields should already be set before passing the response through the encrypt function:
identifier
should be set to the identifier as set in the QuestionnaireResponse in theCarePlan.activity
this response fulfills.basedOn
should be set to a CarePlan reference with the correct ID (e.g.CarePlan/123
).source
should reference the Patient (as contained resource). The patient should have one identifier with systemurn:ietf:rfc:3986
. ( can be taken from the originating CarePlan)
The practitioners can be retrieved as part of the CarePlan, as part of a CareTeam, or as a list of Practitioners in a CareTeam.
Encrypting answers yourself
If you want to handle the encryption yourself
Every answer should be encrypted in the item -> answer field, using an extension with the prefix
http://openhealthhub.com/fhir/StructureDefinition/encrypted-
followed by the type of the answer (e.g. decimalType
, stringType
, attachment
).
Because decrypting all answers one-by-one can have a big impact on performance, we also want all answers in an encrypted JSON object. This
should be added as an extension at the top level of the QuestionnaireResponse with
url http://openhealthhub.com/StructureDefinition/encryptedAnswers
.
All answers should be encrypted as a JSON object. The format of this object is as follows:
{ "value": "Raspberry", "codes": [{ "code": "7658-8", "system": "https://fhir.loinc.org/CodeSystem/loinc", "display": "Raspberry BasoBnd Ab Qn", "version": "2.71" }], "text": "Raspberry label" }
Field explanation:
- The
value
property contains a single value of the answer. codes
: If the answer has codes linked to it (as entered in Improve Designer), they should be put in thecodes
array.- The
text
field holds a text representation of the answer if available (e.g. voor 'choice' type questions).
If the answer has multiple values (e.g. in case of a multiple select question) there should be multiple answer objects.
Most questionnaire items should have a one-to-one match between their linkIds and the ids for the answer
value objects. For result questionnaire items result_<result_index>_formula_<formula_index>
should be created to indicate first which
item (result_<result_index>
) and second (formula_<formula_index>
) which result sub-item the answer is for.
Finally, all formula results should also be emitted as formula_<formula_id>
.
The JSON object with all the combined answers should be an object, with a property for every question with key the linkId
of the question.
The value should be an array of answer objects as described above (also in case of single answer).
For a complete example please have a look at the JavaScript FHIR QuestionnaireResponse Client Example:
Subscription
Create Subscription
curl -X POST 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Subscription' \
-H 'Content-Type: application/json' \
-D '{
"resourceType" : "Subscription",
"status": "requested",
"criteria": "Appointment?name=test",
"channel" : {
"type" : "rest-hook",
"endpoint" : "https://your-webhook/endpoint",
"header": ["Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"]
}
}'
Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent()
.setType(Subscription.SubscriptionChannelType.RESTHOOK)
.setHeader(List.of(new StringType("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")))
.setEndpoint("https://your-webhook/endpoint");
Subscription subscription = new Subscription()
.setCriteria("Appointment?name=test")
.setStatus(Subscription.SubscriptionStatus.REQUESTED)
.setChannel(channel);
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
Subscription subscription = client.create()
.resource(subscription)
.execute();
// we use nuget package Hl7.Fhir.R4
var client = new FhirClient("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4");
var resource = new Subscription
{
Status = Subscription.SubscriptionStatus.Requested,
Criteria = "QuestionnaireResponse",
Channel = new Subscription.ChannelComponent()
{
Type = Subscription.SubscriptionChannelType.RestHook,
Header = new List<string>{"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}
}
};
var subscription = client.Create(resource);
Console.Out.WriteLine(subscription.Id);
const client = FHIR.client('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4');
const subscription = client.create({
resourceType: 'Subscription',
criteria: 'Appointment?name=test',
status: 'requested',
channel: {
type: 'rest-hook',
endpoint: 'https://your-webhook/endpoint',
header: ['Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8']
}
});
client = FHIR::Client.new('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4')
FHIR::Model.client = client
subscription = FHIR::Subscription.new
subscription.criteria = 'Appointment?name=test'
subscription.status = 'requested'
subscription.channel = FHIR::Subscription::Channel.new
subscription.channel.type = 'rest-hook'
subscription.channel.endpoint = 'https://your-webhook/endpoint'
subscription.channel.header = ['Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8']
FHIR::Subscription.create(subscription)
client = AsyncFHIRClient(
'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4'
)
with open('subscription.json', 'r') as file:
subJson = json.load(file)
createResponse = await client.execute('Subscription', method='post', data=subJson)
print(createResponse)
file, err := os.Open("subscription/subscription.json")
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Subscription", file)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", "ad880601-b7e6-4d86-901d-b6fca96fc725")
if method == "POST" {
req.Header.Add("Content-Type", "application/json")
}
response, err := http.DefaultClient.Do(req)
$subscription = new FHIRSubscription();
$status = new FHIRSubscriptionStatus(['requested']);
$subscription->setStatus($status);
$channel = new FHIRSubscriptionChannel();
$channelType = new FHIRSubscriptionChannelType();
$channelType->setValue('rest-hook')
$channel->setEndpoint('https://your-webhook/endpoint')->setType($channelType->setValue('rest-hook'))->addHeader(['Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8']);
$subscription->setCriteria('Appointment?name=test')->setChannel($channel);
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Subscription';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($subscription)
]);
$res = curl_exec($ch);
curl_close($ch);
$createdSubscription = new FHIRSubscription(json_decode($res, true));
The endpoint can be used to create, get, search, and delete Subscriptions that trigger a webhook request when a resource that matches the criteria has been created or updated.
Creating a Subscription
To create a Subscription to get notified about changes you'll need to have an endpoint/URL available that can handle the notification requests.
If you want subscriptions to only be valid until a specific time you can specify the end
value of a subscription. This can be
helpful if you specify an authorization token in the headers that has a limited lifetime. Note that you should keep track of expired
subscriptions yourself, we won't create new subscriptions if they expire. No notifications will be
sent for this subscription after the specified timestamp. The status
of the subscription will then also change to off
.
When creating a Subscription you can choose to also retrieve the content with the notification by setting the payload to one of the 2
allowed values [application/fhir+json
, application/fhir+xml
]. If you don't want to receive the content you'll need to execute the
request defined in the criteria
to retrieve the items.
With Payload
If you have set the payload to one of the allowed values, we'll send the notifications with the (related) content of the subscribed resource
in a Bundle of type history
.
Headers
If your server endpoint needs specific headers in the HTTP Post request you can add them in the channel.header
field. This can for
example be used when the endpoint is secured with authentication. You can for example add an Authorization: Bearer <jwttoken>
value to the
headers. When the notification will be posted these headers will be added to the request.
Supported criteria
We currently support subscriptions on PlanDefinition and QuestionnaireResponse:
PlanDefinition
You can register a Subscription on PlanDefinitions to receive notification when new programs for your client are published or if changes
occur to a specific program by adding an _id
query parameter to the criteria
.
When payload
has been set the posted Bundle will contain the PlanDefinition that was changed/created.
QuestionnaireResponse
You can register a Subscription on QuestionnaireResponse to receive notification when new answers for a questionnaire have become available.
Currently, you can register for notifications on all responses or on responses for a specific program/PlanDefinition. To listen for
responses for a specific program, you'll need to add the based-on.instantiates-canonical
query parameter to your criteria and set the
value to the UUID of the program you want to receive notifications for.
When payload
has been set the posted Bundle will contain the QuestionnaireResponse (with encrypted values) and the Questionnaire that
corresponds to this QuestionnaireResponse.
CarePlan
You can register a Subscription on CarePlan to receive notification when a CarePlan is created or updated, or when a questionnaire that is
part of the CarePlan has been filled out.
Currently, you can register for notifications on all CarePlans, CarePlans for a specific program/PlanDefinition, or for a specific CarePlan.
To listen for CarePlans for a specific program, you'll need to add the instantiates-canonical
query parameter to your criteria and set
the value to the UUID of the program you want to receive notifications for. When you want to receive notifications for a specific CarePlan,
you can use the _id
query parameter with the ID of the CarePlan as value.
When payload
has been set the posted Bundle will contain the CarePlan that was updated/created.
Searching a Subscription
Searching a subscription can be done with or without supplying a criteria parameter (see HTTP Requests for examples). A search can return all subscriptions; subscriptions for a specific resource (i.e. PlanDefinition and QuestionnaireResponse); or subscriptions for a specific resource and a specific id.
Deleting a subscription
A Subscription can be deleted by sending a DELETE
request. The subscription to delete is specified by supplying the id
which
is returned when creating or searching a subscription.
HTTP Requests
Create
POST https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Subscription
Search
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Subscription
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Subscription?criteria=PlanDefinition
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Subscription?criteria=QuestionnaireResponse?based-on.instantiates-canonical=f9af7954-af7f-4c16-abde-74ffeea33956
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Subscription?criteria=CarePlan?instantiates-canonical=f9af7954-af7f-4c16-abde-74ffeea33956
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Subscription?criteria=CarePlan?_id=98524685
Read
GET https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Subscription/8d2acacb-0351-42ca-8da6-c4632e20b6f1
Delete
DELETE https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4/Subscription/8d2acacb-0351-42ca-8da6-c4632e20b6f1
Subscription fields
Field | Example value | Description |
---|---|---|
channel.type | rest-hook | the only supported channel type |
channel.header | ["header1:value1", "header2:value2"] | custom headers to put on the webhook request |
channel.endpoint | http://yourserver:8080/yourPostEndpoint | the endpoint that the webhook should post to |
channel.payload | application/fhir+json | if you want to receive the actual content of the resource set this value, otherwise leave it empty |
status | requested | when creating a subscription requested is the only allowed value, other values are 'active' or 'off' |
end | 2024-12-05T09:00:00Z | specifies the time after which no notifications should be sent anymore, can be left empty if no end time required |
criteria | QuestionnaireResponse?based-on.instantiates-canonical=21bf742e-5220-4b17-8eb8-241d20028de1 | for more info see subscription criteria and search |
Batch & Transaction Requests
Execute Batch
curl -X POST "https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4" -H "Content-Type: application/json" --data-binary "@batch.json"
var getCarePlanEntry = new Bundle.BundleEntryComponent().setRequest(
new Bundle.BundleEntryRequestComponent().setMethod(Bundle.HTTPVerb.GET)
.setUrl("/Careplan/123"));
var createCarePlanEntry = new Bundle.BundleEntryComponent().setRequest(
new Bundle.BundleEntryRequestComponent().setMethod(Bundle.HTTPVerb.POST)
.setUrl("/Careplan")).setResource(new CarePlan());
var batchBundle = new Bundle().setType(Bundle.BundleType.BATCH)
.addEntry(getCarePlanEntry)
.addEntry(createCarePlanEntry);
Bundle bundle = new Bundle
{
Type = Bundle.BundleType.Batch,
Entry = new List<Bundle.EntryComponent>()
};
bundle.Entry.Add(new Bundle.EntryComponent
{
Resource = new CarePlan
{
Period = new Period(FhirDateTime.Now(), null),
InstantiatesCanonical = new List<string> { "PlanDefinition/cca2eaf3-03a9-46c0-88c6-e0287917cea6" }
},
Request = new Bundle.RequestComponent
{
Method = Bundle.HTTPVerb.POST,
Url = "CarePlan"
}
});
bundle.Entry.Add(new Bundle.EntryComponent
{
Request = new Bundle.RequestComponent
{
Method = Bundle.HTTPVerb.GET,
Url = "CarePlan/1"
}
});
var result = client.Transaction(bundle);
Console.Out.WriteLine(result);
}
const client = new Client();
return client.create({
resourceType: "Bundle",
type: 'batch',
entry: [{
request: {
method: 'GET',
url: 'CarePlan/123'
}
}, {
request: {
method: 'POST',
url: 'CarePlan'
},
resource: {
type: 'CarePlan',
...content
}
}]
});
careplan = FHIR::CarePlan.new
batch_request = FHIR::Transaction.new
create_request = FHIR::Bundle::Entry::Request.new
create_request.url = 'CarePlan'
create_request.request_method = 'POST'
create_request.entry = careplan
get_request = FHIR::Bundle::Entry::Request.new
get_request.url = 'CarePlan/123'
get_request.request_method = 'GET'
batch_request.entry = [create_request, get_request]
FHIR::Client.post(batch_request)
with open("batch.json", "r") as file:
batch_json = json.load(file)
async with ClientSession() as session:
async with session.post('https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4', data=json.dumps(batch_json)) as resp:
print(await resp.text())
bundle, err := os.Open("batch/batch.json")
if err != nil {
return nil, err
}
marshaller, err := jsonformat.NewMarshaller(jsonformat.STU3, "", "", jsonformat.ParserConfig{})
if err != nil {
return nil, err
}
bundleBytes, err := marshaller.MarshalResource(bundle)
if err != nil {
return nil, err
}
resp, err := client.Post("https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4", bundleBytes)
if err != nil {
return nil, err
}
unmarshalledBundle := bundle_and_contained_resource_go_proto.Bundle{}
if err := proto.Unmarshal(resp, unmarshalledBundle); err != nil {
return nil, err
}
return unmarshalledBundle, nil
$transactionBundle = array(
'resourceType' => 'Bundle',
'type' => 'transaction',
'entry' => array(
array(
'request' => array(
'method' => 'POST',
'url' => 'CarePlan',
),
'resource' => $this->createCarePlanResource()->getValues(),
),
array(
'request' => array(
'method' => 'GET',
'url' => 'CarePlan/1',
),
),
)
);
$url = 'https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($careplan)
]);
$res = curl_exec($ch);
curl_close($ch);
$createdCarePlan = new FHIRBundle(json_decode($res, true));
Using FHIR's batch/transaction interactions you can execute multiple actions in a single HTTP request.
We currently support batch
requests for all our supported resources. Please note that for batch requests there should not be any
dependencies between actions in the request. Every action will be handled as a single standalone action.
For transaction
requests we currently only support posting a set of QuestionnaireResponses.
HTTP Request
POST https://api.openhealthhub.com/OpenHealthhub/fhir-sandbox/4