Case Study - JITAI Notifications

You can use MyDataHelps APIs to provide Just-in-Time Adaptive Interventions (JITAI), such as notifications, customized for each participant.

Consider that your objective is to implement an interventional study designed to nudge participants with messages that assist in blood pressure management. These notifications are scheduled to be sent at four specific intervals: morning, lunch, afternoon, and evening. Participants have the autonomy to choose their preferred times for these intervals. The goal is to maintain an average delivery rate of 1.5 messages per participant per day. To tailor the timing and content of these messages, you use variables such as the time of day, the participant’s recent blood pressure readings, number of steps taken daily, and a medication adherence score.

Prerequisites

Before you get started, you’ll need a few things.

Project Setup

You will need to make sure that your MyDataHelps project has the following configured:

  1. We are going to be using Omron to collect blood pressure information from participants, and Fitbit to record their daily steps, so make sure you enable these data types for your project.
  2. Create 4 custom fields to store a participant’s preferred notification times: MorningTime, LunchTime, AfternoonTime and EveningTime. These will hold values such as 8 AM or 6 PM and will be set from a survey that you will send to your participants to ask for their preferred notification times.
  3. Design your notification bank. We recommend using prefixes in your notification identifiers to distinguish between different categories of messages such as myproj-morning-meds-4 or myproj-afternoon-activity-5. The notification bank will consist of all of the messages that you might want to send, as well as the criteria you are going to want to use to determine when to send which message, such as time of day, a participant’s past week’s data or how they’ve answered specific recent surveys. Once your notification bank is ready you may load it into your MyDataHelps project.
  4. Create a service account.
  5. In MyDataHelpsDesigner, create a segment for all of the participants that are ready to receive notifications (for example everyone that is in the phase active and has the 4 times - morning, lunch, afternoon and evening - set and is in the Intervention cohort).

NPM Packages

To be able to use all the code in this tutorial, you will need to install the following NPM packages:

NPM Packages
npm install uuid jsonwebtoken luxon
npm install uuid jsonwebtoken luxon

Project Variables

The code examples below reference several project-specific variables:

  • projectID can be found in your project settings. See Project Identifiers.
  • segmentID is visible when editing a segment. See Editing or Deleting a Segment.
  • baseUrl is the base URL for all MyDataHelps API endpoints: https://designer.mydatahelps.org

Implementation

In this example we are going to implement a cloud function (for instance AWS Lambda in Node.js) that is going to interact with the MyDataHelps APIs to dispatch a diverse array of notifications determined by complex criteria. This function is going to run every minute during the day.

Step 1 - Get a service access token

Get a service access token to authenticate with the MyDataHelps APIs. You will need to do this every time the cloud function runs.

This token be referenced in the code examples as ${serviceAccessToken}.

Step 2 - Query the relevant participants

Use the MyDataHelps Participant API to query for all of the participants in the segment you configured.

Query Segment Participants
let serviceHeaders = {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'Authorization': `Bearer ${serviceAccessToken}`
};

const projectId = '1cd23328-5964-46f4-a918-9f9331e09810';
const segmentId = 'a22bffc3-66c0-44bc-92b7-504e09b16bf0';

let allParticipants = [];
let participantPage;
let pageNum = 0;
do {
    participantPage = await fetch(`${baseUrl}/api/v1/administration/projects/${projectId}/participants?segmentId=${segmentId}&pageNumber=${pageNum}&pageSize=100`, {
        method: 'get',
        headers: serviceHeaders
    }).then(res => res.json());
    allParticipants = allParticipants.concat(participantPage.participants);
    pageNum += 1;
} while (allParticipants.length < participantPage.totalParticipants);

console.log(allParticipants);
let serviceHeaders = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': `Bearer ${serviceAccessToken}` }; const projectId = '1cd23328-5964-46f4-a918-9f9331e09810'; const segmentId = 'a22bffc3-66c0-44bc-92b7-504e09b16bf0'; let allParticipants = []; let participantPage; let pageNum = 0; do { participantPage = await fetch(`${baseUrl}/api/v1/administration/projects/${projectId}/participants?segmentId=${segmentId}&pageNumber=${pageNum}&pageSize=100`, { method: 'get', headers: serviceHeaders }).then(res => res.json()); allParticipants = allParticipants.concat(participantPage.participants); pageNum += 1; } while (allParticipants.length < participantPage.totalParticipants); console.log(allParticipants);

