Documentation
API reference

Start here

Search every guideesc close

Developers

Use the API

Sign your script or add-on in with the device flow, get a token, and make your first API calls against your own server.

5 min read

Your tofa server has an HTTP API, and you're welcome to build against it: a Kodi add-on, a dashboard widget, a Python script that marks things watched. The full endpoint reference lives at API reference. This guide covers the part the reference alone doesn't: how your code signs in.

Two rules up front. Your code never asks anyone for a tofa password; sign-in always happens on our site, and your app only ever holds tokens. And everything here talks to your own server, so you can't break anything you couldn't break in the app.

01Find your server and its id#

Every server answers one endpoint without authentication. Ask it who it is:

curl http://192.168.1.50:33333/api/v1/auth/status
{
  "claimed": true,
  "server_id": "8b1f6a2e-4c3d-4f7a-9e0b-2d5c8a913f44",
  "connect_url": "https://api.tofa.tv"
}

You'll need both fields: server_id says which server your token should be scoped to, and connect_url is where sign-in happens.

Which address? Use whatever reaches your server today: the LAN IP and port, or a custom access URL if you've set one up. The API is the same on all of them.

02Request a device code#

Ask the connect service for a pairing code. Pass the server_id from step 1 and a name so the approval screen can say what's asking:

curl -X POST https://api.tofa.tv/device/code \
  -H "Content-Type: application/json" \
  -d '{"server_id": "8b1f6a2e-...", "client_name": "My watched-sync script", "client_type": "script"}'
{
  "device_code": "kf93k...40 characters...",
  "user_code": "ABCD-EFGH",
  "verification_uri": "https://app.tofa.tv/link",
  "verification_uri_complete": "https://app.tofa.tv/link?code=ABCDEFGH",
  "expires_in": 600,
  "interval": 5
}

Show the person the user_code and the link (the response also includes qr_code_svg if a QR code fits your UI better). They sign in at the link and approve. The code is good for ten minutes.

03Poll for the token#

While they approve, poll the token endpoint. This is the standard OAuth device flow, so the poll is a form post, not JSON:

curl -X POST https://api.tofa.tv/device/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=kf93k..."

