Participant management
These are the endpoints staff use to run a trial's participants: registering them, keeping their details, putting them in groups and sites, messaging them, and everything that hangs off a participant record — notes, events, meetings, packs, incentives, food diaries. The endpoints the participant app itself calls are separate: see Participant app.
Every path here is under /trials/<trialId>/participants. A participant has two identifiers: the _id used in paths, and the human-readable id your study gave them (P-0042). See Identifiers.
Permissions are granular. In outline: trial.readParticipants (or site.readParticipants on their sites) to see participants; trial.registerParticipants to create them; trial.writeParticipants to edit details; trial.tagParticipants, trial.allocateParticipants, trial.archiveParticipants, trial.withdrawParticipants and trial.deleteParticipants for those actions; trial.readParticipantPersonalInformation to see names and contact details; trial.support to message them. trial.write and trial.admin imply most of these. The Roles and permissions article has the full list.
Endpoints
| Method | Path | Description |
|---|---|---|
GET |
/trials/<trialId>/participants |
List and filter participants, 80 to a page, or export the list as CSV. |
POST |
/trials/<trialId>/participants |
Register one participant, or many. |
PATCH |
/trials/<trialId>/participants |
Update groups, sites, archival or withdrawal for many at once. |
DELETE |
/trials/<trialId>/participants |
Delete many: {"participants": ["<_id>", …], "reason": "…"}. Web app only: needs a fresh confirmation of identity (see below). |
GET |
/trials/<trialId>/participants/<participantId> |
One participant. Viewing is logged. |
PUT |
/trials/<trialId>/participants/<participantId> |
Update a participant. |
DELETE |
/trials/<trialId>/participants/<participantId> |
Delete a participant and all their data. Body: {"reason": "…"} (required). Web app only: needs a fresh confirmation of identity (see below). |
GET |
…/<participantId>/stages |
The participant's stages with their status, as the app sees them. |
POST |
…/<participantId>/stages/<stageId>/tokens |
Make a link that opens one stage for the participant without signing in. |
POST DELETE |
…/<participantId>/stageOverrides |
Move when a stage becomes available for this participant. |
GET POST |
…/<participantId>/grants |
A one-time sign-in link for the participant. |
PUT DELETE |
…/<participantId>/anchors/<anchorId> |
Set or clear an anchor date. |
GET PUT |
…/<participantId>/messages |
Read the conversation with the participant; mark it read. |
POST |
…/<participantId>/messages |
Send the participant a message. |
POST |
/trials/<trialId>/participants/notifications |
Send a push notification to many. |
GET POST |
…/<participantId>/notes |
Staff notes on a participant. |
PUT DELETE |
…/<participantId>/notes/<noteId> |
Edit or delete a note. |
GET POST |
…/<participantId>/events |
Appointments and other calendar events. |
PUT DELETE |
…/<participantId>/events/<eventId> |
Edit or delete an event. |
GET POST |
…/<participantId>/meetings |
Video meetings. |
PUT DELETE |
…/<participantId>/meetings/<meetingId> |
Edit or cancel a meeting. |
GET |
…/<participantId>/packs |
Packs assigned to the participant; ?availableOnly=true for those that could be. |
PUT DELETE |
…/<participantId>/packs/<packId> |
Assign or unassign a pack. |
GET POST |
…/<participantId>/incentives |
Gift-card incentives issued, and issue one. |
GET POST |
…/<participantId>/foodDiaryEntries |
Food diary. |
PUT DELETE |
…/<participantId>/foodDiaryEntries/<entryId> |
Edit or delete an entry. |
GET POST |
…/<participantId>/insights |
AI insights about the participant — see Dashboards and insights. |
GET |
/trials/<trialId>/participantResources |
Resources shown to participants in the app. |
POST |
/trials/<trialId>/participantResources |
Add a resource. |
PUT DELETE |
/trials/<trialId>/participantResources/<resourceId> |
Edit or remove one. |
Deleting participants erases everything recorded about them, so it needs a fresh confirmation of identity at the moment of the action, which an API key cannot give: the request comes back as 403 with details.code of reauth. Delete participants in the web app — see Authenticating requests.
Responses, signatures and consent records for a participant are under Responses and exports and eConsent; group and site membership can also be changed from the group and site side.
Listing participants
GET /trials/<trialId>/participants returns:
{
"participants": [...],
"total": 240,
"nextPageAvailable": true,
"prevPageAvailable": false
}
| Parameter | Notes |
|---|---|
page |
Page number; 80 participants per page. |
term |
Match on participant ID — case-insensitive, and matched anywhere in the ID. |
sort, sortOrder |
Field to sort by (createdAt by default; id, lastSeenAt, ratioComplete, daysBehind, garminLastSynced) and asc or desc. daysBehind is the most days late on any stage behind schedule; garminLastSynced is when Garmin data last arrived (or they connected, if none has), so asc puts the longest without a sync first; like quietDays below, it leaves out anyone withdrawn, archived, finished or outside the groups Garmin is restricted to. Participants with no value — not behind, or no Garmin connection counted — come last in either direction. |
site |
A site _id. |
groupsInclude, groupsExclude |
Comma-separated group _ids the participant must be in, or must not be in. |
groupsIncludeNone, groupsExcludeNone |
true to include, or exclude, participants in no group. |
includeArchived |
true to include archived participants. |
scheduleBehind, scheduleOnSchedule |
true to keep only participants behind schedule, or on schedule. |
scheduleMinDaysBehind |
A whole number of days: keep only participants at least that many days late on some stage, counted from the day the stage opened. |
garminNotSyncedDays |
A whole number of days: keep only participants whose Garmin has sent no data for at least that long (and never fewer than two days), by the same rule as quietDays below — so never anyone withdrawn, archived or finished, or outside the groups Garmin is restricted to, and nobody while the database is locked. A negative or fractional day count, here or in scheduleMinDaysBehind, is refused with 400. |
allocationAllocated, allocationUnallocated |
Allocation status. |
joinedJoined, joinedNotJoined |
Whether they have signed in to the app. |
withdrawnWithdrawn, withdrawnNotWithdrawn |
Withdrawal status. |
recruitedRecruited, recruitedNotRecruited |
Recruited or not, as the overview counts it: by the trial's recruitmentBasis, plus anyone marked as recruited. |
unread |
true for participants with unread messages. |
format |
csv returns the whole filtered list as a CSV string in {"csv": "…"}, ignoring paging. |
Each participant carries _id, id, createdAt, lastSeenAt, groups, arm (the _id of their study arm, if they are in one and you may see it), sites, isUnallocated, isArchived, isWithdrawn, recruitedAt (when marked as recruited, if they were), progress (ratioComplete, and stagesBehindSchedule: one entry per overdue stage, with enableAt, when it opened, and daysLateBy), whether a password is set (passwordSet) and which channels can reach them (pushAvailable, emailAvailable, smsAvailable, whatsappAvailable). A participant who has connected a wearable also carries connectedServices, one entry per service with key (such as garmin), name, connectedAt and lastSyncedAt (when data last arrived, or null if none has yet). A Garmin entry also has quietDays: the number of whole days since data last arrived (counted from connectedAt if none ever has) once that reaches two, and null otherwise. It is the same rule the trial updates and the participant's device reminder use, so it is always null for a participant who is withdrawn, archived or has completed the study, for one outside the groups Garmin is restricted to, and while the database is locked or the trial no longer has the Garmin feature. connectedServices isn't included in the CSV. name, dob, email, phone and whatsapp are included only for viewers with the personal-information permission.
Registering participants
POST /trials/<trialId>/participants with a single participant:
| Field | Type | Notes |
|---|---|---|
participantId |
string | Required. 1–50 characters. Unique within the trial (case-insensitive). |
password |
string | Up to 200 characters. The password they sign in to the app with. Omit it for a participant who will join by link or consent flow. |
groups |
list of group _ids |
|
group |
group _id |
The same, for one group. |
sites |
list of site _ids |
Sites of this trial to add them to. A site-restricted registrar can name only sites they hold site.registerParticipants on. |
isUnallocated |
boolean | Register as unallocated. |
Returns the new participant. To register many at once send {"participants": [{...}, {...}]} with the same fields per entry; the response is {"participants": [...]}. The whole request is checked before anything is written, so a duplicate ID anywhere in it means nothing is created. Registration counts against the trial's participant limit, which comes with its licence, and a site-restricted registrar's participants are put in all of their sites automatically if sites is left out. A locked database refuses registration.
Updating a participant
PUT /trials/<trialId>/participants/<participantId>:
| Field | Type | Notes |
|---|---|---|
id |
string | Change the participant ID. |
password |
string | Set a new password, or "" to remove it. |
name, email, phone, whatsapp |
string | Contact details. null clears. |
dob |
date | YYYY-MM-DD. |
timezone |
string | IANA name. |
isUnallocated, allocatedReason |
boolean, string | Allocation. |
isArchived |
boolean | Archived participants are hidden and cannot submit. |
isWithdrawn |
boolean | Withdrawn participants cannot submit; staff still can on their behalf. |
recruited, recruitedAt |
boolean, date | true marks the participant as recruited from recruitedAt (YYYY-MM-DD, today if left out, not in the future); false removes the mark. Needs trial.writeParticipants. |
Each field needs its own permission, as listed above; a request touching a field you cannot change is refused as a whole.
Updating participants in bulk
PATCH /trials/<trialId>/participants takes {"updates": [{"participant": "<_id>", "groups": [...], "sites": [...], "isArchived": true, "isWithdrawn": false, "recruited": true, "recruitedAt": "2026-10-20"}, …]}. Each entry names a participant and any of these fields, recruited and recruitedAt working as in a single update; groups and sites replace the participant's membership. A study arm listed in groups becomes the participant's arm; leaving it out does not take them out of their arm (see Groups). Returns {"updated": [{"participant": "<_id>"}, …]}.
Stages, links and sign-in
GET …/<participantId>/stages returns the participant's timeline as the app shows it — every occurrence of every stage with enabled, completed, enableAt, expiresAt — plus their ratioComplete. Occurrences of a recurring stage have _ids of the form <stageId>::<n>; use that form as the stageId in the response endpoints.
POST …/<participantId>/stages/<stageId>/tokens with {"recurrence": 0, "expiresInDays": 30} returns a token and a url that opens that stage for the participant in the browser without a password — for sending a questionnaire link by your own channel.
POST …/<participantId>/grants with {"expiresInDays": 90} makes a one-time sign-in link (url, expiresAt) for the participant app; GET returns the current unexpired one, or nulls. Needs trial.write, and a read-only API key is refused both — the link can be exchanged for a participant's token, so fetching one counts as writing.
POST …/<participantId>/stageOverrides with {"stage": "<stageId>", "stageRecurrence": 0, "enableAt": "<datetime>", "applyToAllRecurrences": false} moves when a stage opens for this participant alone; DELETE with the same as query parameters removes the override. Needs trial.rescheduleParticipants or trial.write.
Anchors
PUT …/<participantId>/anchors/<anchorId> with {"datetime": "<ISO datetime>"} sets an anchor date for the participant, which schedules anchored stages and messages; DELETE clears it. A locked database refuses both.
Messages and notifications
GET …/<participantId>/messages returns {"participant": {...}, "messages": [...], "automaticMessages": [...]} — the two-way conversation, and the automatic messages the participant has received. PUT on the same path marks it read.
POST …/<participantId>/messages:
| Field | Type | Notes |
|---|---|---|
message |
string | Required. 1–5000 characters. |
sendVia |
list | Any of push, email, sms, whatsapp. The message always appears in the app; these send it out as well, where the participant has that channel and the trial's wallet covers it. The SMS copy of a message over 500 characters is cut short with a note to read the rest in the app. |
attachments |
list of strings | Stored file names from an upload with forType=participant. |
Messages are delivered immediately and cannot be recalled. Needs trial.support.
POST /trials/<trialId>/participants/notifications with {"message": "…", "participants": [...], "groups": [...]} sends a push notification (up to 100 characters) to the listed participants and every member of the listed groups. Needs trial.admin.
Notes
POST …/<participantId>/notes with {"note": "…", "inRelationToDate": "<date or null>"} (up to 2000 characters) records a staff note; GET lists them. Writing needs trial.writeParticipantNotes or trial.write; reading needs trial.readParticipantNotes. A note can be edited or deleted by its author, or by a trial admin.
Events
An event is an appointment or other dated item on the participant's calendar in the app.
| Field | Type | Notes |
|---|---|---|
title |
string | 1–500 characters. |
description |
string | |
startsAt |
datetime | |
personnel |
list of user _ids |
Staff attending. |
Creating, editing and deleting events needs trial.write. A trial-wide view of events and meetings is GET /trials/<trialId>/schedule — see Trials.
Video meetings
Video meetings need the video-calls feature on your team. POST …/<participantId>/meetings takes name, startAt (datetime) and personnel (user _ids) and books a meeting room; the participant sees it in the app. See Video meetings.
Packs
GET …/<participantId>/packs lists the packs assigned to the participant; ?availableOnly=true lists the unassigned packs of the right type and site that could be given to them. PUT …/<participantId>/packs/<packId> assigns one; DELETE takes it back. Assigning needs trial.assignPacks or site.assignPacks on the pack's site.
Incentives
Where the incentives feature is on, GET /trials/<trialId>/incentives/brands lists the gift-card brands available, and POST …/<participantId>/incentives with {"brand": "<brand code>", "brandName": "…", "amount": 10, "currency": "GBP"} issues one to the participant, paid from the trial's wallet. GET lists what has been issued. Issuing needs trial.admin.
Food diary
Where food diaries are enabled on the trial, GET …/<participantId>/foodDiaryEntries?date=YYYY-MM-DD (or from and to) lists a participant's entries as {"foodDiaryEntries": [...]}. GET /trials/<trialId>/food-search?q=<term>&limit=25&offset=0 searches the food database (30 requests per minute).
POST …/<participantId>/foodDiaryEntries:
| Field | Type | Notes |
|---|---|---|
date |
date | Required. |
meal |
breakfast, lunch, dinner, snack, other |
Required. |
foodId |
integer | Required. From the search. |
portion |
object | Required. {"size": <amount>, "unit": "grams", "description": "…"}. |
quantity |
number | How many portions. Default 1. |
loggedAt |
datetime | When it was eaten. |
Participant resources
Resources are the documents, links and videos shown to participants under the app's resources tab. POST /trials/<trialId>/participantResources takes a type of file (with storedName, originalName, mimeType, size from an upload with forType=trial), link (linkUrl), youtube (youtubeUrl), video or text (html), plus a title and description. PUT changes title, description, index (position) or the content field; DELETE removes it. Needs trial.write.