Segment Participants
[
  {
    id: 'e5d6b29f-1319-4faf-9573-1bc00060d1d4',
    enrolled: true,
    invitationStatus: null,
    insertedDate: '2024-01-10T20:05:14.96+00:00',
    enrollmentDate: '2024-01-10T20:05:15.392+00:00',
    viewParticipantRecordLink: null,
    accountEmail: 'participant1@example.com',
    customFields: {
      AfternoonTime: '4:00 PM',
      Cohort: 'Intervention',
      DinnerTime: '6:00 PM',
      LunchTime: '12:00 PM',
      MorningTime: '8:00 AM',
      Phase: 'active',
      // ...
    },
    ...
  },
  {
    id: '22f1476b-a186-4aea-92e9-b30c45794a6b',
    enrolled: true,
    invitationStatus: null,
    insertedDate: '2024-01-10T20:02:32.317+00:00',
    enrollmentDate: '2024-01-11T14:23:58.924+00:00',
    viewParticipantRecordLink: null,
    accountEmail: 'participant2@example.com',
    customFields: {
      AfternoonTime: '3:00 PM',
      Cohort: 'Intervention',
      DinnerTime: '6:00 PM',
      LunchTime: '12:00 PM',
      MorningTime: '8:00 AM',
      Phase: 'active',
      // ...
    },
   ...
  },
  // ...
]
[ { id: 'e5d6b29f-1319-4faf-9573-1bc00060d1d4', enrolled: true, invitationStatus: null, insertedDate: '2024-01-10T20:05:14.96+00:00', enrollmentDate: '2024-01-10T20:05:15.392+00:00', viewParticipantRecordLink: null, accountEmail: 'participant1@example.com', customFields: { AfternoonTime: '4:00 PM', Cohort: 'Intervention', DinnerTime: '6:00 PM', LunchTime: '12:00 PM', MorningTime: '8:00 AM', Phase: 'active', // ... }, ... }, { id: '22f1476b-a186-4aea-92e9-b30c45794a6b', enrolled: true, invitationStatus: null, insertedDate: '2024-01-10T20:02:32.317+00:00', enrollmentDate: '2024-01-11T14:23:58.924+00:00', viewParticipantRecordLink: null, accountEmail: 'participant2@example.com', customFields: { AfternoonTime: '3:00 PM', Cohort: 'Intervention', DinnerTime: '6:00 PM', LunchTime: '12:00 PM', MorningTime: '8:00 AM', Phase: 'active', // ... }, ... }, // ... ]

Next, you can filter down the participants to only those with a notification preference time in the previous minute (notification preferences are stored in the MorningTime, LunchTime, AfternoonTime, and DinnerTime custom fields).

Filter Participants
allParticipants = allParticipants.filter(p => isTimeWithinLastMinute(p.customFields.MorningTime, p.demographics.timeZone)
    || isTimeWithinLastMinute(p.customFields.LunchTime, p.demographics.timeZone)
    || isTimeWithinLastMinute(p.customFields.AfternoonTime, p.demographics.timeZone)
    || isTimeWithinLastMinute(p.customFields.DinnerTime, p.demographics.timeZone));

// ...

function isTimeWithinLastMinute(timeStr, timeZone) {

    const inputTime = DateTime.fromFormat(timeStr, 'h:mm a', { zone: timeZone }).set({
        year: DateTime.local().year,
        month: DateTime.local().month,
        day: DateTime.local().day
    });
    const currentTime = DateTime.local().setZone(timeZone);

    const diff = Math.abs(currentTime.diff(inputTime, 'minutes').minutes);

    return diff <= 1;
}
allParticipants = allParticipants.filter(p => isTimeWithinLastMinute(p.customFields.MorningTime, p.demographics.timeZone) || isTimeWithinLastMinute(p.customFields.LunchTime, p.demographics.timeZone) || isTimeWithinLastMinute(p.customFields.AfternoonTime, p.demographics.timeZone) || isTimeWithinLastMinute(p.customFields.DinnerTime, p.demographics.timeZone)); // ... function isTimeWithinLastMinute(timeStr, timeZone) { const inputTime = DateTime.fromFormat(timeStr, 'h:mm a', { zone: timeZone }).set({ year: DateTime.local().year, month: DateTime.local().month, day: DateTime.local().day }); const currentTime = DateTime.local().setZone(timeZone); const diff = Math.abs(currentTime.diff(inputTime, 'minutes').minutes); return diff <= 1; }

Step 3 - Query the required input data for each participant

At this point we already have the participants and all of their custom fields. We will then need to query each participant’s recent blood pressures (Omron) and daily steps (Fitbit) to determine their intervention scenario. We iterate through all of the participants, and for each participant, we query for the Omron blood pressure data and Fitbit daily steps for the past week.

Query Omron Blood Pressure Device Data Points
const weekAgo = DateTime.local().setZone(participant.demographics.timeZone).minus({ days: 7 });

let bloodPressureQueryResponse = await fetch(`${baseUrl}/api/v1/administration/projects/${projectId}/devicedatapoints?participantIdentifier=${participant.participantIdentifier}&namespace=Omron&type=BloodPressureDiastolic,BloodPressureSystolic&observedAfter=${weekAgo}`, {
    method: 'get',
    headers: serviceHeaders
}).then(res => res.json());

let bloodPressureDeviceDataPoints = bloodPressureQueryResponse.deviceDataPoints;
const weekAgo = DateTime.local().setZone(participant.demographics.timeZone).minus({ days: 7 }); let bloodPressureQueryResponse = await fetch(`${baseUrl}/api/v1/administration/projects/${projectId}/devicedatapoints?participantIdentifier=${participant.participantIdentifier}&namespace=Omron&type=BloodPressureDiastolic,BloodPressureSystolic&observedAfter=${weekAgo}`, { method: 'get', headers: serviceHeaders }).then(res => res.json()); let bloodPressureDeviceDataPoints = bloodPressureQueryResponse.deviceDataPoints;

Omron Blood Pressure Device Data Points
[{
    id: 'b1d20fd5-f437-ee11-aacb-0afb9334277d',
    namespace: 'Omron',
    deviceDataContextID: '95d20fd5-f437-ee11-aacb-0afb9334277d',
    insertedDate: '2023-08-11T03:12:00.973Z',
    modifiedDate: '2023-08-11T03:12:00.973Z',
    participantID: 'e5d6b29f-1319-4faf-9573-1bc00060d1d4',
    participantIdentifier: '77080fc6-fefa-436c-8660-a192fedcc83c',
    identifier: '1691718936000',
    type: 'BloodPressureDiastolic',
    value: '64',
    units: 'mmHg',
    properties: {},
    source: null,
    startDate: '2023-08-10T21:55:36-04:00',
    observationDate: '2023-08-10T21:55:36-04:00'
},
{
    id: 'b0d20fd5-f437-ee11-aacb-0afb9334277d',
    namespace: 'Omron',
    deviceDataContextID: '95d20fd5-f437-ee11-aacb-0afb9334277d',
    insertedDate: '2023-08-11T03:12:00.97Z',
    modifiedDate: '2023-08-11T03:12:00.97Z',
    participantID: 'e5d6b29f-1319-4faf-9573-1bc00060d1d4',
    participantIdentifier: '77080fc6-fefa-436c-8660-a192fedcc83c',
    identifier: '1691718936000',
    type: 'BloodPressureSystolic',
    value: '103',
    units: 'mmHg',
    properties: {},
    source: null,
    startDate: '2023-08-10T21:55:36-04:00',
    observationDate: '2023-08-10T21:55:36-04:00'
},
// ...
]
[{ id: 'b1d20fd5-f437-ee11-aacb-0afb9334277d', namespace: 'Omron', deviceDataContextID: '95d20fd5-f437-ee11-aacb-0afb9334277d', insertedDate: '2023-08-11T03:12:00.973Z', modifiedDate: '2023-08-11T03:12:00.973Z', participantID: 'e5d6b29f-1319-4faf-9573-1bc00060d1d4', participantIdentifier: '77080fc6-fefa-436c-8660-a192fedcc83c', identifier: '1691718936000', type: 'BloodPressureDiastolic', value: '64', units: 'mmHg', properties: {}, source: null, startDate: '2023-08-10T21:55:36-04:00', observationDate: '2023-08-10T21:55:36-04:00' }, { id: 'b0d20fd5-f437-ee11-aacb-0afb9334277d', namespace: 'Omron', deviceDataContextID: '95d20fd5-f437-ee11-aacb-0afb9334277d', insertedDate: '2023-08-11T03:12:00.97Z', modifiedDate: '2023-08-11T03:12:00.97Z', participantID: 'e5d6b29f-1319-4faf-9573-1bc00060d1d4', participantIdentifier: '77080fc6-fefa-436c-8660-a192fedcc83c', identifier: '1691718936000', type: 'BloodPressureSystolic', value: '103', units: 'mmHg', properties: {}, source: null, startDate: '2023-08-10T21:55:36-04:00', observationDate: '2023-08-10T21:55:36-04:00' }, // ... ]
Query Fitbit Daily Steps
let dailyStepsQueryResponse = await fetch(`${baseUrl}/api/v1/administration/projects/${projectId}/devicedatapoints?participantIdentifier=${p.participantIdentifier}&namespace=Fitbit&type=Steps&observedAfter=${weekAgo}`, {
    method: 'get',
    headers: serviceHeaders
}).then(res => res.json());

let dailySteps = dailyStepsQueryResponse.deviceDataPoints;
let dailyStepsQueryResponse = await fetch(`${baseUrl}/api/v1/administration/projects/${projectId}/devicedatapoints?participantIdentifier=${p.participantIdentifier}&namespace=Fitbit&type=Steps&observedAfter=${weekAgo}`, { method: 'get', headers: serviceHeaders }).then(res => res.json()); let dailySteps = dailyStepsQueryResponse.deviceDataPoints;

Fitbit Daily Steps Device Data Points
[{
    id: '12b7e011-b590-ee11-aad0-0afb9334277d',
    namespace: 'Fitbit',
    deviceDataContextID: null,
    insertedDate: '2023-12-02T01:49:50.17Z',
    modifiedDate: '2023-12-02T01:49:50.17Z',
    participantID: 'e5d6b29f-1319-4faf-9573-1bc00060d1d4',
    participantIdentifier: '77080fc6-fefa-436c-8660-a192fedcc83c',
    identifier: null,
    type: 'Steps',
    value: '4487',
    units: null,
    properties: {
      DataLakeUrl: 'https://pep-device-data-prod.s3.amazonaws.com/fitbit-activities-day/155babd3-2551-ec11-aab5-0afb9334277d/activities_date_2023-12-01.json'
    },
    source: null,
    startDate: '2023-12-01T00:00:00+00:00',
    observationDate: '2023-12-02T00:00:00+00:00'
},
{
    id: 'a69d6951-e98f-ee11-aace-0afb9334277d',
    namespace: 'Fitbit',
    deviceDataContextID: null,
    insertedDate: '2023-12-01T01:31:18.04Z',
    modifiedDate: '2023-12-01T01:31:18.04Z',
    participantID: 'e5d6b29f-1319-4faf-9573-1bc00060d1d4',
    participantIdentifier: '77080fc6-fefa-436c-8660-a192fedcc83c',
    identifier: null,
    type: 'Steps',
    value: '7462',
    units: null,
    properties: {
      DataLakeUrl: 'https://pep-device-data-prod.s3.amazonaws.com/fitbit-activities-day/155babd3-2551-ec11-aab5-0afb9334277d/activities_date_2023-11-30.json'
    },
    source: null,
    startDate: '2023-11-30T00:00:00+00:00',
    observationDate: '2023-12-01T00:00:00+00:00'
},
// ...
]
[{ id: '12b7e011-b590-ee11-aad0-0afb9334277d', namespace: 'Fitbit', deviceDataContextID: null, insertedDate: '2023-12-02T01:49:50.17Z', modifiedDate: '2023-12-02T01:49:50.17Z', participantID: 'e5d6b29f-1319-4faf-9573-1bc00060d1d4', participantIdentifier: '77080fc6-fefa-436c-8660-a192fedcc83c', identifier: null, type: 'Steps', value: '4487', units: null, properties: { DataLakeUrl: 'https://pep-device-data-prod.s3.amazonaws.com/fitbit-activities-day/155babd3-2551-ec11-aab5-0afb9334277d/activities_date_2023-12-01.json' }, source: null, startDate: '2023-12-01T00:00:00+00:00', observationDate: '2023-12-02T00:00:00+00:00' }, { id: 'a69d6951-e98f-ee11-aace-0afb9334277d', namespace: 'Fitbit', deviceDataContextID: null, insertedDate: '2023-12-01T01:31:18.04Z', modifiedDate: '2023-12-01T01:31:18.04Z', participantID: 'e5d6b29f-1319-4faf-9573-1bc00060d1d4', participantIdentifier: '77080fc6-fefa-436c-8660-a192fedcc83c', identifier: null, type: 'Steps', value: '7462', units: null, properties: { DataLakeUrl: 'https://pep-device-data-prod.s3.amazonaws.com/fitbit-activities-day/155babd3-2551-ec11-aab5-0afb9334277d/activities_date_2023-11-30.json' }, source: null, startDate: '2023-11-30T00:00:00+00:00', observationDate: '2023-12-01T00:00:00+00:00' }, // ... ]

Step 4 - Construct your notification bank metadata

The collection of notifications you possess may involve intricate configurations. Some notifications might be designed to be delivered to participants exclusively under precise conditions. During this step, we will append detailed context to each notification to delineate the conditions under which each should be transmitted, for example, if a particular notification should only be sent in the mornings, or if it is related to a physical activity nudge.

Define Notification Bank
let notificationBank = [{
    notificationIdentifier: "project-meds-user-morning-6",
    morning: true,
    afternoon: false,
    lunch: true,
    dinner: false,
    user: true,
    standard: false,
    activity: false,
    planning: false
}, {
    notificationIdentifier: "project-physical-planning-7",
    morning: false,
    afternoon: false,
    lunch: false,
    dinner: true,
    user: false,
    standard: false,
    activity: false,
    planning: true
},
// ...
];
let notificationBank = [{ notificationIdentifier: "project-meds-user-morning-6", morning: true, afternoon: false, lunch: true, dinner: false, user: true, standard: false, activity: false, planning: false }, { notificationIdentifier: "project-physical-planning-7", morning: false, afternoon: false, lunch: false, dinner: true, user: false, standard: false, activity: false, planning: true }, // ... ];

Step 5 - Implement the business logic.

Below, we implement a specific condition under which to send a participant notifications. Your real use case scenario might contain a few or many of such conditions.

In the condition we’ve selected, you want to send notifications to participants with a blood pressure measurement of over 130/80 in the past week, with a medication adherence score of less than 21 on the H-SCALE and that have not met their step goal during the previous week. For these participants, you want to send a message with a 37.5% probability (1.5 avg messages per day divided by 4 decision points). Half the time you want these messages to be medication adherence messages and half the time you want them to be physical activity messages. For medication adherence messages, you want these to be user generated 67% of the time and standard 33% of the time. For the physical activity messages, you want these to be activity suggestions 33% of the time and planning suggestions 67% of the time.

This condition and accompanying probabilities are summarized in the table below:

Example: Condition 7


Condition Criteria

    * BP is above 130/80 * Medication adherence for last week is <21 on the H-SCALE * Step goal for last week was not met

Total message frequency: avg. 1.5 msg/day

Message Send Probabilities
    * No message (.625) * Send Message (.375)
      * Medication adherence messages (.375 * .67 = .2513)
        * User generated (.2513 * .67 = .1684) * Standard (.2513 * .33 = .0829)
      * Physical activity messages (.375 * .33 = .1238)
        * Activity suggestions (.1238 * .33 = .0409) * Planning suggestions (.1238 * .67 = .0829)
Condition 7 Implementation
let timeOfDay = isTimeWithinLastMinute(participant.customFields.MorningTime, participant.demographics.timeZone) ? "morning" :
    isTimeWithinLastMinute(participant.customFields.LunchTime, participant.demographics.timeZone) ? "lunch" :
        isTimeWithinLastMinute(participant.customFields.AfternoonTime, participant.demographics.timeZone) ? "afternoon" :
            isTimeWithinLastMinute(participant.customFields.DinnerTime, participant.demographics.timeZone) ? "dinner" : "error";

if (
    bloodPressureDeviceDataPoints.some(d => (d.type === "BloodPressureSystolic" && parseInt(d.value) > 130) || (d.type === "BloodPressureDiastolic" && parseInt(d.value) > 80))
    && parseInt(participant.customFields.MedicationAdherenceLastWeek) < 21
    && dailyStepsDeviceDataPoints.map(ddp => parseInt(ddp.value)).reduce((a, b) => a + b, 0) / dailyStepsDeviceDataPoints.length < parseInt(participant.customFields.StepsGoalLastWeek)
) {
    // 1st dice roll - 66% meds 33% physical activity
    let firstDiceRoll = Math.random() <= 0.666666 ? "meds" : "physical";

    let secondDiceRoll;
    if (firstDiceRoll === "meds") {
        // 2nd dice roll for meds - 66% user 33% standard
        secondDiceRoll = Math.random() <= 0.666666 ? "user" : "standard";
    }
    else {
        // 2nd dice roll for physical activity - 66% planning 33% activity
        secondDiceRoll = Math.random() <= 0.666666 ? "planning" : "activity";
    }

    // 3rd dice roll - randomization - send or don't send
    let thirdDiceRoll = Math.random() <= 0.375 ? "1" : "0";

    // Select which notification to send from the ones that qualify
    let qualifyingNotifications = notificationBank.filter(n => n[timeOfDay] && n[firstDiceRoll] && n[secondDiceRoll]);
    let selectedNotification = qualifyingNotifications[Math.floor(Math.random() * qualifyingNotifications.length)];

    let diceRoll = `${firstDiceRoll}-${secondDiceRoll}-${thirdDiceRoll}`;

    deviceDataPointsToPersist.push({
        participantIdentifier: participant.participantIdentifier,
        type: `${timeOfDay}NotificationDiceRoll`,
        observationDate: DateTime.local().toISO(),
        value: diceRoll
    });

    if (thirdDiceRoll === "1") {
        notificationsToSend.push({
            participantIdentifier: participant.participantIdentifier,
            notificationIdentifier: selectedNotification.notificationIdentifier
        });
    }
}
let timeOfDay = isTimeWithinLastMinute(participant.customFields.MorningTime, participant.demographics.timeZone) ? "morning" : isTimeWithinLastMinute(participant.customFields.LunchTime, participant.demographics.timeZone) ? "lunch" : isTimeWithinLastMinute(participant.customFields.AfternoonTime, participant.demographics.timeZone) ? "afternoon" : isTimeWithinLastMinute(participant.customFields.DinnerTime, participant.demographics.timeZone) ? "dinner" : "error"; if ( bloodPressureDeviceDataPoints.some(d => (d.type === "BloodPressureSystolic" && parseInt(d.value) > 130) || (d.type === "BloodPressureDiastolic" && parseInt(d.value) > 80)) && parseInt(participant.customFields.MedicationAdherenceLastWeek) < 21 && dailyStepsDeviceDataPoints.map(ddp => parseInt(ddp.value)).reduce((a, b) => a + b, 0) / dailyStepsDeviceDataPoints.length < parseInt(participant.customFields.StepsGoalLastWeek) ) { // 1st dice roll - 66% meds 33% physical activity let firstDiceRoll = Math.random() <= 0.666666 ? "meds" : "physical"; let secondDiceRoll; if (firstDiceRoll === "meds") { // 2nd dice roll for meds - 66% user 33% standard secondDiceRoll = Math.random() <= 0.666666 ? "user" : "standard"; } else { // 2nd dice roll for physical activity - 66% planning 33% activity secondDiceRoll = Math.random() <= 0.666666 ? "planning" : "activity"; } // 3rd dice roll - randomization - send or don't send let thirdDiceRoll = Math.random() <= 0.375 ? "1" : "0"; // Select which notification to send from the ones that qualify let qualifyingNotifications = notificationBank.filter(n => n[timeOfDay] && n[firstDiceRoll] && n[secondDiceRoll]); let selectedNotification = qualifyingNotifications[Math.floor(Math.random() * qualifyingNotifications.length)]; let diceRoll = `${firstDiceRoll}-${secondDiceRoll}-${thirdDiceRoll}`; deviceDataPointsToPersist.push({ participantIdentifier: participant.participantIdentifier, type: `${timeOfDay}NotificationDiceRoll`, observationDate: DateTime.local().toISO(), value: diceRoll }); if (thirdDiceRoll === "1") { notificationsToSend.push({ participantIdentifier: participant.participantIdentifier, notificationIdentifier: selectedNotification.notificationIdentifier }); } }

Step 6 - Record the dice rolls and send the notifications

The final step left to do is to persist the dice rolls via the Persist Device Data API and send the notifications via the Bulk Send Notifications API.

Persist Dice Rolls and Send the Notifications
let persistDiceRollsResponse = await fetch(`${baseUrl}/api/v1/administration/projects/${projectId}/devicedatapoints`, {
    method: 'post',
    headers: serviceHeaders,
    body: JSON.stringify(deviceDataPointsToPersist)
}).then(res => res.json());

let sendNotificationsResponse = await fetch(`${baseUrl}/api/v1/administration/projects/${projectId}/notifications`, {
    method: 'post',
    headers: serviceHeaders,
    body: JSON.stringify(notificationsToSend)
}).then(res => res.json());
let persistDiceRollsResponse = await fetch(`${baseUrl}/api/v1/administration/projects/${projectId}/devicedatapoints`, { method: 'post', headers: serviceHeaders, body: JSON.stringify(deviceDataPointsToPersist) }).then(res => res.json()); let sendNotificationsResponse = await fetch(`${baseUrl}/api/v1/administration/projects/${projectId}/notifications`, { method: 'post', headers: serviceHeaders, body: JSON.stringify(notificationsToSend) }).then(res => res.json());