Until they've approved you'll get authorization_pending; keep polling every interval seconds (poll faster and you'll get slow_down). Once approved:

{
  "access_token": "eyJhbGciOi...",
  "refresh_token": "d41d8cd98f...",
  "token_type": "Bearer",
  "expires_in": 2592000
}

Store both tokens. The access token is good for 30 days.

04Call your server#

From here it's plain Bearer auth against your own server:

curl http://192.168.1.50:33333/api/v1/users/me/continue \
  -H "Authorization: Bearer eyJhbGciOi..."

Everything in the API reference works this way, with one exception. Media URLs that a player fetches directly (playlists, segments, images) can't carry headers, so they authenticate with a short-lived token in the st query parameter instead. You don't build those URLs yourself: GET /stream/{id}/info negotiates playback and hands back ready-to-use URLs, and GET /auth/image-token covers artwork.

05Refresh before it expires#

When the access token ages out, trade the refresh token for a new pair instead of pairing again:

curl -X POST https://api.tofa.tv/servers/8b1f6a2e-.../device-token/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "d41d8cd98f..."}'

Refresh tokens rotate: each refresh returns a new one and retires the old. If a retired token is ever used again, the whole session family is revoked as a safety measure, so make sure you persist the newest pair before discarding the old.

Shortcut for server admins: API keys#

Since server 0.9.34, a server admin can skip the device flow for scripts that only talk to their own server. Under Server > Settings > API keys, create a key, give it a name and, if you want, an expiry. Copy it when it appears; it is shown once. Send it exactly like a token:

Authorization: Bearer <key>

A key acts as the admin who created it, so treat it like that admin's password: keep it in a secrets store or an environment variable, never in a repository, and revoke it from the same page the moment it leaks or the script retires. Revocation is immediate. Keys only work on a direct connection to your own server: on your network, through your own domain or tunnel. They are not accepted by our accounts service or by the remote access path it provides, so a script that runs away from home, needs to find servers, or acts for an ordinary member still uses the device flow above.

Right below your keys, Server > Settings > API shows a reference of every endpoint your server exposes, generated from the version you are running, with a filter and a copyable curl example for each call. That is the quickest way to find the exact path and parameters for the version you have.

The whole thing in Python#

A complete sign-in, first call included:

import json, time, urllib.request, urllib.parse

SERVER = "http://192.168.1.50:33333"

def post_json(url, data):
    req = urllib.request.Request(url, json.dumps(data).encode(),
                                 {"Content-Type": "application/json"})
    return json.load(urllib.request.urlopen(req))

# 1. who is this server?
status = json.load(urllib.request.urlopen(f"{SERVER}/api/v1/auth/status"))
connect, server_id = status["connect_url"], status["server_id"]

# 2. request a device code
code = post_json(f"{connect}/device/code",
                 {"server_id": server_id, "client_name": "example script"})
print(f"Go to {code['verification_uri']} and enter {code['user_code']}")

# 3. poll until approved
while True:
    time.sleep(code["interval"])
    form = urllib.parse.urlencode({
        "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
        "device_code": code["device_code"],
    }).encode()
    req = urllib.request.Request(f"{connect}/device/token", form,
        {"Content-Type": "application/x-www-form-urlencoded"})
    try:
        tokens = json.load(urllib.request.urlopen(req))
        break
    except urllib.error.HTTPError as e:
        if json.load(e).get("error") != "authorization_pending":
            raise

# 4. call the server
req = urllib.request.Request(f"{SERVER}/api/v1/users/me/continue",
    headers={"Authorization": f"Bearer {tokens['access_token']}"})
for item in json.load(urllib.request.urlopen(req)):
    print(item["title"], f"{item['progress_percent']}%")

Reach the server from anywhere#

The examples above use a LAN address. When your code runs away from home, or the server sits behind a network you can't open, ask the connect service how to reach it. With any account token (owner or invited user):

curl https://api.tofa.tv/servers/8b1f6a2e-.../connection-info \
  -H "Authorization: Bearer eyJhbGciOi..."
{
  "connection_type": "direct",
  "online": true,
  "connect_url": "https://8b1f6a2e....direct.tofa.tv:33333",
  "proxy_url": null,
  "proxy_available": false,
  "candidates": [
    { "type": "lan-direct", "url": "https://192-168-1-50.8b1f6a2e....lan.tofa.tv:33333", "stagger_ms": 0 },
    { "type": "wan-direct", "url": "https://8b1f6a2e....direct.tofa.tv:33333", "stagger_ms": 100 },
    { "type": "relay", "url": "https://api.tofa.tv/servers/8b1f6a2e-.../relay", "stagger_ms": 200, "available": true }
  ]
}

candidates is the list to use: try them in order (the stagger_ms offsets are what our own apps use to race them), and settle on the first that answers. Three types exist today, and unknown types may appear later, so skip entries you don't recognize rather than failing.

  • lan-direct — the server on its own network, with a hostname our wildcard certificate covers so TLS validates normally.
  • wan-direct — a port-forwarded or tunneled path from the internet. Custom access URLs the operator configured appear as extra wan-direct entries.
  • relay — the same API proxied through us, at https://api.tofa.tv/servers/{id}/relay. Append the normal API path: .../relay/api/v1/users/me/continue. Two things to know. It forwards the API only; the server's web interface is not there, so the root answers 404, and anything your code scrapes from the server's front page won't be found on this route. And until the server's relay channel is up you'll get 503 with error server_relay_not_connected; that clears on its own, so retry rather than giving up.

proxy_url is the older, pre-candidates form of the relay entry: it is only non-null when the server has no direct path at all. New code should read candidates and treat proxy_url as legacy.

Good citizenship#

  • One pairing per install. Pair once, store the tokens, refresh. Don't run the device flow on every start.
  • Respect the poll interval in step 3; the endpoint enforces it.
  • Feature-detect, don't version-match. GET /api/v1/system/info reports api_version and a capabilities list. Servers update on their own schedule, so check for the capability you need instead of assuming.
  • The API is in beta. Additive changes can land in any release. We won't break documented endpoints casually, but pin your expectations to the reference, not to observed behavior.

If you build something, tell us through help and feedback. We'd genuinely like to see it.