Sending data from your experiment

The code that sends data from a jsPsych experiment or plain JavaScript, and what each response means.

jsPsych

Use the @jspsych/extension-pipe extension. Register it when you set up jsPsych and it saves your data on its own. There's no save trial to add, and by default it sends each trial as it happens rather than waiting until the experiment ends. The panel below is the same one on your experiment dashboard, where YOUR_EXPERIMENT_ID is already filled in. Copy from there when you're ready to run.

Load the extension and register it. That is the whole integration — there is no save trial to add.

<script src="https://unpkg.com/@jspsych/extension-pipe"></script>
const jsPsych = initJsPsych({
  extensions: [
    {
      type: jsPsychExtensionPipe,
      params: {
        experiment_id: "YOUR_EXPERIMENT_ID",
        filename: () => `${subject_id}.csv`
      }
    }
  ]
});

const subject_id = jsPsych.randomization.randomID(10);

const timeline = [];
// ...add your trials to the timeline...

jsPsych.run(timeline);

Each trial is sent as it happens, so a participant who closes the tab partway through does not take all of their data with them: their completed trials arrive as a separate file ending in .partial.json, and do not count toward your session limit. A participant who finishes produces one ordinary file.

Add format: "json" to save JSON instead of CSV. Add stream: false to send only at the end.

The code is the same whichever storage provider you chose. Your experiment never has to name a provider.

What sending each trial as it happens does for a participant who drops out, and the limits it runs under. Saving as you go

If you built an experiment on the older jsPsychPipe plugin and its save trial, it still works. We don't recommend it for new experiments, but nothing about it is broken.

Plain JavaScript

You don't need jsPsych, or any framework at all. Use datapipe-client, a small library with no jsPsych in it. The menu at the top right of the panel above switches every sample to it.

<script src="https://unpkg.com/datapipe-client"></script>

That gives you a DataPipe global. If you use a bundler, npm install datapipe-client instead. Either way you get one function for each JavaScript tab in the panel: saveData, createSession, saveBase64Data, and getCondition.

Send whatever your experiment produces. The data string is stored byte for byte under the filename you give it.

One thing to know about the library before you read the reference, because it's easy to get wrong and doesn't fail loudly: getCondition throws, and nothing else does. Saving fails quietly on purpose, because a failed upload is retried and the data is still in the browser. A condition is different. It usually decides which timeline a participant runs, so there's no sensible fallback. Catch the error and decide what the participant sees, rather than letting them run the wrong condition.

The library is a thin layer over DataPipe's HTTP API, and you can call that API yourself instead. Saving data, saving a file, and requesting a condition are each one POST with a JSON body carrying your experiment ID, a filename, and the data as a string. You give up two things. Compression becomes your job (see Request size limits), and saving as you go is impractical, because it writes each trial to a database rather than posting it to DataPipe.

Every function, its options, and what it returns. datapipe-client reference

Every field, response code, and error code for all three participant endpoints. API reference

Filenames must be unique

Two submissions to the same experiment can never share a filename. The second one is rejected with FILE_EXISTS and isn't stored. Generate a fresh random ID per participant and build the filename from it, as the samples above do. (This code was called OSF_FILE_EXISTS before September 2026. See What's changed.)

How the check works, and what each provider does with a duplicate. Filenames, archives and your storage

Media and binary files

Base64 data collection lets you send binary files (audio recordings, video, or images) encoded as base64 strings. DataPipe decodes the string and stores the resulting file alongside the rest of your experiment's data. Each request sends one file.

Three things to know before you rely on it:

  • It has its own switch, independent of Accept new data. Turn on Accept base64 file uploads on the dashboard, or these requests are rejected with BASE64DATA_COLLECTION_NOT_ACTIVE. The flip side is that this switch keeps accepting files after you've turned off Accept new data. Turn both off when a study ends.
  • Your validation rules don't apply to it. DataPipe checks only that the string really is base64. It can't tell what the decoded file contains, which is why the switch is separate and why it's worth turning off outside active collection.
  • It doesn't count toward your session limit, and it isn't blocked by one. A file upload can still arrive after an experiment has hit its cap on data submissions.

What the session cap does and doesn't cover. Session limits

Request size limits

A single request to DataPipe can be at most 32 MB. The server infrastructure enforces this limit, and it can't be raised. Most experiment data is well under it. A typical jsPsych dataset is 50 KB to 5 MB.

The extension and datapipe-client both compress request bodies with gzip before sending, and so does version 0.6.0 or later of the older @jspsych-contrib/plugin-pipe plugin. Text data (JSON, CSV) typically shrinks by 2–10x, which in practice raises the ceiling to roughly 60–300 MB for most experiment data. Compression needs no setup.

Compression helps less with binary data sent to the base64 endpoint, such as video or audio recordings, because binary data doesn't compress as well as text. Individual files larger than about 25 MB may still exceed the limit even after compression.

If you call the API yourself rather than through the extension or datapipe-client, you can compress the request body with the browser's CompressionStream API and set the Content-Encoding: gzip header. The server decompresses it automatically.

A request that is still over 32 MB never reaches DataPipe's own code. The hosting infrastructure rejects it before any endpoint runs, so DataPipe can't give you an error response and nothing on your dashboard explains it. Your request gets back a bare 500 Internal Error, or a plain network failure depending on the client, and unlike a queued 202, the data isn't held anywhere for retry. If participants are hitting this, shrink the payload: split large recordings into smaller files, lower a sampling rate, or send data more often instead of once at the end. Retrying the same request won't help.

What the response means

In normal operation, every submission gets one of three answers, and only one of them means you need to do something.

  • 201: stored. The file is in your storage provider.
  • 202: accepted, not delivered yet. DataPipe is holding the data and will keep trying your provider on its own. Treat it as success and don't resubmit.
  • 400: rejected, and nothing was stored. The error field in the response body says why: data collection switched off, the session limit reached, a duplicate filename, or data that failed validation.

Responses from the data endpoint also carry a metadataMessage field. It reports what DataPipe did with your Psych-DS metadata. It never decides whether a submission is accepted.

What a 202 means for your data, and where to find the file while it waits. When an upload fails

The full status table, with every error code and what to do about it. Responses