API
SessionHUD serves what it knows about the session as JSON, on this Mac or your studio network: the take number, whether Pro Tools is recording, and the session state behind the outputs. It is for Companion, tally lights, OBS scripts and your own tools.
Recipes
Show the take number on a Stream Deck button
SessionHUD has no Stream Deck plug-in. Use Bitfocus Companion, which drives a Stream Deck and can read this API.
- Copy the API address from SessionHUD ▸ Settings… ▸ Browser & API ▸ Links ▸
API. With Companion on the same Mac it is
http://127.0.0.1:7373/api/v1/now, and nothing else needs to be on. - In Companion, add a connection using its Generic HTTP module.
- Have it request that address every 250–500 ms and read the answer as JSON. Put the
takekey on the button, showing a dash when it is empty ornull. Turn the button red whilestatusis"recording". - If Companion runs on another computer, turn SessionHUD’s Browser button on and leave
Reachable from at Local network. Use the Mac’s IP
address, never its
.localname (see Connecting), and give the Mac a reserved IP address on your router so the address does not change.
Show the take number in OBS
- The easy way: turn the Browser button on and add a Browser Source at
http://127.0.0.1:7373/. That is the viewer page, drawn with SessionHUD’s own layout. Or pick SessionHUD as a camera. Neither needs the API. - A homemade HTML overlay, a local file or a page from another web server cannot read the API. Browsers block it (see From a web page).
- To put the values in OBS text sources or drive a REC tally, use Companion’s OBS
connection alongside the Generic HTTP one above, or an OBS Python script (Tools ▸ Scripts)
that polls
/api/v1/now. On macOS, OBS loads only a “framework” Python, such as the python.org installer, set in Tools ▸ Scripts ▸ Python Settings. OBS’s Lua scripts cannot make web requests.
A REC light or tally
Light it while status in /api/v1/now is
"recording", and turn it off for anything else. That includes cued pre-roll
on an armed transport, so the light comes on before the first sample is written. Treat no
answer as off. A Python version is under For scripts.
Show PLAY and STOP
/api/v1/now only says recording, canceled or
idle, and idle covers playing and stopped alike. For the transport
word, poll /api/v1/state about four times a second and read
transport (see transport values). Show a dash
when its authority is stale or unavailable.
curl -s http://127.0.0.1:7373/api/v1/state | jq -r '.transport.value // "–"'
Quick start
With SessionHUD running, from Terminal on the same Mac:
curl -s http://127.0.0.1:7373/api/v1/now
curl -s http://127.0.0.1:7373/api/v1/state | jq '.take'
/api/v1/now is the small answer most integrations want: the take, a
status word and the session name. /api/v1/state is the whole picture, where
every value says where it came from and how sure SessionHUD is of it. The same
/api/v1/now path also opens as a WebSocket that pushes each change.
Connecting
| Setting | Value |
|---|---|
| Base URL | http://127.0.0.1:7373. Plain HTTP; there is no TLS. |
| Port | 7373 unless you change it in Settings ▸ Browser & API. The address to use is in Settings ▸ Browser & API ▸ Links ▸ API, with a copy button. |
| When it answers | While SessionHUD is running and licensed, or in its evaluation. On this Mac it always answers. From other machines, only while the Browser button is on. The Browser button also turns the viewer page on and off. |
| Who can reach it | This Mac only while the Browser button is off. With it on, anyone on the local network — Reachable from in Settings ▸ Browser & API is Local network unless you set it to This Mac only. Local means a private address — 10.x, 172.16–31.x, 192.168.x, link-local, Tailscale’s 100.64–127.x and IPv6 unique-local; anything reaching the Mac from a public address is refused, port forward or not. The first time you press the Browser button, macOS may ask whether to allow incoming connections; allow them. |
| Authentication | None. There are no tokens, keys or pairing codes. |
Address the Mac by IP, not by name. Every request’s
Host header must be localhost or a literal IP address. A name such
as studio-mac.local gets 403 Forbidden. From another machine, use
the Mac’s address, for example http://192.168.1.20:7373/api/v1/now.
From a web page. Only SessionHUD’s own viewer page can read the API in a browser. A homemade HTML overlay, a local file or another web server is blocked. Scripts, Companion and OBS plug-ins are not affected.
GET /api/v1/now
The current take and status, flattened so a tool that can only bind a key to a button can use it.
{
"api": 1,
"app": "SessionHUD",
"takeNumber": 7,
"take": "7",
"hasTake": true,
"takeAuthority": "authoritative",
"status": "recording",
"updatedAt": "2026-10-06T14:32:07Z",
"recording": true,
"session": "Kestrel Radio 60",
"talent": "A. Okafor",
"client": null
}
| Key | Type | Meaning |
|---|---|---|
api | number | Always 1 on this path. See Versioning. |
app | string | Always "SessionHUD". |
takeNumber | number or null | The take number. null when SessionHUD does not know it. It never guesses 0 or 1. |
take | string or null | The same number as text, without zero-padding: "7", not "07". |
hasTake | boolean | true when there is a take number to show. |
takeAuthority | string | How sure SessionHUD is of the take. One of the authority values. |
status | string | "recording" during a record pass, including cued pre-roll on an armed transport. "canceled" after a pass is aborted (⌘.) and its file deleted, while the transport stays at rest. "idle" otherwise, playing included. For play and stop, read transport from /api/v1/state. |
updatedAt | string | When this answer was made, in ISO 8601 UTC. |
recording | boolean | true exactly when status is "recording". |
session | string or null | The open session’s name. |
talent | string or null | What you typed in the Metadata window’s Talent field. |
client | string or null | What you typed in the Metadata window’s Client field. |
- Show a dash when
takeNumberisnull. SessionHUD’s own outputs do the same. - The last take stays after a pass ends and during playback. It goes to
nullwhen SessionHUD can’t find one: no session is open, you arm a different track that has no takes yet, or Pro Tools is recording into a folder SessionHUD can’t see. - Before Pro Tools connects, SessionHUD’s screens may show a sample take. The API
never does; it answers
null. - When
statusturns"recording",takestill shows the previous take for a moment. The new number follows once Pro Tools starts writing. A canceled pass reads"canceled"a fraction of a second after the stop. If you name files by take, decide after a short wait.
Streaming /api/v1/now
Open a WebSocket on the same path instead of polling:
ws://127.0.0.1:7373/api/v1/now
- The first message is the current answer, in the same shape as the GET, as one JSON text frame. After that, one is sent for each change to the take, its authority, the status, the session or the names — never for timecode or for pressing Play.
- Send pings to detect a dead connection; the server answers them and sends none of its own. Anything else you send is ignored.
- It closes with code
1001when SessionHUD quits, when the Browser button moves the API on or off the network, or when you change the port or Reachable from. A connection can also simply drop. Reconnect after half a second, backing off to a few seconds.
GET /api/v1/state
Most integrations never need /state. Use it for the transport word, timecode
or markers. For a button or overlay you can ignore source and
authority; SessionHUD’s own screens do.
The full session state the outputs are drawn from. Every value is wrapped in a field that records where the value came from and how much to trust it:
{ "value": 7, "source": "filesystem", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z" }
| Field key | Meaning |
|---|---|
value | The value. Absent, not null, when nothing is known. |
source | Where it came from. See source. |
authority | How sure SessionHUD is. See authority. An unknown value is "unavailable". |
timestamp | When it was last observed, ISO 8601 UTC. A value never known carries 0001-01-01T00:00:00Z. |
The response is one object. stateSchemaVersion is a plain number,
manual is an object of fields, and every other key is a field:
| Key | Value | Notes |
|---|---|---|
stateSchemaVersion | number | Currently 2. Not a field. |
ptslConnected | boolean | Whether SessionHUD is connected to Pro Tools. |
ptslVersion | number | The Pro Tools scripting interface version, such as 2026. |
proToolsRunning | boolean | Whether Pro Tools is running on this Mac. |
transport | string | See transport. |
recordMode | string | See recordMode. |
transportArmed | boolean | Whether the transport’s record button is armed. |
take | take | The current take. See shapes below. |
concurrentTakes | array of take | Every take file of the current pass; more than one when several tracks are armed. |
takeCanceled | boolean | true after a pass is aborted and its file deleted; take has fallen back to the take before it. |
takeTrack | string | The armed track take numbers count on: the one picked in the Track: menu on SessionHUD’s controls bar when it is armed, otherwise the lowest-numbered armed track. Known before anything is recorded; unknown with nothing armed. |
sessionName | string | The open session’s name. |
sessionPath | — | Always "unavailable" here. File paths are not served. |
sampleRate | number | In hertz, such as 48000. |
bitDepth | string | Such as "24-bit". |
audioFormat | string | Such as "WAV". |
timecodeRate | string | See timecodeRate. |
sessionStartTime | timecode | The session’s start time. |
timelineIn | timecode | The timeline selection’s in point, not the playhead. |
timecode | timecode | Running timecode. From MTC while frames arrive (source mtc, authority authoritative); authority stale when they stop; source derived, authority inferred when worked out from the record start. |
elapsed | number | Seconds since the current pass started recording. |
markers | array of marker | The session’s memory locations. |
currentMarker | marker | The last marker at or before the running timecode. Known only while timecode is: during a record pass, or while Pro Tools’ MTC generator is sending to SessionHUD. Unknown rather than a nearest guess, so it is unknown with the transport stopped. |
tracks | array of track | Tracks with their record, selection and input-monitor state. |
manual | object | Fields you type in the Metadata window (the Metadata button, or File ▸ Metadata…): talent, role, client, project, engineer, note, and countdownTarget, the moment the countdown runs to, ISO 8601 UTC. Each is a field with source "manual". |
Value shapes
| Shape | Keys |
|---|---|
| take | number, trackName, fileName, filePath. filePath is always "" on this API; fileName is the bare file name. |
| timecode | hours, minutes, seconds, frames, rate (a timecodeRate). |
| marker | number, name, isMarker; and when known comments, startSamples, endSamples (samples from session start) and trackName (track markers). |
| track | id, name, index, isRecordEnabled, isSelected, isInputMonitoring, isAudioTrack; and channelCount when known. |
An optional key in a shape is left out when it has no value. A full response during a take:
{
"stateSchemaVersion": 2,
"ptslConnected": { "value": true, "source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z" },
"ptslVersion": { "value": 2026, "source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:20:51Z" },
"proToolsRunning": { "value": true, "source": "derived", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z" },
"transport": { "value": "recording", "source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z" },
"recordMode": { "value": "normal", "source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z" },
"transportArmed": { "value": true, "source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z" },
"take": {
"value": { "number": 7, "trackName": "VOX A", "fileName": "VOX A_07.wav", "filePath": "" },
"source": "filesystem", "authority": "authoritative", "timestamp": "2026-10-06T14:31:55Z"
},
"concurrentTakes": {
"value": [ { "number": 7, "trackName": "VOX A", "fileName": "VOX A_07.wav", "filePath": "" } ],
"source": "filesystem", "authority": "authoritative", "timestamp": "2026-10-06T14:31:55Z"
},
"takeCanceled": { "source": "derived", "authority": "unavailable", "timestamp": "0001-01-01T00:00:00Z" },
"takeTrack": { "value": "VOX A", "source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:31:40Z" },
"sessionName": { "value": "Kestrel Radio 60", "source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z" },
"sessionPath": { "source": "derived", "authority": "unavailable", "timestamp": "0001-01-01T00:00:00Z" },
"sampleRate": { "value": 48000, "source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z" },
"bitDepth": { "value": "24-bit", "source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z" },
"audioFormat": { "value": "WAV", "source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z" },
"timecodeRate": { "value": "fps24", "source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z" },
"sessionStartTime": {
"value": { "hours": 1, "minutes": 0, "seconds": 0, "frames": 0, "rate": "fps24" },
"source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z"
},
"timelineIn": {
"value": { "hours": 1, "minutes": 0, "seconds": 0, "frames": 0, "rate": "fps24" },
"source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z"
},
"timecode": {
"value": { "hours": 1, "minutes": 0, "seconds": 12, "frames": 7, "rate": "fps24" },
"source": "mtc", "authority": "authoritative", "timestamp": "2026-10-06T14:32:07Z"
},
"elapsed": { "value": 12.29, "source": "derived", "authority": "highConfidence", "timestamp": "2026-10-06T14:32:07Z" },
"markers": {
"value": [ { "number": 3, "name": "Scene 3", "startSamples": 480000, "isMarker": true } ],
"source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:31:40Z"
},
"currentMarker": {
"value": { "number": 3, "name": "Scene 3", "startSamples": 480000, "isMarker": true },
"source": "derived", "authority": "inferred", "timestamp": "2026-10-06T14:32:07Z"
},
"tracks": {
"value": [
{ "id": "1b7c0e2a", "name": "VOX A", "index": 1, "isRecordEnabled": true, "isSelected": true, "isInputMonitoring": true, "isAudioTrack": true, "channelCount": 1 },
{ "id": "5d90f3c4", "name": "MUSIC", "index": 2, "isRecordEnabled": false, "isSelected": false, "isInputMonitoring": false, "isAudioTrack": true, "channelCount": 2 }
],
"source": "ptsl", "authority": "authoritative", "timestamp": "2026-10-06T14:31:40Z"
},
"manual": {
"talent": { "value": "A. Okafor", "source": "manual", "authority": "authoritative", "timestamp": "2026-10-06T13:58:12Z" },
"role": { "source": "derived", "authority": "unavailable", "timestamp": "0001-01-01T00:00:00Z" },
"client": { "source": "derived", "authority": "unavailable", "timestamp": "0001-01-01T00:00:00Z" },
"project": { "source": "derived", "authority": "unavailable", "timestamp": "0001-01-01T00:00:00Z" },
"engineer": { "source": "derived", "authority": "unavailable", "timestamp": "0001-01-01T00:00:00Z" },
"note": { "source": "derived", "authority": "unavailable", "timestamp": "0001-01-01T00:00:00Z" },
"countdownTarget": { "value": "2026-10-06T16:00:00Z", "source": "manual", "authority": "authoritative", "timestamp": "2026-10-06T13:58:40Z" }
}
}
Fetch /state when you need it rather than polling it fast: it is built fresh
for every request and has no streaming form. For a live take and status, stream
/api/v1/now.
Values
Treat every list below as open. A later release can add a value, and a client should handle one it does not know as it would an unknown value, never as an error. Ignore keys you do not recognise.
authority
| Value | Meaning |
|---|---|
authoritative | Observed directly and unambiguous. |
highConfidence | Observed, with one benign assumption, such as elapsed time agreeing with a take file that is still growing. |
inferred | A real observation whose link to this moment is not proven, such as a take number read from the highest-numbered file in the record folder at rest, or timecode worked out from the record start. SessionHUD’s outputs show it like a sure value. |
stale | Was true; its source has gone away, such as timecode after MTC stops. The value is kept. |
unavailable | Nothing is known. There is no value. |
source
| Value | Meaning |
|---|---|
ptsl | Reported by Pro Tools. |
filesystem | Read from the take files in the session’s record folder. |
mtc | MIDI Time Code from Pro Tools’ MTC generator. |
manual | Typed by you in the Metadata window. |
derived | Worked out by SessionHUD. Also the source of a field nothing has ever filled. |
transport
The ones you will usually see are playing, stopped,
recording, and isCued during pre-roll.
All values
| Value | Meaning |
|---|---|
unknown | Pro Tools reported a state SessionHUD does not recognise. |
playing | Playing. |
stopped | Stopped. |
recording | Recording. |
playingHalfSpeed | Half-speed playback. |
recordingHalfSpeed | Half-speed record. |
fastForward | Fast forward. |
rewind | Rewind. |
scrub | Scrubbing. |
shuttle | Shuttling. |
primed | Waiting to start, record-ready. |
isCueing | Cueing up to roll. |
isCued | Cued; about to roll. With transportArmed, a record pass is starting. |
isCuedForPreview | Cued for a preview. |
isStopping | Coming to rest. |
isPreviewing | Previewing. |
To ask “is a take being recorded?”, read status from
/api/v1/now rather than testing transport yourself. It also
counts pre-roll on an armed transport, and it reads an unknown arm as not recording.
recordMode
| Value | Meaning |
|---|---|
unknown | Not recognised. |
normal | Normal record. |
loop | Loop record. |
destructive | Destructive record. |
quickPunch | QuickPunch. |
trackPunch | TrackPunch. |
destructivePunch | Destructive punch. |
timecodeRate
All values
| Value | Frames per second |
|---|---|
fps23976 | 23.976 |
fps24 | 24 |
fps25 | 25 |
fps2997 | 29.97 non-drop |
fps2997Drop | 29.97 drop |
fps30 | 30 non-drop |
fps30Drop | 30 drop |
fps47952 | 47.952 |
fps48 | 48 |
fps50 | 50 |
fps5994 | 59.94 non-drop |
fps5994Drop | 59.94 drop |
fps60 | 60 non-drop |
fps60Drop | 60 drop |
fps100 | 100 |
fps11988 | 119.88 non-drop |
fps11988Drop | 119.88 drop |
fps120 | 120 non-drop |
fps120Drop | 120 drop |
Errors and limits
| Status | When |
|---|---|
200 OK | The answer, as application/json. |
101 Switching Protocols | A WebSocket on /api/v1/now opened. |
400 Bad Request | A malformed request, a WebSocket upgrade without a key, headers over 16 KiB or a body over 4 KiB. |
401 Unauthorized | /api/v1/diagnostics, which is not part of the public API. |
403 Forbidden | The Host header is a name rather than localhost or an IP address, or a WebSocket was opened from a web page SessionHUD did not serve. |
404 Not Found | An unknown path, or a method other than GET on /api/v1/now or /api/v1/state. Nothing writes to Pro Tools. |
500 Internal Server Error | The answer could not be encoded. Retry, and send a Diagnostics export to support if it repeats. |
| No answer (connection refused) | From another machine while the Browser button is off or Reachable from is This Mac only, or from outside the local network. Also when SessionHUD isn’t running, or its evaluation has ended without a license. |
Error bodies are plain text. One request per connection, nothing cached. No rate limit, but polling faster than about four times a second gains nothing; use the WebSocket. Up to 32 connections can be open at once, and more are dropped. A connection that has not sent a complete request within 10 seconds is closed.
Versioning
- The
/api/v1/paths change only by addition: new keys, new values in the lists above, new fields in/state. A change that would break a client gets a new path. apiin/api/v1/nowandstateSchemaVersionin/api/v1/statego up only with such a change./api/v1/hud,/asset/andPOST /api/v1/pairbelong to the viewer page;/api/v1/diagnosticsalways answers401. Don’t build on any of them.- There are no write routes, and nothing here reaches Pro Tools. SessionHUD has no OSC, NDI or Stream Deck plug-in; Companion reads this API.
For scripts
Shell: the take, or a dash
curl -s http://127.0.0.1:7373/api/v1/now | jq -r '.take // "–"'
JavaScript: follow every change
Node 22 or later, which has WebSocket built in. Not from a web page; see
From a web page.
function follow(url, onNow) {
let delay = 500;
const open = () => {
const socket = new WebSocket(url);
socket.onopen = () => { delay = 500; };
socket.onmessage = (event) => onNow(JSON.parse(event.data));
socket.onclose = () => {
setTimeout(open, delay);
delay = Math.min(delay * 2, 5000);
};
};
open();
}
follow("ws://127.0.0.1:7373/api/v1/now", (now) => {
console.log(now.status, now.take ?? "–");
});
Python: a tally light
Standard library only. Replace the print with whatever drives your lamp:
import json, time, urllib.request
URL = "http://127.0.0.1:7373/api/v1/now"
lit = None
while True:
try:
with urllib.request.urlopen(URL, timeout=2) as response:
recording = json.load(response)["status"] == "recording"
except (OSError, ValueError, KeyError):
recording = False # SessionHUD not answering: this lamp goes dark
if recording != lit:
print("TALLY ON" if recording else "TALLY OFF")
lit = recording
time.sleep(0.25)
Next: Release notes · Support.