Developer API

Script your own Track data with a personal API key: read your weeks, log hours, write summaries and set goals.

The Track API lets your own scripts read and update your own Track data: hours per category and sub-category, weekly summaries, goals and your list of sub-categories. Ratings and Kotu / Kyōtsu scores can be read too. Sync hours from Anki or a media player, back up your log, or build your own charts.

A key only ever reaches the data of the account that created it, and only the Track endpoints below. It can’t read or post comments, see notifications, change your profile or manage keys.

Quick start

  1. Open your profile (account menu → Your profile) and scroll down to Developers.
  2. Create a key: give it a name and pick Read only or Read & write.
  3. Copy the key. It’s shown once; only a fingerprint of it is stored.
  4. Try it:
export FAZON_API_KEY="fz_..."   # your key
curl -s -H "Authorization: Bearer $FAZON_API_KEY" https://api.fazon.nexus/v1/api/track/weeks

API keys

Authentication

Send the key in the Authorization header of every request:

Authorization: Bearer fz_...

Keys only work on the /v1/api/… endpoints on this page, and those endpoints only accept keys: being logged in to the site in a browser doesn’t count. A missing, malformed, unknown or revoked key gets 401 with a WWW-Authenticate: Bearer header.

Basics

Weeks, categories and hours

Weeks run from Sunday 03:00 UTC to the next Sunday 03:00 UTC (Saturday evening in the Americas). A week’s key is the Saturday before it starts, as YYYY-MM-DD: week 2026-10-10 runs from 2026-10-11T03:00:00Z to 2026-10-18T03:00:00Z. Anything you did before 03:00 UTC on a Sunday belongs to the previous week. You can read and write past weeks and the current week (back to 2000-01-01); a future week, a date that isn’t a Saturday or a malformed key gets 400.

The week’s 7 days are labelled by date, Saturday to Friday: day i runs from Sunday 03:00 UTC + i days for 24 hours, so each label is the date just before most of its window (the same calendar as the site).

The current week’s key in Python:

from datetime import datetime, timedelta, timezone

def week_key(t=None):
    """The key (Saturday label) of the Track week containing t, by default now."""
    t = (t or datetime.now(timezone.utc)) - timedelta(hours=3)
    sunday = t.date() - timedelta(days=(t.weekday() + 1) % 7)
    return (sunday - timedelta(days=1)).isoformat()

Categories (ids, in display order): active (active immersion), passive (passive immersion), phonetic (phonetic training), output, soroban, anki.

Sub-categories are yours to define per category (say Anime and YouTube under active). Hours go either to a sub-category or to the category’s unassigned bucket. Sub-categories can be added to one week at a time on the site, and a week’s form shows only the ones added to it or with hours in it. Names are shared between weeks: “Anime” is the same sub-category (one id) in every week that uses it, and giving a week hours for a sub-category puts it in that week.

Hours come back as decimal hours built from exact hundredths of an hour (1.17 = 1 h 10.2 min), so round(hours * 60) gives whole minutes. When you send hours you can use:

FormExampleMeaning
a number1.5, 0.75, -0.5decimal hours
a decimal string"1.5"the same
an "h:mm" string"1:30", "0:05", "-0:45"hours and minutes

Values are rounded to the nearest hundredth of an hour (36 seconds). Negative values are only allowed as an add. A week holds at most 168 hours per sub-category row and per category.

Ratings (1 😞, 2 😐, 3 👌, 4 🔥 per category) and Kotu / Kyōtsu daily scores are set on the site; the API reads them. A day’s Kyōtsu score is the one you entered, else the result of the Kyōtsu trainer sessions you finished that day.

The week object

Every week endpoint returns weeks in this shape (pretty-printed here):

