Authenticating requests
The base address
All requests go to:
https://api.trialflare.com
Paths in the reference articles are relative to this — GET /users/me means GET https://api.trialflare.com/users/me. Only HTTPS is served.
The Authorization header
Send your API key as a bearer token on every request:
Authorization: Bearer <your API key>
A request without the header is refused with 401. A request with a key that has expired, been deleted, or belongs to a disabled account is also refused with 401. A key that is valid but does not carry the permission the endpoint needs is refused with 403, and the refusal is recorded in the trial's event log like any other permission failure.
A complete request, listing the trials in your team:
curl https://api.trialflare.com/teams/<teamId>/trials \
-H "Authorization: Bearer <your API key>"
And one that creates a participant:
curl -X POST https://api.trialflare.com/trials/<trialId>/participants \
-H "Authorization: Bearer <your API key>" \
-H "Content-Type: application/json" \
-d '{"participantId": "P-0042", "password": "correct-horse-battery"}'
Request and response bodies
Request bodies are JSON, and need a Content-Type: application/json header. Field names are camelCase. Unless an article says otherwise, PUT and PATCH requests are partial: send only the fields you want to change, and everything else is left as it is.
Responses are JSON objects. Lists come wrapped in a named property rather than as a bare array — {"trials": [...]}, {"participants": [...]} — so a response always has room for extra properties such as a page count.
Bodies are validated before the request reaches the platform. A body that fails validation is refused with 422 and a validations object naming each field and what was wrong with it:
{
"message": "json: participantId: Length must be between 1 and 50.\n",
"validations": {"json": {"participantId": ["Length must be between 1 and 50."]}}
}
Identifiers
Every object — team, trial, participant, stage, data type, response — has an _id: a 24-character hexadecimal string. It is the identifier you use in paths and in references between objects (a participant's groups is a list of group _ids, a stage component's dataType is a data type _id).
Participants have a second identifier, id: the human-readable participant ID your study assigned (P-0042). It is what you see in the web app and in exports, but paths and references always use the _id.
Dates and times
Timestamps are ISO 8601 strings in UTC, with a trailing Z: 2026-09-22T14:03:11.412000Z. Send dates and times in the same format. Where an endpoint accepts a plain date (a date of birth, a food diary day) it takes YYYY-MM-DD.
Errors
Every error is a JSON object with a message you can show to a person, and sometimes a details object with more:
| Status | Meaning |
|---|---|
400 |
The request was understood but cannot be carried out — a participant ID already exists, a trial's database is locked, a limit would be exceeded. message says which. |
401 |
No API key, or a key that is not valid. |
403 |
Your account does not hold the permission the endpoint needs, or your team does not have the feature. |
404 |
The object does not exist, or exists in a trial you cannot see. |
422 |
The body failed validation. See above. |
429 |
Too many requests. See below. |
Rate limits
Some endpoints are limited to a number of requests per minute per account — the reference articles note the limit where one applies. A request over the limit is refused with 429 and an Allowed limit property saying what the limit is. Wait for the minute to roll over and retry; there is no penalty beyond the refusal. The limits are set for interactive use, so an integration that fetches everything for a trial once and then polls for changes will never meet them. One that calls GET /users/me before every request will.
Permissions
The API enforces the same trial permissions and team permissions as the web app, with the same site and group restrictions. In particular:
- Restricted and PII data types are removed from responses and exports unless you hold
trial.readParticipantStudyDataRestricted/trial.readParticipantStudyDataPii. They are silently absent rather than refused, so an integration that sees fewer fields than it expects should check the permissions of the account behind the key. - Personal information about participants — name, date of birth, email, phone — is only included in participant objects for accounts that hold the permission to see it.
- Site-restricted accounts see only participants, files and messages for their sites, and lists are filtered accordingly.
- Locked trials refuse configuration changes; locked databases refuse new and edited study data. Both come back as
403with a message saying so. - Some actions are reserved for the web app. Locking or unlocking a trial or its database, and signing a casebook, each need a fresh confirmation of identity — a password, passkey or authenticator code — at the moment of the action. An API key cannot provide one, so these come back as
403withdetails.codeofreauth. Do them in the web app.
Long-running work
A few operations take too long for a single request — building a trial from documents, generating stages from a description, some exports. They return immediately with a job:
{"jobId": "66f1c2a9e4b0f4a1d2c3b4a5"}
Poll it with GET /jobs/<jobId>:
{"status": "pending", "progress": "Reading the protocol…", "result": null, "error": null}
status moves from pending to done — at which point result holds what the operation produced — or to error, with a message in error. Only the account that started a job can read it. Poll every few seconds; jobs typically run for tens of seconds to a few minutes.
Downloading exports
Exports that produce a file — Excel, SPSS, Stata, R, GraphPad, PDF, the study record, raw hardware data — return a request property holding a temporary signed URL. Download the file from that URL with a plain GET and no Authorization header. The URL expires; fetch the export again if it has. CSV exports are returned inline instead, as a csv string.
Uploading files
Files are not sent through the API. Instead you ask for an upload slot, put the bytes there directly, then tell Trialflare about the file:
GET /uploads/file/request?name=<file name>&size=<bytes>&type=<MIME type>&forType=<scope>&forId=<id>(use/uploads/video/requestfor a video) —forTypeistrial(with the trial_id),participant(with the participant_id),site,query,teamoruser. The response has asignedRequestURL and thefileNamethe file will be stored under.PUTthe file's bytes tosignedRequest, with aContent-Typeheader matching thetypeyou declared and noAuthorizationheader.- Use
fileNameas thestoredNamein whatever you are creating — a document version, a participant resource, a message attachment.
Files are limited to 30 MB; videos (a type beginning video/) to 5 GB.
The API version
Every response carries an X-Trialflare-Version header naming the build that served it. Quote it when reporting a problem.