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 will need an experiment ID, which you get when you create an experiment on DataPipe. Code examples for jsPsych and JavaScript are available 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 is configurable per experiment.
- 32 MB per request. Enforced by the server infrastructure and not raisable. A typical jsPsych dataset is 50 KB to 5 MB, so this bites mainly on base64 media. Gzipped request bodies are decompressed transparently, which effectively raises the ceiling for text data.
- 60 seconds per request. Every
/api/*path runs behind a hosting layer with a hard 60-second ceiling. It is why/api/finalizereturns immediately and does its work in the background rather than answering when the job is done. - JSON bodies only. Send
Content-Type: application/json. The three participant endpoints do not 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
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 configured, 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 — the request will fail if a file with this name already exists. |
data | string | The file contents as a string. |
metadataOptions | object (optional) | Extra Psych-DS metadata to merge into this experiment's dataset description. Ignored unless metadata is switched on for the experiment. |
sessionId | string (optional) | The session returned by /api/session/, if this experiment staged its trials as it went. It carries no data — the data field above is still the submission — and only tells DataPipe which staged copy this request supersedes, so it can be discarded. |
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 are produced, rather than only at the end. A participant who abandons the experiment partway then leaves behind a recoverable partial session instead of nothing at all.
You will not usually call this directly — the jsPsychPipe plugin does it for you, along with the staging writes that follow. It is documented because those writes go to a Firebase Realtime Database rather than to this API, and this response is what tells a client where.
| 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 rather than opaque. 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 consume one from that limit — the count is still taken when a submission completes. A 503 with SESSION_START_ERROR means incremental upload is unavailable and the experiment should simply submit at the end, as it would otherwise.
Example response
{
"sessionId": "8fKq2mXpR7vNwLzB4cTy1dHs",
"databaseURL": "https://<project>-default-rtdb.firebaseio.com",
"maxTrialBytes": 65536,
"maxTrials": 10000,
"flushIntervalMs": 10000,
"flushEveryNTrials": 10
}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 a missing number is tolerated rather than treated 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 sequentially (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 could not 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 | The request was rejected and 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
Codes beginning OSF_ are historical names kept for backward compatibility. OSF_FILE_EXISTS, OSF_UPLOAD_ERROR and OSF_UPLOAD_EXCEPTION are returned for every storage provider, not only OSF. INVALID_OSF_TOKEN and INVALID_REFRESH_TOKEN occur only on experiments still collecting to OSF.
The same applies to the message text: several messages still name OSF whatever provider an experiment actually uses — a queued upload reports “Data received. OSF upload will be retried automatically” on Google Drive, Dataverse, and Zenodo alike. Read “OSF” in a message as “your storage provider”, and match on the error code rather than the message when you are writing code.
| 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 its configuration could not be read. |
USER_DATA_NOT_FOUND | 400 | The account that owns the experiment could not be read. |
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 configured for this experiment. |
INVALID_BASE64_DATA | 400 | The data is not valid base64. |
METADATA_ERROR | 400 | Psych-DS metadata could not be produced from this submission, so the data was not stored. The submission is kept and recovered automatically in the background. |
OSF_FILE_EXISTS | 400 | A file with this name already exists in the experiment's storage. Filenames must be unique. |
OSF_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 | 400 | The API token for the storage provider has expired. The owner must create a new one and reconnect it. |
INVALID_OSF_TOKEN | 400 | The OSF token for this account is invalid or expired. |
INVALID_REFRESH_TOKEN | 400 | The owner's OSF refresh token is no longer valid. |
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. |
OSF_UPLOAD_EXCEPTION | 500 | An unexpected error occurred while uploading to the storage provider. |
DATA_PERSIST_ERROR | 500 | DataPipe could not save the data. It was not stored, 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 has not 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 is answered 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 | A queued upload could not be read. Nothing has been deleted — try again, or fetch the files individually. |
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 is answered 405.
| Field | Type | Description |
|---|---|---|
experimentID | string | The experiment to finalize. Required. |
The response does not 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 | The background job could not be scheduled. Nothing has been merged or deleted. |
Statuses the dashboard reports
The outcome lands on the experiment record. The full vocabulary is:
| 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 relieve — 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. Files are only ever deleted after the archive that replaces them is verified. |
What finalizing does to your files, and which providers support it. Finishing a study