{
  "week": "2026-10-03",
  "start": "2026-10-04T03:00:00Z",
  "end": "2026-10-11T03:00:00Z",
  "current": false,
  "logged": true,
  "totals": {
    "active": 13.75, "all": 18.75, "anki": 1, "nopassive": 14.75,
    "output": 0, "passive": 4, "phonetic": 0, "soroban": 0
  },
  "hours": {
    "active": {
      "unassigned": 2,
      "subcats": [
        {"id": 12, "name": "Anime", "archived": false, "hours": 10.5},
        {"id": 13, "name": "YouTube", "archived": false, "hours": 1.25}
      ]
    },
    "anki": {"unassigned": 0, "subcats": [{"id": 15, "name": "Core 2k", "archived": false, "hours": 1}]},
    "output": {"unassigned": 0, "subcats": []},
    "passive": {"unassigned": 4, "subcats": []},
    "phonetic": {"unassigned": 0, "subcats": []},
    "soroban": {"unassigned": 0, "subcats": []}
  },
  "ratings": {"active": 4},
  "goals": {"active": 12},
  "summary": "Finished my first show without subtitles.",
  "private": false,
  "kotu": 85,
  "kyotsu": 80,
  "updated": "2026-10-10T21:14:03Z"
}
Field
week, start, endthe week’s key and its exact start and end
currenttrue for the week running now
loggedtrue if anything is logged (hours, scores, a summary or ratings). Unlogged weeks are listed too, with zeros
totalshours per category, plus all (every category) and nopassive (all but passive)
hoursevery category: its unassigned hours and each sub-category with hours (id, name, archived, hours), in your display order
ratingsrated categories only ({} if none)
goalscategories with a goal for that week only ({} if none)
summary, privatethe week’s summary as Markdown source ("" if none; see below) and whether it’s hidden from others
kotu, kyotsuthe week’s averages of the daily percentages (1 decimal), null without any
updatedwhen the week last changed, null if it isn’t logged

Add ?days=1 to any week endpoint (reads and writes) for the day-by-day scores too:

{
  "days": ["2026-10-03", "2026-10-04", "2026-10-05", "2026-10-06", "2026-10-07", "2026-10-08", "2026-10-09"],
  "kotu_days": [80, null, 90, null, null, null, null],
  "kotu_source": ["manual", null, "manual", null, null, null, null],
  "kyotsu_days": [null, 70, 90, null, null, null, null],
  "kyotsu_source": [null, "manual", "auto", null, null, null, null]
}

kotu_source and kyotsu_source say where each day’s value comes from: "manual" (entered on the site), "auto" (the Kyōtsu trainer: its Tokyo mode for Kotu, its Keihan mode for Kyōtsu) or null (no value).

Endpoints

GET /v1/api/track/weeks

Your weeks in a range, oldest first, every week of the range whether logged or not.

Query
fromfirst week key (default: 11 weeks before to, so 12 weeks in all)
tolast week key (default: the current week)
days1 to include the day-by-day scores

At most 260 weeks (5 years) per request; from after to is a 400.

curl -s -H "Authorization: Bearer $FAZON_API_KEY" \
  "https://api.fazon.nexus/v1/api/track/weeks?from=2026-01-03&to=2026-10-10"
{"from": "2026-01-03", "to": "2026-10-10", "current_week": "2026-10-10", "weeks": [ ... ]}

GET /v1/api/track/weeks/{week}

One week (any past or current week, logged or not), as a week object.

curl -s -H "Authorization: Bearer $FAZON_API_KEY" \
  "https://api.fazon.nexus/v1/api/track/weeks/2026-10-10?days=1"

GET /v1/api/track/subcats

Your sub-categories, archived ones included, in display order (by category, then your order):

{"subcats": [
  {"id": 12, "category": "active", "name": "Anime", "archived": false, "position": 0},
  {"id": 13, "category": "active", "name": "YouTube", "archived": false, "position": 1},
  {"id": 15, "category": "anki", "name": "Core 2k", "archived": false, "position": 0}
]}

POST /v1/api/track/subcats

Read & write. Create a sub-category, with the site’s rules: a name of 1–40 characters (trimmed, no control characters) that isn’t already used by an active sub-category of that category (ignoring case). It isn’t added to any week until it gets hours there. Answers 201 with the new sub-category. A taken name or a full category is a 409.

curl -s -X POST -H "Authorization: Bearer $FAZON_API_KEY" -H "Content-Type: application/json" \
  -d '{"category":"active","name":"Anime"}' https://api.fazon.nexus/v1/api/track/subcats
{"id": 12, "category": "active", "name": "Anime", "archived": false, "position": 0}

Adding sub-categories to a week, renaming them and removing them is done on the site.

PATCH /v1/api/track/weeks/{week}/hours

Read & write. Change some of a week’s hours and nothing else. The body is a list of entries; each targets one row and either sets it or adds to it:

{"entries": [
  {"category": "active", "subcat": "anime", "add": "1:30"},
  {"category": "anki", "add": 0.75},
  {"category": "passive", "subcat_id": 14, "set": 3}
]}
Entry field
categoryrequired: one of the category ids
subcata sub-category name: matched against that category’s active (non-archived) sub-categories, ignoring case
subcat_idor a sub-category id (from GET …/subcats); this also reaches archived ones, for old weeks
(neither)the category’s unassigned hours
setthe row’s new value, ≥ 0
adda change to the row’s current value; may be negative, but the result can’t go below 0

