API Documentation

Submit predictions and retrieve results programmatically over HTTP.

Getting started

The base path for every endpoint is:

/online_predictors/api/

Two things that will otherwise cost you an afternoon:

  • Trailing slashes are required. A POST to a path without its trailing slash cannot be redirected without losing the request body.
  • No CSRF token is needed for the submission endpoints. A plain requests.post(...) works.

Your token

Every request carries a token: an opaque string of your choosing, up to 100 characters. There is no registration step and no approval — pick one and start submitting. The web interface generates one for you and keeps it in your browser's local storage.

Treat your token as a secret. It is the only thing that identifies your results: anyone who knows it can list and download everything you have submitted. Do not commit it to a repository.

The token travels differently depending on the endpoint:

EndpointsWhere the token goes
api/submit/… a token field in the request body
api/past-results/…, results/{id}/download/ an X-Bio2Byte-Token request header
api/past-results/shiftcrypt/alignment/ a ?token= query parameter
api/results/{id}/, api/results/{id}/history/ no token — anyone holding the request id can read it

Rate limit

A token may queue 100 jobs per hour, counted over a rolling 60 minutes and shared across all three submission endpoints — the limit is on jobs, not on calls to any one endpoint.

Rejected submissions (any 4xx) do not count against it, so a script with a bug will not lock itself out.

Every successful submission reports where you stand:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 0

Over the limit, the response is 429 Too Many Requests with a Retry-After in seconds:

HTTP/1.1 429 Too Many Requests
Retry-After: 1847

{
    "error": "Submission limit reached: 100 jobs per hour for this token. Retry in 1847 seconds.",
    "limit": 100,
    "remaining": 0,
    "reset_after": 1847
}

A second, looser ceiling applies per network address, so that rotating tokens does not sidestep the limit. If you have a legitimate need for a higher allowance, get in touch and we can exempt your token.

Single sequence prediction

Submit
POST /online_predictors/api/submit/single-sequence/
Content-Type: application/json

{
    "token": "your_token_here",
    "predictor_type": "dynamine",
    "sequence": ">INS_HUMAN\nMLSDEDFKAVFGMTRSAFANLPLWKQQNLKKEKGLF\n"
}
With curl
curl -X POST https://bio2byte.be/online_predictors/api/submit/single-sequence/ \
     -H 'Content-Type: application/json' \
     -d '{"token": "your_token_here",
          "predictor_type": "dynamine",
          "sequence": ">INS_HUMAN\nMLSDEDFKAVFGMTRSAFANLPLWKQQNLKKEKGLF\n"}'
Uploading a FASTA file instead
curl -X POST https://bio2byte.be/online_predictors/api/submit/single-sequence/ \
     -F token=your_token_here \
     -F predictor_type=dynamine \
     -F job_name='Insulin comparison' \
     -F fasta_file=@sequences.fasta
Field
tokenrequired
predictor_typerequired; one name, or several separated by commas
sequenceFASTA text — required unless you upload a file
fasta_filea file upload, as multipart/form-data instead of JSON
job_nameoptional label, up to 120 characters, unique per token

Available predictor_type values:

  • dynamine
  • disomine
  • efoldmine
  • agmata
  • psper
These are not the same as the page names in the menu: EarlyFolding submits efoldmine and PSPer submits psper. Anything else is accepted at submission time and then fails in the worker.

Run the whole suite in one job by listing several:

"predictor_type": "dynamine,disomine,efoldmine,agmata"
Response
{
    "request_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
    "task_id": "c31d822c-55af-4a3e-b22f-8d6a83876199",
    "status": "enqueued",
    "message": "Request submitted successfully"
}

All three submission endpoints answer with exactly these four fields, and word the same problem the same way — a missing token, an over-long job name or a duplicate one reads identically whichever predictor you use.

MSA prediction

Submit
POST /online_predictors/api/submit/msa/
Content-Type: application/json

{
    "token": "your_token_here",
    "msa_data": ">Protein1\nMLSDEDFKAVFGMTRSAFANLPLWKQQ\n>Protein2\nMLSDEDFKAVFGMTRSAFANLPLWKQQ\n"
}
With curl
curl -X POST https://bio2byte.be/online_predictors/api/submit/msa/ \
     -H 'Content-Type: application/json' \
     -d '{"token": "your_token_here",
          "msa_data": ">Protein1\nMLSDEDFKAVFGMTRSAFANLPLWKQQ\n>Protein2\nMLSDEDFKAVFGMTRSAFANLPLWKQQ\n"}'
