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>"}, …]}.

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.