SessionHUD launches Friday, October 23. Downloads and licenses open that day.

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.

  1. 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.
  2. In Companion, add a connection using its Generic HTTP module.
  3. Have it request that address every 250–500 ms and read the answer as JSON. Put the take key on the button, showing a dash when it is empty or null. Turn the button red while status is "recording".
  4. 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 .local name (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

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

SettingValue
Base URLhttp://127.0.0.1:7373. Plain HTTP; there is no TLS.
Port7373 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 answersWhile 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 itThis 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.
AuthenticationNone. 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
}
KeyTypeMeaning
apinumberAlways 1 on this path. See Versioning.
appstringAlways "SessionHUD".
takeNumbernumber or nullThe take number. null when SessionHUD does not know it. It never guesses 0 or 1.
takestring or nullThe same number as text, without zero-padding: "7", not "07".
hasTakebooleantrue when there is a take number to show.
takeAuthoritystringHow sure SessionHUD is of the take. One of the authority values.
statusstring"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.
updatedAtstringWhen this answer was made, in ISO 8601 UTC.
recordingbooleantrue exactly when status is "recording".
sessionstring or nullThe open session’s name.
talentstring or nullWhat you typed in the Metadata window’s Talent field.
clientstring or nullWhat you typed in the Metadata window’s Client field.

Streaming /api/v1/now

Open a WebSocket on the same path instead of polling:

ws://127.0.0.1:7373/api/v1/now

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 keyMeaning
valueThe value. Absent, not null, when nothing is known.
sourceWhere it came from. See source.
authorityHow sure SessionHUD is. See authority. An unknown value is "unavailable".
timestampWhen 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:

KeyValueNotes
stateSchemaVersionnumberCurrently 2. Not a field.
ptslConnectedbooleanWhether SessionHUD is connected to Pro Tools.
ptslVersionnumberThe Pro Tools scripting interface version, such as 2026.
proToolsRunningbooleanWhether Pro Tools is running on this Mac.
transportstringSee transport.
recordModestringSee recordMode.
transportArmedbooleanWhether the transport’s record button is armed.
taketakeThe current take. See shapes below.
concurrentTakesarray of takeEvery take file of the current pass; more than one when several tracks are armed.
takeCanceledbooleantrue after a pass is aborted and its file deleted; take has fallen back to the take before it.
takeTrackstringThe 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.
sessionNamestringThe open session’s name.
sessionPath—Always "unavailable" here. File paths are not served.
sampleRatenumberIn hertz, such as 48000.
bitDepthstringSuch as "24-bit".
audioFormatstringSuch as "WAV".
timecodeRatestringSee timecodeRate.
sessionStartTimetimecodeThe session’s start time.
timelineIntimecodeThe timeline selection’s in point, not the playhead.
timecodetimecodeRunning 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.
elapsednumberSeconds since the current pass started recording.
markersarray of markerThe session’s memory locations.
currentMarkermarkerThe 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.
tracksarray of trackTracks with their record, selection and input-monitor state.
manualobjectFields 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

ShapeKeys
takenumber, trackName, fileName, filePath. filePath is always "" on this API; fileName is the bare file name.
timecodehours, minutes, seconds, frames, rate (a timecodeRate).
markernumber, name, isMarker; and when known comments, startSamples, endSamples (samples from session start) and trackName (track markers).
trackid, 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

ValueMeaning
authoritativeObserved directly and unambiguous.
highConfidenceObserved, with one benign assumption, such as elapsed time agreeing with a take file that is still growing.
inferredA 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.
staleWas true; its source has gone away, such as timecode after MTC stops. The value is kept.
unavailableNothing is known. There is no value.

source

ValueMeaning
ptslReported by Pro Tools.
filesystemRead from the take files in the session’s record folder.
mtcMIDI Time Code from Pro Tools’ MTC generator.
manualTyped by you in the Metadata window.
derivedWorked 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
ValueMeaning
unknownPro Tools reported a state SessionHUD does not recognise.
playingPlaying.
stoppedStopped.
recordingRecording.
playingHalfSpeedHalf-speed playback.
recordingHalfSpeedHalf-speed record.
fastForwardFast forward.
rewindRewind.
scrubScrubbing.
shuttleShuttling.
primedWaiting to start, record-ready.
isCueingCueing up to roll.
isCuedCued; about to roll. With transportArmed, a record pass is starting.
isCuedForPreviewCued for a preview.
isStoppingComing to rest.
isPreviewingPreviewing.

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

ValueMeaning
unknownNot recognised.
normalNormal record.
loopLoop record.
destructiveDestructive record.
quickPunchQuickPunch.
trackPunchTrackPunch.
destructivePunchDestructive punch.

timecodeRate

All values
ValueFrames per second
fps2397623.976
fps2424
fps2525
fps299729.97 non-drop
fps2997Drop29.97 drop
fps3030 non-drop
fps30Drop30 drop
fps4795247.952
fps4848
fps5050
fps599459.94 non-drop
fps5994Drop59.94 drop
fps6060 non-drop
fps60Drop60 drop
fps100100
fps11988119.88 non-drop
fps11988Drop119.88 drop
fps120120 non-drop
fps120Drop120 drop

Errors and limits

StatusWhen
200 OKThe answer, as application/json.
101 Switching ProtocolsA WebSocket on /api/v1/now opened.
400 Bad RequestA 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 ForbiddenThe 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 FoundAn unknown path, or a method other than GET on /api/v1/now or /api/v1/state. Nothing writes to Pro Tools.
500 Internal Server ErrorThe 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

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.