Each entry needs exactly one of set or add, and at most one of subcat or subcat_id.

A sub-category name you don’t have is a 400 that says so; create it first with POST /v1/api/track/subcats:

{"error": "entries[0]: you have no sub-category named \"Drama\" in active; create it first with POST /v1/api/track/subcats {\"category\":\"active\",\"name\":\"Drama\"}"}

Answers the updated week object.

curl -s -X PATCH -H "Authorization: Bearer $FAZON_API_KEY" -H "Content-Type: application/json" \
  -d '{"entries":[{"category":"anki","add":"0:45"},{"category":"active","subcat":"Anime","add":"1:30"}]}' \
  https://api.fazon.nexus/v1/api/track/weeks/2026-10-10/hours

PUT /v1/api/track/weeks/{week}/summary

Read & write. Set the week’s summary and, optionally, whether it’s private. Nothing else changes.

Summaries are Markdown (GitHub-flavoured: headings, bold/italic, strikethrough, lists and task lists, quotes, code, tables, links; a single newline is a line break). The site shows them rendered, with raw HTML shown as text, only http/https/mailto links, and images shown as links. The API stores and returns the source as-is, so if you display summaries yourself, treat them as untrusted text and sanitise any HTML you render from them.

{"summary": "Finished my first show without subtitles.", "private": false}
Field
summaryrequired; Markdown (GitHub-flavoured), up to 10,000 characters (trimmed; "" clears it)
privateoptional; true hides the summary from everyone else. Leave it out to keep the current setting (new weeks start public)

Public summaries show on your profile, the leaderboard and the summaries feed once the week is published, as on the site. Answers the updated week object.

curl -s -X PUT -H "Authorization: Bearer $FAZON_API_KEY" -H "Content-Type: application/json" \
  -d '{"summary":"Lots of anime this week.","private":true}' \
  https://api.fazon.nexus/v1/api/track/weeks/2026-10-10/summary

POST /v1/api/track/weeks/{week}/goals

Read & write. Set this week’s goals: hours you aim for per category. The rules are the site’s:

curl -s -X POST -H "Authorization: Bearer $FAZON_API_KEY" -H "Content-Type: application/json" \
  -d '{"goals":{"active":35,"anki":3.5}}' \
  https://api.fazon.nexus/v1/api/track/weeks/2026-10-10/goals

Answers the week object. Goals are private while the week runs; once it’s published, the goal achievers board shows them if you met at least one.

Python example

Standard library only. Logs today’s study into the current week in one atomic request, then prints the last 12 weeks:

import json, os, urllib.error, urllib.request
from datetime import datetime, timedelta, timezone

API = "https://api.fazon.nexus/v1/api/track"
KEY = os.environ["FAZON_API_KEY"]

def call(method, path, body=None):
    req = urllib.request.Request(
        API + path, method=method,
        data=None if body is None else json.dumps(body).encode(),
        headers={"Authorization": "Bearer " + KEY, "Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        raise SystemExit(f"{e.code}: {json.load(e)['error']}")

def week_key(t=None):
    t = (t or datetime.now(timezone.utc)) - timedelta(hours=3)
    sunday = t.date() - timedelta(days=(t.weekday() + 1) % 7)
    return (sunday - timedelta(days=1)).isoformat()

week = call("PATCH", f"/weeks/{week_key()}/hours", {"entries": [
    {"category": "anki", "add": "0:45"},
    {"category": "active", "subcat": "Anime", "add": "1:30"},
]})
print(f"{week['week']}: {week['totals']['all']} h so far")

for w in call("GET", "/weeks")["weeks"]:
    print(w["week"], f"{w['totals']['active']:6.2f} h active", w["kotu"], w["kyotsu"])

Errors

StatusWhen
400invalid input: a bad week key or a future week, an unknown category or sub-category, a value out of range, invalid JSON. The message says what’s wrong
401no key, a malformed or unknown key, or a revoked one
403a read-only key used for a write
404no such endpoint (or the wrong method for it)
409a conflict: a sub-category name you already use, too many sub-categories, a goal that’s already set, goals for a past week
413request body over 64 KB
429rate limit; wait for Retry-After seconds
500something broke on our side; try again later