Documentation

Public API

Submit from scripts and training jobs, and embed leaderboards on your own site. Everything the web app does for submissions is available as JSON.

Authentication

Create a personal token under Settings → API tokens and send it as Authorization: Bearer hula_…. Tokens act as you: keep them out of notebooks you share. Public leaderboards and competition listings need no token; a token also unlocks boards that are visible to participants (or to you as a host).

Errors are JSON with an error message (written for humans — show it to your users) and the usual status codes: 400 invalid input, 401 missing/invalid token, 403 not allowed, 404 not found, 409 conflict, 413 file too large, 429 submission limit reached.

http
HTTP/1.1 429 Too Many Requests
{ "error": "Your team has used all 5 submissions for today. The limit resets at midnight UTC.", "code": "quota" }

Endpoints

EndpointAuthDescription
GET/api/v1/competitionsnonePublicly listed competitions with tracks, phase and dates.
GET/api/v1/competitions/{slug}optionalDetails, limits, submission format per track and the leaderboards you can see.
GET/api/v1/competitions/{slug}/leaderboardoptionalStandings. Query: track, split = public | private | <extra split>, format = json | csv. Respects the hosts' visibility settings for the caller. CORS-enabled.
GET/api/v1/competitions/{slug}/submissionstokenYour team's submissions (newest first). Query: track.
POST/api/v1/competitions/{slug}/submissionstokenCreate a submission. Body: { track?, fileName, sizeBytes, description?, answers? }. Returns { submission, upload: { url, method, headers } }.
POST/api/v1/submissions/{id}/finalizetokenCall after uploading the file; validates it and queues scoring. Idempotent.
GET/api/v1/submissions/{id}tokenStatus, scores on the splits you may see, and any validation error.
GET/api/v1/metokenThe account the token belongs to.

Leaderboards

The leaderboard endpoint returns exactly what the competition page shows the caller — the hosts decide whether public and private boards are visible to everyone, to participants or only to hosts, and nothing is revealed before they choose to. Responses carry Access-Control-Allow-Origin: *, so you can fetch them from the browser to embed standings.

bash
# Public leaderboard as JSON (no token needed when the board is public)
curl "https://hula.neuralprocessinglab.com/api/v1/competitions/libribrain-words/leaderboard?track=deep"

# …or as CSV
curl -o leaderboard.csv "https://hula.neuralprocessinglab.com/api/v1/competitions/libribrain-words/leaderboard?track=deep&format=csv"

Submitting with curl

Submitting is three calls: create the submission (we check your quota, the deadline and the file name), upload the bytes to the returned URL, then finalize. Large files are fine — compress them as .csv.gz or .zip. If the competition asks submission questions, pass the answers as {"answers": {"<question id>": "yes"}} (ids are listed by GET /competitions/{slug}).

bash
export HULA_TOKEN=hula_…   # Settings → API tokens

# 1. create the submission and get an upload URL
curl -s -X POST "https://hula.neuralprocessinglab.com/api/v1/competitions/libribrain-words/submissions" \
  -H "Authorization: Bearer $HULA_TOKEN" -H "Content-Type: application/json" \
  -d '{"track":"deep","fileName":"submission.csv.gz","sizeBytes":'$(wc -c < submission.csv.gz)',"description":"curl test"}'

# 2. PUT the file to upload.url (send the returned headers)
curl -X PUT --data-binary @submission.csv.gz -H "content-type: application/octet-stream" "<upload.url>"

# 3. finalize → queued for scoring
curl -s -X POST "https://hula.neuralprocessinglab.com/api/v1/submissions/<id>/finalize" -H "Authorization: Bearer $HULA_TOKEN"

# 4. poll until status is COMPLETE or FAILED
curl -s "https://hula.neuralprocessinglab.com/api/v1/submissions/<id>" -H "Authorization: Bearer $HULA_TOKEN"

Python helper

A complete end-to-end submit-and-wait helper using requests. Run it as HULA_TOKEN=… python submit.py submission.csv.gz.

python
import os, sys, time, requests

BASE = "https://hula.neuralprocessinglab.com/api/v1"
TOKEN = os.environ["HULA_TOKEN"]  # create one under Settings → API tokens
S = requests.Session()
S.headers["Authorization"] = f"Bearer {TOKEN}"

def submit(slug, path, track=None, description="", answers=None):
    """Upload a .csv / .csv.gz / .zip and wait for its score."""
    body = {"fileName": os.path.basename(path), "sizeBytes": os.path.getsize(path),
            "description": description, "answers": answers or {}}
    if track:
        body["track"] = track
    r = S.post(f"{BASE}/competitions/{slug}/submissions", json=body)
    if not r.ok:
        sys.exit(f"Could not create submission: {r.json().get('error')}")
    sub, up = r.json()["submission"], r.json()["upload"]
    with open(path, "rb") as f:  # stream the file straight to storage
        requests.request(up["method"], up["url"], data=f, headers=up["headers"]).raise_for_status()
    S.post(f"{BASE}/submissions/{sub['id']}/finalize").raise_for_status()
    while True:  # scoring usually takes a few seconds
        sub = S.get(f"{BASE}/submissions/{sub['id']}").json()["submission"]
        if sub["status"] in ("COMPLETE", "FAILED"):
            return sub
        time.sleep(3)

if __name__ == "__main__":
    result = submit("libribrain-words", sys.argv[1], track="deep", description="from the API")
    print(result["status"], result.get("error") or result["scores"])

Submission statuses

  • PENDING_UPLOAD — created, waiting for the file and the finalize call (expires after an hour).
  • QUEUED → RUNNING (code competitions) → SCORING — in progress.
  • COMPLETE — scores holds every split you may see, e.g. {"Public": {"balacc@10": 0.31}}.
  • FAILED — error says what was wrong; errorDetails lists line numbers and the first offending ids. Failed file submissions don't count toward your daily limit.