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 403 with 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 403 with details.code of reauth. 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:

  1. GET /uploads/file/request?name=<file name>&size=<bytes>&type=<MIME type>&forType=<scope>&forId=<id> (use /uploads/video/request for a video) — forType is trial (with the trial _id), participant (with the participant _id), site, query, team or user. The response has a signedRequest URL and the fileName the file will be stored under.
  2. PUT the file's bytes to signedRequest, with a Content-Type header matching the type you declared and no Authorization header.
  3. Use fileName as the storedName in 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.