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/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
| Endpoint | Auth | Description |
|---|---|---|
GET/api/v1/competitions | none | Publicly listed competitions with tracks, phase and dates. |
GET/api/v1/competitions/{slug} | optional | Details, limits, submission format per track and the leaderboards you can see. |
GET/api/v1/competitions/{slug}/leaderboard | optional | Standings. 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}/submissions | token | Your team's submissions (newest first). Query: track. |
POST/api/v1/competitions/{slug}/submissions | token | Create a submission. Body: { track?, fileName, sizeBytes, description?, answers? }. Returns { submission, upload: { url, method, headers } }. |
POST/api/v1/submissions/{id}/finalize | token | Call after uploading the file; validates it and queues scoring. Idempotent. |
GET/api/v1/submissions/{id} | token | Status, scores on the splits you may see, and any validation error. |
GET/api/v1/me | token | The 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.
# 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}).
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.
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 —
scoresholds every split you may see, e.g.{"Public": {"balacc@10": 0.31}}. - FAILED —
errorsays what was wrong;errorDetailslists line numbers and the first offending ids. Failed file submissions don't count toward your daily limit.