API reference
Every endpoint your experiment can call, what it returns, and the limits every request runs under.
All endpoints accept JSON request bodies with Content-Type: application/json. You'll need an experiment ID, which DataPipe assigns when you create your experiment. Code examples for jsPsych and JavaScript are on each experiment's dashboard.
The API is the same whichever storage provider an experiment uses. DataPipe routes each submission to that experiment's own destination (a Google Drive folder, a Dataverse dataset, or a Zenodo deposition), so your experiment code never names a provider.
Limits
Three limits apply to every request, and none of them can be changed per experiment.
- 32 MB per request. Enforced by the server infrastructure and not adjustable. A typical jsPsych dataset is 50 KB to 5 MB, so this mainly matters for base64 media. Gzipped request bodies are decompressed transparently, which in practice raises the ceiling for text data.
- 60 seconds per request. Every
/api/*path runs behind a hosting layer with a hard 60-second ceiling. That's why/api/finalizereturns immediately and finishes its work in the background instead of waiting for the job to end. - JSON bodies only. Send
Content-Type: application/json. The three participant endpoints don't check the HTTP method, so a request sent with the wrong verb arrives with an empty body and comes back asMISSING_PARAMETERrather than405. The two authenticated endpoints below do check, and answer405.
Compressing a request body yourself, and what the size limit means in practice. Request size limits
The trial size, session, abandonment, and file-size limits that apply only to incremental sessions. Save-as-you-go limits
Save text data
Save text data
POSThttps://pipe.jspsych.org/api/data/
Save a text file (CSV, JSON, etc.) to your experiment's storage. If you have validation rules set up, DataPipe checks the data before sending it on.
| Field | Type | Description |
|---|---|---|
experimentID | string | Your experiment ID, found on the experiment dashboard. |
filename | string | Name for the stored file (e.g., subject01.csv). Must be unique, or the request fails. |
data | string | The file contents as a string. |
sessionId | string (optional) | The session returned by /api/session/, if this experiment staged its trials as it went. It carries no data of its own. The data field above is still the submission. It only tells DataPipe which staged copy this request replaces, so DataPipe can discard it. |
Example request body
{
"experimentID": "abc123",
"filename": "subject01.csv",
"data": "rt,response\n204,1\n389,0"
}Start an incremental session
Start an incremental session
POSThttps://pipe.jspsych.org/api/session/
Open a session so an experiment can send trials as they happen, rather than only at the end. A participant who abandons the experiment partway through then leaves behind a recoverable partial session instead of nothing at all.
You won't usually call this yourself. The @jspsych/extension-pipe extension calls it by default, along with the staging writes that follow, and datapipe-client does the same when a plain JavaScript experiment starts a session with it. It's documented here because those writes go to a Firebase Realtime Database rather than to this API, and this response tells a client where to send them.
| Field | Type | Description |
|---|---|---|
experimentID | string | Your experiment ID, found on the experiment dashboard. |
filename | string (optional) | The name this participant will submit under. Used only to name a recovered partial session, so an abandoned run is identifiable. A completed submission always uses the filename sent to /api/data/. |
The same checks as /api/data/ run here, with the same error codes. The experiment must exist, not be finalized, be accepting data, and be under its session limit. Starting a session does not use up one of those sessions. The count is still taken when a submission completes. A 503 with SESSION_START_ERROR means incremental upload is unavailable, whether because the service is unreachable, because an experiment already has an unusually large number of sessions open at once, or because it has been switched off entirely. The experiment should submit at the end, as it would without streaming.
Example response
{
"sessionId": "8fKq2mXpR7vNwLzB4cTy1dHs",
"databaseURL": "https://<project>-default-rtdb.firebaseio.com",
"maxTrialBytes": 16384,
"maxTrials": 1000,
"flushIntervalMs": 10000,
"flushEveryNTrials": 10,
"maxDisconnects": 20
}Trials are then written to staging/<sessionId>/trials/<n> in that database, each one a JSON string, numbered from zero and never rewritten. The session is write-only. Nothing can read it back, and DataPipe tolerates a missing number rather than treating it as an error. Send sessionId with the final /api/data/ request to close it.
Save base64-encoded data
Save base64-encoded data
POSThttps://pipe.jspsych.org/api/base64/
Save a binary file (audio, video, images) encoded as a base64 string. DataPipe decodes the string and stores the resulting file alongside the experiment's other data.
| Field | Type | Description |
|---|---|---|
experimentID | string | Your experiment ID. |
filename | string | Name for the decoded file (e.g., recording_01.webm). Must be unique. |
data | string | The base64-encoded file contents. |
Get condition assignment
Get condition assignment
POSThttps://pipe.jspsych.org/api/condition/
Get the next condition number for balanced assignment. Returns a value from 0 to n−1, cycling in order (0, 1, 2, ..., 0, 1, 2, ...).
| Field | Type | Description |
|---|---|---|
experimentID | string | Your experiment ID. |
Example response
{
"message": "Success",
"condition": 2
}Responses
All responses are JSON. On failure, the body carries an error code from the table below and a message describing the problem. When metadata is on, write responses also include a metadataMessage field reporting what happened to the metadata file. It never affects whether the data itself was stored.
| Status | Meaning |
|---|---|
201 | Stored. The body is { "message": "Success" }. The condition endpoint returns 200 with a condition field instead. |
202 | Accepted and queued. DataPipe has your data safely but couldn't reach your storage provider yet, so it will retry automatically. error is null. Treat this as success and do not resubmit. Retrying would store the participant's data twice. |
400 | Rejected. The data was not stored. |
500 | Something failed on our side. See the individual codes below for whether the data was stored. |
What DataPipe does with a queued submission, and how to get it back. When an upload fails
Error codes
FILE_EXISTS, UPLOAD_ERROR, and UPLOAD_EXCEPTION are returned for every storage provider. INVALID_OSF_TOKEN occurs only on experiments still collecting to OSF, which is why it still names it.
INVALID_OSF_TOKEN, INVALID_REFRESH_TOKEN, and PROVIDER_TOKEN_EXPIRED no longer reject a submission. If a connected account's credential has expired, been revoked, or gone invalid, DataPipe queues the submission for retry (202, error: null), the same as a provider outage, because reconnecting the account fixes it. They are listed below because a queued entry's failureReason and the failure-notification email still name them. PROVIDER_NOT_CONNECTED is the one credential code still rejected outright: with no connection at all, a retry has nothing to succeed against.
Renamed in September 2026
Three codes dropped their OSF_ prefix: OSF_FILE_EXISTS → FILE_EXISTS, OSF_UPLOAD_ERROR → UPLOAD_ERROR, and OSF_UPLOAD_EXCEPTION → UPLOAD_EXCEPTION. They are returned on every provider, so the old names described nothing. If your experiment compares error against one of the old strings, that comparison no longer matches and the branch stops running — most often a retry that regenerates a filename after OSF_FILE_EXISTS. Match the new names, or both while you roll experiments over. The HTTP status codes are unchanged, so anything branching on those is unaffected. See What's changed for the rest of the September 2026 update.
The message text is human-readable only, and is reworded without notice. When writing code, match on the error code, not the message.
| Error code | Status | Meaning |
|---|---|---|
MISSING_PARAMETER | 400 | One or more required fields are missing from the request body. |
EXPERIMENT_NOT_FOUND | 400 | No experiment matches the provided ID. |
EXPERIMENT_DATA_NOT_FOUND | 400 | The experiment exists, but DataPipe could not read its configuration. |
USER_DATA_NOT_FOUND | 400 | DataPipe could not read the account that owns the experiment. |
INVALID_OWNER | 400 | The experiment owner does not match a valid user account. |
EXPERIMENT_FINALIZED | 400 | The experiment has been finalized and no longer accepts submissions. |
DATA_COLLECTION_NOT_ACTIVE | 400 | Data collection is not enabled for this experiment. |
BASE64DATA_COLLECTION_NOT_ACTIVE | 400 | Base64 data collection is not enabled for this experiment. |
CONDITION_ASSIGNMENT_NOT_ACTIVE | 400 | Condition assignment is not enabled for this experiment. |
SESSION_LIMIT_REACHED | 400 | The experiment has reached its session limit. Raise the limit in the dashboard. |
INVALID_DATA | 400 | The data did not pass the validation rules set for this experiment. |
INVALID_BASE64_DATA | 400 | The data is not valid base64. |
METADATA_ERROR | 400 | DataPipe could not produce Psych-DS metadata from this submission, so it did not store the data. It keeps the submission and recovers it automatically in the background. |
FILE_EXISTS | 400 | A file with this name already exists in the experiment's storage. Filenames must be unique. |
UPLOAD_ERROR | 400 | The storage provider rejected the upload. |
PROVIDER_NOT_CONNECTED | 400 | The owner has not connected an account for this experiment's storage provider. |
PROVIDER_TOKEN_EXPIRED | 202 (queued) | The API token for the storage provider has expired. The submission is queued and retried automatically rather than rejected. The owner must still create a new token and reconnect it, since retrying alone cannot fix an expired static token, but no participant sees an error for it. |
INVALID_OSF_TOKEN | 202 (queued) | The OSF token for this account is invalid or expired. Queued and retried automatically; reconnecting the account is what lets a later retry succeed. |
INVALID_REFRESH_TOKEN | 202 (queued) | The owner's refresh token is no longer valid (OSF, Google Drive, or Zenodo). Queued and retried automatically; reconnecting the account is what lets a later retry succeed. |
UNKNOWN_ERROR_GETTING_CONDITION | 400 | An unexpected error occurred while assigning a condition. |
TOKEN_RESOLUTION_ERROR | 500 | DataPipe could not resolve the owner's storage credentials. |
UPLOAD_EXCEPTION | 500 | An unexpected error occurred while uploading to the storage provider. |
DATA_PERSIST_ERROR | 500 | DataPipe could not save the data, and a live participant may need to resubmit. |
Queue status
Queue status
GEThttps://pipe.jspsych.org/api/queuestatus
List the queued uploads DataPipe is holding for an experiment, or download them. This is the endpoint behind the queued files panel on the dashboard, and the scriptable way to recover data that hasn't reached your storage provider.
Unlike the three participant endpoints, this one is authenticated. Send a Firebase ID token for the account that owns the experiment as Authorization: Bearer <token>. Anything other than GET gets 405.
| Field | Type | Description |
|---|---|---|
experimentID | query string | The experiment whose queue you want. Required. |
download | query string (optional) | The id of a single queue entry. Responds with that file's contents as an attachment, decoded back to the original bytes for base64 submissions. |
downloadAll | query string (optional) | Set to true to receive every waiting, in-flight, and failed file for the experiment as one ZIP. |
With no download or downloadAll, the response is 200 with an entries array and a count, newest first. Each entry carries id, filename, dataType, status, errorCode, retryCount, maxRetries, createdAt, lastAttemptAt, nextRetryAt, and failureReason. Only entries that are pending, processing, or failed are listed. A completed upload leaves the queue.
Example response
{
"entries": [
{
"id": "abc123_subject01.csv",
"filename": "subject01.csv",
"dataType": "data",
"status": "pending",
"errorCode": 503,
"retryCount": 2,
"maxRetries": 5,
"createdAt": "2026-08-22T14:03:11.000Z",
"lastAttemptAt": "2026-08-22T17:03:44.000Z",
"nextRetryAt": "2026-08-22T21:03:44.000Z",
"failureReason": "Provider error 503: Service Unavailable"
}
],
"count": 1
}| Status | Meaning |
|---|---|
400 | No experimentID query parameter. |
401 | No Authorization header, or the token could not be verified. |
403 | You do not own that experiment. A nonexistent experiment gets the same answer, so this endpoint never reveals which experiment IDs exist. |
404 | The requested queue entry does not belong to that experiment, or downloadAll found nothing queued. |
405 | The request was not a GET. |
500 | DataPipe could not read a queued upload. Nothing has been deleted. Try again, or fetch the files one at a time. |
Finalize
Finalize
POSThttps://pipe.jspsych.org/api/finalize
Start finalizing an experiment: merge everything in its storage into one archive and permanently stop accepting submissions. This is the endpoint behind the Finalize control on the dashboard.
Authenticated the same way as queue status: Authorization: Bearer <token> for the owning account. Anything other than POST gets 405.
| Field | Type | Description |
|---|---|---|
experimentID | string | The experiment to finalize. Required. |
The response doesn't tell you the outcome. Merging a whole study takes longer than the 60-second request ceiling, so a successful call returns 202 with { "status": "queued" } and the work runs in the background. Watch the experiment's dashboard, which reports queued, then running, then the result. Calling again while a pass is in flight returns 202 with the current status rather than starting a second one.
| Status | Meaning |
|---|---|
202 | Accepted. Body is { "status": "queued" }, or queued/running if a pass was already under way. |
200 | { "status": "already-finalized" }. Nothing to do. Finalizing is permanent. |
400 | No experimentID, or the experiment predates the per-experiment storage DataPipe now creates and carries { "status": "not-eligible" } with a detail. |
401 | Missing or unverifiable bearer token. |
403 | You do not own that experiment, or it does not exist. |
405 | The request was not a POST. |
500 | DataPipe could not schedule the background job. Nothing has been merged or deleted. |
Statuses the dashboard reports
The outcome lands on the experiment record. These are all the possible statuses:
| Status | Meaning | |
|---|---|---|
queued | in flight | The background job has been scheduled. |
running | in flight | The merge is under way. |
finalized | done | One archive now holds the whole dataset and the experiment accepts no further submissions. |
already-finalized | done | It had already been finalized. |
not-eligible | refused | This storage provider has no file-count ceiling to work around. Today that means anything other than Zenodo. |
queued-uploads-pending | refused | Uploads are still waiting to be stored, and they belong inside the archive. Let the upload queue drain and try again. |
nothing-to-archive | refused | The experiment has never received any data. |
leased-elsewhere | refused | Another finalizing pass or archive merge is already running for this experiment. Try again shortly. |
archive-too-large | refused | The merged archive would exceed the provider's per-file limit. Nothing was uploaded or deleted. |
failed | error | Something went wrong during the pass. DataPipe deletes files only after verifying the archive that replaces them. |
What finalizing does to your files, and which providers support it. Finishing a study