Uploading an alignment file instead
curl -X POST https://bio2byte.be/online_predictors/api/submit/msa/ \
     -F token=your_token_here \
     -F msa_file=@alignment.afa

The alignment needs at least two sequences, all padded to the same length with -; anything else is rejected with a 400 describing the problem.

Response
{
    "request_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
    "task_id": "c31d822c-55af-4a3e-b22f-8d6a83876199",
    "status": "enqueued",
    "message": "Request submitted successfully"
}

ShiftCrypt prediction

Submit
POST /online_predictors/api/submit/shiftcrypt/
Content-Type: application/json

{
    "token": "your_token_here",
    "chemical_shifts": "<NEF or NMR-STAR file contents>",
    "model_class": "1",
    "original_numbering": true
}
With curl

Embedding shift data in JSON means escaping every newline in it, so from a shell the file upload below is almost always the easier route.

curl -X POST https://bio2byte.be/online_predictors/api/submit/shiftcrypt/ \
     -H 'Content-Type: application/json' \
     -d '{"token": "your_token_here",
          "chemical_shifts": "save_chemical_shift_list_1\n...",
          "model_class": "2",
          "original_numbering": false}'
Uploading a NEF or NMR-STAR file instead
curl -X POST https://bio2byte.be/online_predictors/api/submit/shiftcrypt/ \
     -F token=your_token_here \
     -F model_class=2 \
     -F original_numbering=true \
     -F chemical_file=@bmr25703_1.nef
Field
tokenrequired
chemical_shiftsNEF or NMR-STAR text — required unless you upload a file
chemical_filea file upload, as multipart/form-data instead of JSON
model_class"1", "2" or "3"; defaults to "1"
original_numberingboolean, defaults to true
job_nameoptional label, up to 120 characters, unique per token

The format is detected from the contents; you do not declare it.

Response
{
    "request_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
    "task_id": "c31d822c-55af-4a3e-b22f-8d6a83876199",
    "status": "enqueued",
    "message": "Request submitted successfully"
}

Retrieving results

GET /online_predictors/api/results/{request_id}/
With curl

Use -i so you can see the status code — it is what distinguishes a finished job from a running one.

curl -i https://bio2byte.be/online_predictors/api/results/3f2504e0-4f89-11d3-9a0c-0305e82c3301/

The status code tells you the outcome, so you can poll on it alone:

CodeMeaningBody
200 Finished the prediction payload itself
202 Still enqueued or running a status envelope; Retry-After: 5
422 The prediction failed a status envelope, with error set
404 No request with that id {"error": "Request not found"}
On 200 the body is the prediction itself, not wrapped in an envelope — a JSON object for single-sequence and MSA predictions, and a JSON array for ShiftCrypt.
While it is running (202)
{
    "request_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
    "status": "RUNNING",
    "request_type": "single_sequence",
    "progress": 40,
    "attempt": 1,
    "created_at": "2026-08-07T12:34:56.789+02:00",
    "updated_at": "2026-08-07T12:35:10.123+02:00",
    "error": null,
    "status_message": "Running predictions",
    "history_url": "/online_predictors/api/results/3f2504e0-4f89-11d3-9a0c-0305e82c3301/history/",
    "timing": {
        "queued_at": "2026-08-07T12:34:56.789+02:00",
        "started_at": "2026-08-07T12:35:02.100+02:00",
        "finished_at": null,
        "queue_wait": "00:00:05",   "queue_wait_seconds": 5,
        "elapsed_time": "00:04:10", "elapsed_seconds": 250,
        "left_time": "00:10:50",    "left_seconds": 650,
        "wall_time": "00:04:15",    "wall_seconds": 255,
        "time_limit": "00:15:00",   "time_limit_seconds": 900,
        "last_heartbeat_at": "2026-08-07T12:39:12.400+02:00",
        "heartbeat_age_seconds": 4
    }
}
The timing block

Four durations, deliberately kept apart — every one of them is also given in seconds so you do not have to parse a clock string:

queue_wait submitted until a worker picked it up. A long wait means the platform is busy, not that your job is slow.
elapsed_time picked up until finished, or until now. This is what the time limit is measured against.
left_time how much of the limit remains; null unless the job is running.
wall_time submitted until finished — queue wait plus elapsed.
status_message what the job is doing right now, in words ("Running predictions", "Saving results"), or how it finished. Reported for every prediction type.
heartbeat_age_seconds how long ago the worker last reported in. A job whose worker dies stops reporting and is failed shortly after, rather than appearing to run forever.

The same block appears on every entry of the past-results endpoints, which is where to look for a finished job's figures.

