Embedding a Zus Form

Overview

This document covers how to embed Zus Forms into an existing application.

📘

Note that this document often refers to “sandbox” in URLs. When integrating with a production environment you should remove all references to sandbox, so forms.sandbox.zusapi.com would become forms.zusapi.com.

Embedding a Form

All Zus Forms are web pages served at https://forms.sandbox.zusapi.com . You must specify the form you want by providing the form ID in the URL path and you can specify an optional theme override using the theme query string parameter parameter. So the URL https://forms.sandbox.zusapi.com/123456789?theme=default would take you to the form with ID 123456789 with the default theming applied.

Getting a list of available forms

Calling GET <https://api.sandbox.zusapi.com/forms-data/forms> will retrieve all the forms available to your builder. The id property returned by this endpoint supplies the form ID that you must pass via the aforementioned query parameter.

User authentication

Zus Forms require the user to be authenticated by Zus. If you are using Zus as an identity provider (IdP) then you can use the Zus Access Token JWT associated with the active user. If you are using an OIDC compliant IdP then you can use your IdP’s identity token in a token exchange call to POST <https://api.sandbox.zusapi.com/auth/token>. The response body of the token exchange contains a Zus Access Token JWT that you can use the same way as if you were using Zus as an IdP.

📘

Configuring token exchange is not in scope for this document. If you need to set up token exchange then please complete this support ticket form and select "Account log-in/authentication" for the question "What portion of the Zus Developer Sandbox does your support ticket relate to?"

Embedding the form

In a web application you must embed Zus Forms using an iframe like so:

<iframe id="zus-form" src="https://forms.sandbox.zusapi.com/123456789" />

In a React Native app you must embed Zus Forms using a WebView like so:

<WebView source={{uri: "<https://forms.sandbox.zusapi.com/123456789"}}/>>

Passing the Zus Access Token to the embedded form

If you embed the form as above then you should see a page with the message “Authorization Required”. You must now pass a Zus Access Token to the form. This communication is handled using the postMessage API. The Zus Form will let your application know it is ready to receive a token when it posts a message with the type of "ready". At that point your application can post a message back with the type of "setToken" and a body object with a token property containing the Zus Access Token JWT.

❗️

While it is safe to post a message containing the JWT to the Zus Form, you should never log your Zus Access Token and neither will Zus.

The easiest way to handle all of this is to have an event listener in your application waiting for the “ready” message and responding with your access token. In a web application that would be implemented like so:

window.addEventListener('message', ({data}) => {
  if (data.type == 'ready') {
    document.getElementById("zus-form")
      .contentWindow
      .postMessage(
        {type: 'setToken', body: { token: YOUR_ZUS_ACCESS_TOKEN } }, 'https://forms.sandbox.zusapi.com'
      );
  }
});

In a React Native app the messaging mechanism can be circumvented and you can write the access token directly to the window object. Note that the onMessage prop is required for the injected JS to work (see the third answer in this StackOverflow post - https://stackoverflow.com/questions/46690261/injectedjavascript-is-not-working-in-webview-of-react-native ).

const injectedJS = `window.accessToken = "${YOUR_ZUS_ACCESS_TOKEN}";`;

<WebView
  source={{uri: "https://forms.sandbox.zusapi.com/123456789"}}
  injectedJavaScript={injectedJS}
  onMessage={(e) => {}} 
/>

Other Messages

In addition to alerting the parent application that the Zus Form is ready to receive a token, the Zus Form will also message the parent when:

  • a new form submission is started (message type: formSubmissionCreated)
  • when an existing form submission has been resumed (message type: formSubmissionResumed)
  • when a form submission has been completed (message type: formSubmissionCompleted)
  • when a form submission has been canceled (message type: formSubmissionCanceled)*
  • when the user has clicked the "Done" button on the confirmation page (message type formSubmissionAcknowledged)*
  • when the user navigates to review all responses on the form after submission (message type: viewResponsesTriggered)*

*Note that the last 3 message types do not contain a message body, just the type.

Each message contains a body with a questionnaireResponseId containing the UUID for the QuestionnaireResponse FHIR resource created in ODS. The body also contains a formId to ensure that the response is associated with the form that you expected.

Specifying the patient context

Today all Zus Forms are filled out with data about a patient. In many cases the patient will be the user filling out the form in which case the FHIR resource ID for that patient will be in the JWT claims. The Zus backend will validate the JWT and use that claim to associate the user with any downstream data.

In some cases a non-patient user (eg. a provider or other care team member) may be filling out a form about a patient. In that case you must set the patient query parameter with a patient FHIR resource ID when embedding the form. If a user accesses a form with no patient context (ie. they are not a patient themselves and a patient ID was not supplied in the URL) then you will get an error message.

Displaying a Form Response

In order to view the response of a given form you can use the URL <https://forms.sandbox.zusapi.com/{formId}/responses/{questionnaireResponseId}>. This will display a single page version of the form containing all of the user responses consistent with the original theme of the form displayed to the user. This can otherwise be embedded into a web or mobile app the same way as the forms that are filled out by users.

Data Generated

When a user first opens a form the Zus backend will immediately write a QuestionnaireResponse FHIR resource associated with the patient context for the form. The QuestionnaireResponse.status field is marked as in-progress and answers to the form are progressively written to the item array as the user fills out the form (requests are sent to the Zus backend upon update of each question in the form). If the user leaves the form and comes back (on the same machine) then the form will be reloaded in its prior draft state. This relies on storing the “active forms” for the current user/patient/form context in localStorage. If the user cancels the form then the existing QuestionnaireResponse is abandoned and the status is left as in-progress and a new one is created. If the user submits the form then the QuestionnaireResponse.status field is set to completed and the FHIR “extraction” process is begun. Any structured data submitted in the form is then written to the appropriate FHIR resources associated with the patient, though limited to whatever FHIR resources Zus supports in extraction and are configured for the form.

📘

Extraction is a nascent and complex feature that is currently out of scope for this document.

📘

You can access the generated forms data by querying ODS directly like GET <https://api.sandbox.zusapi.com/fhir/QuestionnaireResponse?subject=Patient/PATIENT_FHIR_RESOURCE_ID>. There are also options for subscribing to updates in ODS to support backend processes that trigger based on form submission.

Reference Code

Below are two more complete examples for using an iframe and using a React Native webview.

iframe

<html>
    <script>

        // you must implement this to retrieve a Zus Access Token for the current user
        const getAccessToken = () => {
            return "ZUS_ACCESS_TOKEN";
        }

        window.addEventListener('message', ({data}) => {
            if (data.type == 'ready') {
                document.getElementById("zus-form")
                .contentWindow
                .postMessage(
                    {type: 'setToken', body: { token: getAccessToken()}}, 'https://forms.sandbox.zusapi.com'
                );
            }
        });
        
    </script>
    <body>
        <iframe id="zus-form" height="100%" width="100%" src="https://forms.sandbox.zusapi.com/formId" />
    </body>    
</html>

WebView

import React from "react";
import WebView from "react-native-webview";

// you must implement this to retrieve a Zus Access Token for the current user
const getAccessToken = () => {
  return "ZUS_ACCESS_TOKEN";
}

const App = () => {
  const accessToken = getAccessToken();
  const injectedJS = `(function() { window.accessToken = "${accessToken}"; })()`;
  return (
    <WebView
      source={{
        uri: "https://forms.sandbox.zusapi.com/formId",
      }}
      injectedJavaScriptBeforeContentLoaded={injectedJS}         
      onMessage={(e) => {}} 
    ></WebView>
  );
};

export default App;