If it failed (422)
{
    "request_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
    "status": "FAILED",
    "error": "Invalid predictor types: earlyfolding",
    ...
}
The processing log
GET /online_predictors/api/results/{request_id}/history/

Returns what the job did, in order — state transitions interleaved with the phase each task reports as it works. Useful for seeing which stage a long job is in rather than only that it is running.

{
    "timeline": [
        {"kind": "state",   "timestamp": "...", "state": "RUNNING", "message": null},
        {"kind": "message", "timestamp": "...", "progress": 30, "message": "Running MSA predictions"},
        {"kind": "message", "timestamp": "...", "progress": 90, "message": "Saving results"},
        {"kind": "state",   "timestamp": "...", "state": "FINISHED", "message": null}
    ],
    "history":  [ ... state transitions only ... ],
    "messages": [ ... progress notes only ... ]
}

history and messages are the two halves on their own; timeline is the merged, chronological view. The same log is behind “Processing log” on the My predictions page.

Downloading in other formats

Once finished, results/{request_id}/download/ serves the same prediction as a file. It takes ?format=json|csv|metadata|input|distributions and requires your token in the X-Bio2Byte-Token header, because unlike the endpoint above it checks that the result is yours.

With curl
curl -OJ 'https://bio2byte.be/online_predictors/results/3f2504e0-4f89-11d3-9a0c-0305e82c3301/download/?format=csv' \
     -H 'X-Bio2Byte-Token: your_token_here'
Listing everything you have submitted

The past-results endpoints return every request for a token — one per prediction type.

With curl
curl https://bio2byte.be/online_predictors/api/past-results/single-sequence/ \
     -H 'X-Bio2Byte-Token: your_token_here'

Also available for msa and shiftcrypt in place of single-sequence.

Complete example

Python
import time

import requests

BASE = "http://bio2byte.be/online_predictors/api"
TOKEN = "your_token_here"

response = requests.post(
    BASE + "/submit/single-sequence/",
    json={
        "token": TOKEN,
        "predictor_type": "dynamine",
        "sequence": ">INS_HUMAN\nMLSDEDFKAVFGMTRSAFANLPLWKQQNLKKEKGLF\n",
    },
)
if response.status_code == 429:
    raise SystemExit("rate limited, retry in %s s" % response.headers["Retry-After"])
response.raise_for_status()
request_id = response.json()["request_id"]
print("submitted", request_id,
      "-", response.headers["X-RateLimit-Remaining"], "submissions left this hour")

while True:
    response = requests.get("%s/results/%s/" % (BASE, request_id))

    if response.status_code == 200:
        print("results:", response.json())
        break
    if response.status_code == 422:
        raise SystemExit("prediction failed: %s" % response.json()["error"])
    if response.status_code != 202:
        response.raise_for_status()

    # Still working. Honour Retry-After rather than guessing.
    time.sleep(int(response.headers.get("Retry-After", 5)))
Bash and curl

Needs jq to read the request id out of the response. -w '%{http_code}' prints the status code, which is what the polling loop switches on.

#!/usr/bin/env bash
set -euo pipefail

BASE='https://bio2byte.be/online_predictors/api'
TOKEN='your_token_here'

REQUEST_ID=$(curl -sS -X POST "$BASE/submit/single-sequence/" \
    -H 'Content-Type: application/json' \
    -d '{"token": "'"$TOKEN"'",
         "predictor_type": "dynamine",
         "sequence": ">INS_HUMAN\nMLSDEDFKAVFGMTRSAFANLPLWKQQNLKKEKGLF\n"}' \
    | jq -r '.request_id')
echo "submitted $REQUEST_ID"

while true; do
    CODE=$(curl -sS -o /tmp/b2b_result.json -w '%{http_code}' \
        "$BASE/results/$REQUEST_ID/")

    case "$CODE" in
        200) echo 'finished:'; cat /tmp/b2b_result.json; break ;;
        202) echo 'still running...'; sleep 5 ;;
        422) echo "failed: $(jq -r '.error' /tmp/b2b_result.json)" >&2; exit 1 ;;
        *)   echo "unexpected HTTP $CODE" >&2; cat /tmp/b2b_result.json >&2; exit 1 ;;
    esac
done
One-liner, no polling

Submit and print the response, to check your token and payload work:

curl -i -X POST https://bio2byte.be/online_predictors/api/submit/single-sequence/ \
     -H 'Content-Type: application/json' \
     -d '{"token": "your_token_here", "predictor_type": "dynamine", "sequence": "MLSDEDFKAVFGMTRSAFANLPLWKQQNLKKEKGLF"}'