Samklang

Samklang Server API.

Write your own client for Samklang Server.

Samklang Server is open. The Samklang apps are one client of it, and anyone may write another: an app for another platform, a wall panel, a script or a home automation. Find the server with Bonjour, pair once, then send and receive JSON lines over TLS.

This page is made from asyncapi.json (AsyncAPI 3.0).

The server makes every decision itself. A client shows the state it is sent and sends commands back; it never has to work anything out. (Even calibration works like that: the app only records sound, and the server does the rest.)

Overview

TransportTLS over TCP, port 7090 (-remote-port). Not HTTP
DiscoveryBonjour / mDNS, _samklang._tcp
Framingone JSON object per line (\n), both ways, UTF-8
Protocol version1
Securitya self-signed certificate you pin, pairing once (the first client of a new server needs no code; later ones a 4-digit code), then a token

A connection lives as long as you like. The server pushes the full state whenever something changes (and at once when you connect); you send commands whenever you like on the same connection.

Finding a server

Browse for _samklang._tcp. Each server advertises its name (the computer's name, or -server-name) and these TXT records:

KeyExampleMeaning
v1protocol version; skip servers with a version you don't know
id3f2ac81e90b4d7c2the first 16 hex digits of the certificate's SHA-256 fingerprint
ver0.9.3the server's own version
oslinuxdarwin, linux or windows

id stays the same for as long as the server keeps its settings folder, across restarts and new IP addresses: use it to recognise a server you have paired with.

Connecting

Open a TLS connection (TLS 1.2 or later) to the address and port from Bonjour. The certificate is self-signed, so normal verification fails: check it yourself instead.

Pairing and hello

The first line you send decides who you are. You have 20 seconds to send it; a first line that isn't pair or hello closes the connection.

Pairing (once). Try without a code first:

{"cmd":"pair","code":"","name":"Anna's tablet"}

A new server that nobody has paired with takes the first client without a code, during its first 30 minutes after starting: that client is its owner (a Raspberry Pi has no screen to show a code on). Starting the server again opens those 30 minutes again, as long as nobody has paired.

Otherwise the answer is code_needed, and the user types the server's 4-digit code into your app. It is shown in any paired client (pairing_code; Samklang shows it under Settings › Add a device), in the Mac's Samklang Server window, and in the log (remote control: pairing code 4719):

{"cmd":"pair","code":"4719","name":"Anna's tablet"}
{"type":"paired","token":"Vb3…","server":{"id":"3f2ac81e90b4d7c2","name":"Vardagsrum","version":"0.9.3","os":"linux","proto":"1"}}

Store token, which is secret: whoever has it controls the server. The code changes after every successful pairing.

Hello (every connection after):

{"cmd":"hello","token":"Vb3…"}
{"type":"hello","server":{"id":"3f2ac81e90b4d7c2","name":"Vardagsrum","version":"0.9.3","os":"linux","proto":"1"}}

An app on the same computer as the server (connecting to 127.0.0.1) may send {"cmd":"hello"} without a token, unless the server runs with -remote-local-pairing.

Errors end the connection:

{"type":"error","error":"code_needed","retry_after":0}
{"type":"error","error":"bad_code","retry_after":0}
{"type":"error","error":"locked","retry_after":60}
{"type":"error","error":"unknown_token"}

Commands and messages

After the handshake, send commands one per line. Every command has a cmd, and every message from the server a type.

Most commands get no reply: you see the result in the next state. A command the server can't carry out (or doesn't know) is written to its log and otherwise ignored. A few commands are answered, to you only, with the ref you sent (any string), so you can match the answer to the question: pairing_code, clock, satellite_firmware and the calibration commands.

Commands for a multiroom speaker act on the one playing (else Everywhere); name another with "group". A device is always named by its label.

Behaviour to rely on

Handshake

The first line you send decides who you are: pair once, hello every time after. See Pairing and hello.

Commands: pair, hello, pairing_code
From the server: paired, hello, error, pairing_code

pair command

The first line of a new client: pair, once, and get a token.

Only as the first line of a connection. Answered with paired, or with error (code_needed, bad_code or locked), which ends the connection. See Pairing and hello.

Answered with paired or error.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways pair.
codestringyesThe server's 4-digit pairing code, or "" to try without one (a new server takes its first client without a code).
namestringnoYour client's name, as the server's list of paired apps shows it (up to 100 bytes; "Remote" when empty).

Examples

Without a code

{"cmd":"pair","code":"","name":"Anna's tablet"}

With the code

{"cmd":"pair","code":"4719","name":"Anna's tablet"}

hello command

The first line of a paired client, every time after pairing.

Only as the first line of a connection. Answered with hello, or with error unknown_token, which ends the connection.

Answered with hello or error.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways hello.
tokenstringnoThe token from paired. Leave it out only from the server's own computer (127.0.0.1), unless the server runs with -remote-local-pairing.

Examples

With a token

{"cmd":"hello","token":"Vb3…"}

From the same computer

{"cmd":"hello"}

pairing_code command

The code another device pairs with now.

For an "add a device" screen (Samklang shows it under Settings › Add a device). Any paired client may ask.

Answered with pairing_code.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways pairing_code.
refstringnoAny string you like: the answer carries it back, so you can match the two.

Example

{"cmd":"pairing_code","ref":"p1"}

paired answer

Pairing worked: keep the token.

Store token together with the certificate's whole fingerprint. The pairing code changes after every successful pairing.

Answers pair.

Fields

FieldTypeRequiredMeaning
typestringyesAlways paired.
tokenstringyesYour token: send it in hello from now on. It is secret: whoever has it controls the server.
serverobject, fieldsyesThe server.

server: The server.

FieldTypeRequiredMeaning
idstringyesThe first 16 hex digits of its certificate's SHA-256 fingerprint (Bonjour's id).
namestringyesIts name.
versionstringyesSamklang Server's version.
osstringyesIts operating system: darwin, linux or windows.
protostringyesThe protocol version: 1.

Example

{
  "type": "paired",
  "token": "Vb3…",
  "server": {
    "id": "3f2ac81e90b4d7c2",
    "name": "Vardagsrum",
    "version": "0.9.3",
    "os": "linux",
    "proto": "1"
  }
}

hello answer

The handshake worked: the state follows.

Answers hello.

Fields

FieldTypeRequiredMeaning
typestringyesAlways hello.
serverobject, fieldsyesThe server.

server: The server.

FieldTypeRequiredMeaning
idstringyesThe first 16 hex digits of its certificate's SHA-256 fingerprint (Bonjour's id).
namestringyesIts name.
versionstringyesSamklang Server's version.
osstringyesIts operating system: darwin, linux or windows.
protostringyesThe protocol version: 1.

Example

{
  "type": "hello",
  "server": {
    "id": "3f2ac81e90b4d7c2",
    "name": "Vardagsrum",
    "version": "0.9.3",
    "os": "linux",
    "proto": "1"
  }
}

error answer

The handshake failed; the server closes the connection.

Answers pair or hello.

Fields

FieldTypeRequiredMeaning
typestringyesAlways error.
errorstringyescode_needed: the server is set up already; ask the user for its code. bad_code: wrong code. locked: too many wrong codes; try again after retry_after seconds. unknown_token: the token was unpaired (or the server's settings were reset); pair again.
retry_afterintegernoSeconds until pairing opens again. Sent with every answer to pair (0 unless locked); not with unknown_token.

Examples

Code needed

{"type":"error","error":"code_needed","retry_after":0}

Bad code

{"type":"error","error":"bad_code","retry_after":0}

Locked

{"type":"error","error":"locked","retry_after":60}

Unknown token

{"type":"error","error":"unknown_token"}

pairing_code answer

The answer to pairing_code.

Answers pairing_code.

Fields

FieldTypeRequiredMeaning
typestringyesAlways pairing_code.
refstringyesThe ref the command carried ("" when it had none).
codestringyesThe 4-digit code another device pairs with now.

Example

{"type":"pairing_code","ref":"p1","code":"4719"}

State

Right after the handshake you get the current covers and state, then a new message each time something changes.

From the server: state, artwork, group_artwork

state message

The whole picture: sent when you connect, and whenever it changes.

Replace what you had, don't merge. It is sent right after the handshake and then each time something changes (never when nothing did).

Several groups can play at once: two people playing to two groups, each its own song. Each playing group has its own now in groups. A device in two groups plays the group that started last; the other group plays on without it, and gets it back when that group stops.

Sent to every client.

Fields

FieldTypeRequiredMeaning
typestringyesAlways state.
speakerstringyesThe multiroom speaker (group) the top-level fields describe: the one playing (the first, when several are), else Everywhere. "" when there is none.
everywherebooleanyesWhether there is a multiroom speaker. When false, only settings, groups, devices, satellites, sound_boards and updates say anything.
playingbooleanyesWhether something plays on it (a paused stream still counts as playing).
pausedbooleanyesWhether it is paused.
can_controlbooleanyesWhether the sender (the iPhone or Mac playing to Samklang) takes transport and volume commands.
volumenumbernoThe sender's volume, 0 to 1. Missing until the sender has set one.
settingsobject, fieldsyesWhat the server looks for and offers.
groupsarray of objects, fieldsyesEvery multiroom speaker: Everywhere and the user's groups.
auto_syncbooleanyesAutomatic sync is on.
target_msintegeryesThe delay auto sync brings every device onto, in ms.
trackobject, fieldsnoWhat is playing. Missing when nothing is, or when the sender says nothing about it.
artwork_idintegeryesThe cover that goes with this state: the id of the last artwork.
membersarray of objects, fieldsyesThe devices playing now.
devicesarray of objects, fieldsyesEvery device the server knows, online or not.
satellitesarray of objects, fieldsnoThe Samklang satellites connected, with a speaker or not (one with a speaker is among the devices too). Missing when there are none.
sound_boardsarray of objects, fieldsnoThe sound boards a satellite can have, to offer. Missing on a server without satellite support.
updatesobject, fieldsnoWhat is new for Samklang Server and Samklang. Missing until the server has looked, and while check_updates is off.

settings: What the server looks for and offers.

FieldTypeRequiredMeaning
find_airplaybooleanyesUse paired AirPlay receivers, such as an Apple TV. Default on.
find_chromecastbooleanyesUse Chromecasts and Cast groups. Default on.
find_upnpbooleanyesUse UPnP, DLNA and Sonos renderers. Default off.
multiroombooleanyesOffer the multiroom speakers (they need AirPlay and Chromecast both on). Default on.
find_cast_groupsbooleanyesUse the speaker groups made in Google Home too; off, each Chromecast plays on its own. Default off.
find_bluetoothbooleanyesPlay to Bluetooth speakers paired with the server's computer. Default off: headphones paired with it would turn up too.
check_updatesbooleanyesLook for new versions of Samklang Server, Samklang and the satellites once a day. Default on. Off, nothing is fetched from Samklang's website.

groups[]: A multiroom speaker.

FieldTypeRequiredMeaning
namestringyesIts name: the key every group command uses.
auto_addbooleannoDevices seen for the first time join it.
autobooleanyesTrue for Everywhere: every device not left out. A user's group has members instead.
membersarray of stringsnoA user's group: its devices, by label.
playingbooleanyesWhether it plays (a paused stream still counts).
google_groupsbooleannoIts Chromecasts play through Google Home groups (see google_groups).
google_fitsarray of objects, fieldsnoThe Google Home groups that would (or do) play in place of its Chromecasts. Missing: the option isn't offered.
nowobject, fieldsnoWhat this group plays, while it plays (or its sender paused and left).

groups[].google_fits[]: A Google Home speaker group.

FieldTypeRequiredMeaning
namestringyesThe Google Home group's name.
speakersarray of stringsyesThe speakers it plays in place of.

groups[].now: What this group plays, while it plays (or its sender paused and left).

FieldTypeRequiredMeaning
pausedbooleanyesWhether it is paused.
can_controlbooleanyesWhether its sender takes transport and volume commands.
volumenumbernoThe sender's volume, 0 to 1. Missing until the sender has set one.
trackobject, fields as tracknoWhat is playing. Missing when nothing is, or when the sender says nothing about it.
artwork_idintegeryesIts cover, sent as group_artwork with this id.
membersarray of stringsyesThe devices it plays on now, by label.

track: What is playing. Missing when nothing is, or when the sender says nothing about it.

FieldTypeRequiredMeaning
titlestringyesThe track's title.
artiststringyesIts artist.
albumstringyesIts album.
duration_msintegernoIts length in ms, when known.
started_at_msintegernoWhile playing: when the track began, in Unix ms. Count on from it yourself (against the server's clock: see clock).
position_msintegernoWhile paused: where it stopped, in ms.

members[]: A device playing now.

FieldTypeRequiredMeaning
typestringyesHow the server reaches it: airplay, cast, upnp, bluetooth (paired with the server's computer) or sat (a satellite's speaker, or its headphone jack).
labelstringyesIts name in Samklang: the key every command uses.
kindstringyesWhat it is, to show ("Chromecast Audio", "AirPlay").
detailstringnoMore about it, to show ("Google Home group").
msintegeryesUnder auto sync its fine-tune, else its delay: what set changes.
delay_msintegeryesIts delay.
lag_msintegernoIts measured lag, when known.
connectedbooleanyesWhether it is receiving.
levelnumberyesIts volume relative to the sender's, 0 to 1.
timingstringyesmeasured (its lag is measured and corrected) or fixed (known exactly).
syncingbooleannoAuto sync is still bringing it onto the target.
speakersarray of objects, fieldsnoA Cast group's speakers, each with its own volume.

members[].speakers[]: One speaker of a playing Cast group.

FieldTypeRequiredMeaning
labelstringyesThe speaker's label.
levelnumberyesIts volume relative to the group's, 0 to 1.

devices[]: A device the server knows, online or not.

FieldTypeRequiredMeaning
typestringyesHow the server reaches it: airplay, cast, upnp, bluetooth (paired with the server's computer) or sat (a satellite's speaker, or its headphone jack).
labelstringyesIts name in Samklang: the key every command uses.
kindstringyesWhat it is, to show.
detailstringnoMore about it, to show.
includedbooleanyesEverywhere plays on it (see include).
groupbooleannoIt is a Cast group.
speakersarray of stringsnoA Cast group's speakers.
in_groupstringnoA speaker: the Cast group that plays it.
onlinebooleanyesSeen now.
last_seenstring (date-time)noWhen an offline device was last seen (RFC 3339).
shownbooleanyesIts own AirPlay speaker is offered (always true for AirPlay receivers). See show.
groupsarray of stringsnoThe user's groups it is in.
newbooleannoSeen for the first time, until a client sends seen_device.

satellites[]: A Samklang satellite: a Raspberry Pi Pico W or Pico 2 W that plays on a Bluetooth speaker.

FieldTypeRequiredMeaning
idstringyesThe satellite's id: the key the satellite commands use.
namestringyesIts name.
statestringyesnone (no speaker yet), connecting, ready or playing.
speakerstringnoThe speaker it plays on.
speaker_addrstringnoThat speaker's Bluetooth address.
foundarray of objects, fieldsnoBluetooth speakers it found after satellite_scan.
boardstringnoThe sound board the user said is attached: an id from the state's sound_boards. Missing for none.
pinsobject, fieldsnoA board wired by hand (i2s): its pins.
qualitystringnoIts speaker's sound as set: auto, standard or high.
xqbooleannoWhether its speaker plays SBC XQ now.
jackstringnoWith a sound board: its headphone jack's label, a device of its own.
chipstringnorp2040 (Pico W) or rp2350 (Pico 2 W).
updatestringnoIts firmware update: available (the user is asked; satellite_update approves), updating, or waiting (a developer's server, which updates as soon as the satellite doesn't play). Missing: none.
versionstringnoIts firmware's version. Missing on older firmware, or a developer's build.
update_tostringnoThe version an update brings.

satellites[].found[]: A speaker it found.

FieldTypeRequiredMeaning
addrstringyesIts Bluetooth address: what satellite_use takes.
namestringyesIts name.
rssiintegeryesIts signal strength, in dBm.

satellites[].pins: A board wired by hand (i2s): its pins.

FieldTypeRequiredMeaning
dataintegeryesThe data pin (GP number).
clockintegeryesThe bit clock's pin; the word clock is on the pin after.

sound_boards[]: A sound board a satellite can have.

FieldTypeRequiredMeaning
idstringyesIts id: what satellite_board takes.
namestringyesIts name, to show.
detailstringyesMore about it, to show.
custombooleannoWired by hand: ask the user for its pins.

updates: What is new for Samklang Server and Samklang. Missing until the server has looked, and while check_updates is off.

FieldTypeRequiredMeaning
serverobject, fieldsnoA newer Samklang Server, when there is one. It is updated on the computer it runs on.
appobject, fieldsnoSamklang's latest version, for Samklang itself; your own client can ignore it.
checkedstring (date-time)yesWhen the server last read the update file (RFC 3339).

updates.server: A newer Samklang Server, when there is one. It is updated on the computer it runs on.

FieldTypeRequiredMeaning
versionstringyesIts version.
urlstringnoWhere to get it.
notesstringnoWhat is new, to show.

updates.app: Samklang's latest version, for Samklang itself; your own client can ignore it.

FieldTypeRequiredMeaning
versionstringyesIts version.
urlstringnoWhere to get it.
notesstringnoWhat is new, to show.

Examples

Playing

{
  "type": "state",
  "speaker": "Vardagsrum",
  "everywhere": true,
  "playing": true,
  "paused": false,
  "can_control": true,
  "volume": 0.6,
  "settings": {
    "find_airplay": true,
    "find_chromecast": true,
    "find_upnp": false,
    "multiroom": true,
    "find_cast_groups": false,
    "find_bluetooth": false,
    "check_updates": true
  },
  "groups": [
    {
      "name": "Everywhere",
      "auto": true,
      "playing": false
    },
    {
      "name": "Vardagsrum",
      "auto": false,
      "members": [
        "Kök",
        "Sovrum"
      ],
      "playing": true,
      "now": {
        "paused": false,
        "can_control": true,
        "volume": 0.6,
        "track": {
          "title": "…",
          "artist": "…",
          "album": "…",
          "duration_ms": 215000,
          "started_at_ms": 1793366400000
        },
        "artwork_id": 3,
        "members": [
          "Kök",
          "Sovrum"
        ]
      }
    }
  ],
  "auto_sync": true,
  "target_ms": 2000,
  "track": {
    "title": "…",
    "artist": "…",
    "album": "…",
    "duration_ms": 215000,
    "started_at_ms": 1793366400000
  },
  "artwork_id": 3,
  "members": [
    {
      "type": "cast",
      "label": "Kök",
      "kind": "Chromecast Audio",
      "ms": -40,
      "delay_ms": 1960,
      "lag_ms": 1210,
      "connected": true,
      "level": 0.65,
      "timing": "measured"
    },
    {
      "type": "sat",
      "label": "Sovrum",
      "kind": "Samklang satellite",
      "ms": 0,
      "delay_ms": 2000,
      "connected": true,
      "level": 1,
      "timing": "fixed",
      "syncing": true
    }
  ],
  "devices": [
    {
      "type": "cast",
      "label": "Kök",
      "kind": "Chromecast Audio",
      "included": true,
      "online": true,
      "shown": true,
      "groups": [
        "Vardagsrum"
      ]
    },
    {
      "type": "sat",
      "label": "Sovrum",
      "kind": "Samklang satellite",
      "included": true,
      "online": true,
      "shown": true,
      "groups": [
        "Vardagsrum"
      ]
    },
    {
      "type": "upnp",
      "label": "Hall",
      "kind": "UPnP",
      "included": true,
      "online": false,
      "last_seen": "2026-10-30T13:02:11Z",
      "shown": true
    }
  ],
  "satellites": [
    {
      "id": "28CDC1008DC6",
      "name": "Hall",
      "state": "ready",
      "speaker": "ENEBY Portable",
      "speaker_addr": "6C:5C:3D:34:BC:5C",
      "found": [
        {
          "addr": "6C:5C:3D:34:BC:5C",
          "name": "ENEBY Portable",
          "rssi": -58
        }
      ],
      "board": "pimoroni-audio-pack",
      "quality": "auto",
      "xq": true,
      "jack": "Hall (jack)",
      "chip": "rp2350",
      "version": "1.0.0",
      "update": "available",
      "update_to": "1.1.0"
    }
  ],
  "sound_boards": [
    {
      "id": "pimoroni-audio-pack",
      "name": "Pimoroni Pico Audio Pack",
      "detail": "PCM5100A, line out and headphones (PIM544)"
    },
    {
      "id": "waveshare-pico-audio",
      "name": "Waveshare Pico-Audio",
      "detail": "…"
    },
    {
      "id": "i2s",
      "name": "Another I2S DAC or amplifier",
      "detail": "…",
      "custom": true
    }
  ],
  "updates": {
    "server": {
      "version": "1.1.0",
      "url": "https://samklang.wieoapps.com/setup.html",
      "notes": "…"
    },
    "app": {
      "version": "1.1.0",
      "url": "https://apps.apple.com/…"
    },
    "checked": "2026-11-02T09:00:00Z"
  }
}

No multiroom speaker

{
  "type": "state",
  "speaker": "",
  "everywhere": false,
  "playing": false,
  "paused": false,
  "can_control": false,
  "settings": {
    "find_airplay": true,
    "find_chromecast": false,
    "find_upnp": false,
    "multiroom": true,
    "find_cast_groups": false,
    "find_bluetooth": false,
    "check_updates": true
  },
  "groups": [],
  "auto_sync": false,
  "target_ms": 0,
  "artwork_id": 0,
  "members": [],
  "devices": [
    {
      "type": "airplay",
      "label": "Vardagsrum",
      "kind": "AirPlay",
      "included": true,
      "online": true,
      "shown": true
    }
  ]
}

artwork message

The cover of the track playing.

Sent once whenever it changes, before the state that carries the same artwork_id, and when you connect.

Sent to every client.

Fields

FieldTypeRequiredMeaning
typestringyesAlways artwork.
idintegeryesIts id: the state's artwork_id names it.
jpegstringyesThe JPEG, base64. "" when there is none.

Example

{"type":"artwork","id":3,"jpeg":"<base64>"}

group_artwork message

The cover of one playing group.

Sent when it changes, before the state whose groups[].now.artwork_id names it, and when you connect.

Sent to every client.

Fields

FieldTypeRequiredMeaning
typestringyesAlways group_artwork.
groupstringyesThe group's name.
idintegeryesIts id: the group's now.artwork_id names it.
jpegstringyesThe JPEG, base64. "" when there is none.

Example

{"type":"group_artwork","group":"Kök","id":7,"jpeg":"<base64>"}

Playing

Commands for a multiroom speaker act on the one playing (else Everywhere); name another with group. A device is always named by its label.

Commands: transport, volume, level, resync

transport command

Play/pause, next or previous on the sender.

Only does something when the state's can_control is true.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways transport.
actionstringyesplaypause, next or previous.
groupstringnoThe multiroom speaker (group) it is for. Default: the one playing, else Everywhere.

Example

{"cmd":"transport","action":"playpause"}

volume command

The sender's volume.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways volume.
valuenumberyes0 to 1.
groupstringnoThe multiroom speaker (group) it is for. Default: the one playing, else Everywhere.

Example

{"cmd":"volume","value":0.6}

level command

One device's volume relative to the sender's.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways level.
labelstringyesThe device, by its label; also a speaker inside a playing Cast group.
valuenumberyes0 to 1 (outside is clamped).
groupstringnoThe multiroom speaker (group) it is for. Default: the one playing, else Everywhere.

Example

{"cmd":"level","label":"Kök","value":0.65}

resync command

Bring one playing device back into sync now.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways resync.
labelstringyesThe device, by its label.
groupstringnoThe multiroom speaker (group) it is for. Default: the one playing, else Everywhere.

Example

{"cmd":"resync","label":"Kök"}

Sync

With auto sync on, the server measures each device's lag and brings every device onto the same delay, target_ms; you can fine-tune each one. With it off, you set each device's delay yourself.

Commands: auto_sync, set, clock
From the server: clock

auto_sync command

Automatic sync on or off.

For every group. Ignored while there is no multiroom speaker.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways auto_sync.
onbooleanyesOn or off.

Example

{"cmd":"auto_sync","on":true}

set command

A device's fine-tune (under auto sync) or its delay.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways set.
labelstringyesThe device, by its label.
msintegeryesUnder auto sync the fine-tune, −2000 to 2000; without it the delay, 0 to 5000. Out of range is ignored.
groupstringnoThe multiroom speaker (group) it is for. Default: the one playing, else Everywhere.

Example

{"cmd":"set","label":"Kök","ms":-40}

clock command

Ask for the server's clock.

For anything timed against the server (track progress, calibration). Halve the round trip to estimate the offset. While calibrating, it also tells the server you are still there.

Answered with clock.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways clock.
tnumberyesYour own clock, in Unix ms.
refstringnoAny string you like: the answer carries it back, so you can match the two.

Example

{"cmd":"clock","t":1793366400123.5,"ref":"c1"}

clock answer

The answer to clock.

Answers clock.

Fields

FieldTypeRequiredMeaning
typestringyesAlways clock.
refstringyesThe ref the command carried ("" when it had none).
tnumberyesYour t, sent back.
server_msnumberyesThe server's clock when it answered, in Unix ms.

Example

{"type":"clock","ref":"c1","t":1793366400123.5,"server_ms":1793366400119.2}

Devices

A device is always named by its label.

Commands: include, show, rename, seen_device, forget_device

include command

Leave a device out of Everywhere, or take it back.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways include.
labelstringyesThe device, by its label.
onbooleanyesfalse leaves it out, true takes it back.

Example

{"cmd":"include","label":"Kök","on":false}

show command

Hide (or show) a device's own AirPlay speaker.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways show.
labelstringyesThe device, by its label.
onbooleanyesShow it.

Example

{"cmd":"show","label":"Kök","on":false}

rename command

Rename a device in Samklang. The server restarts.

Its AirPlay speaker gets the new name too. The server restarts: reconnect and say hello again.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways rename.
labelstringyesThe device, by its label.
namestringyesIts new name. Not one another device or group has.

Example

{"cmd":"rename","label":"Kök","name":"Köket"}

seen_device command

The user has seen a new device: it is no longer marked new.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways seen_device.
labelstringyesThe device, by its label.

Example

{"cmd":"seen_device","label":"Kök"}

forget_device command

Drop an offline device from the list.

It comes back if it is seen again.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways forget_device.
labelstringyesThe device, by its label.

Example

{"cmd":"forget_device","label":"Kök"}

Groups

Everywhere plays on every device not left out. The user's groups play on their own devices, each as its own AirPlay speaker.

Commands: save_group, delete_group, group_member, google_groups

save_group command

Make a group, or change one.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways save_group.
oldstringno"" (or left out) to make a group; a group's name to change that group.
namestringyesThe group's name. A new name gives it a new AirPlay speaker.
membersarray of stringsyesIts devices, by label.
auto_addbooleannoNew devices join it.

Example

{"cmd":"save_group","old":"","name":"Downstairs","members":["Kök","Sovrum"],"auto_add":false}

delete_group command

Delete a group.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways delete_group.
groupstringyesThe group's name.

Example

{"cmd":"delete_group","group":"Downstairs"}

group_member command

Put a device in a group, or take it out.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways group_member.
groupstringyesThe group's name.
labelstringyesThe device, by its label.
onbooleanyesIn or out.

Example

{"cmd":"group_member","group":"Downstairs","label":"Kök","on":true}

google_groups command

Play a group's Chromecasts through Google Home groups.

The Google Home groups used are the ones in the group's google_fits.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways google_groups.
groupstringyesThe group's name.
onbooleanyesOn or off.

Example

{"cmd":"google_groups","group":"Downstairs","on":true}

Settings

What the server looks for. Changing them restarts the server.

Commands: settings

settings command

Change what the server looks for. The server restarts.

Send all of them (start from the state's settings). The server saves them and restarts: your connection drops; reconnect and say hello again. Changing only check_updates doesn't restart it, and the same settings again do nothing.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways settings.
settingsobject, fieldsyesAll the settings. One you leave out keeps its default (not its current value), so start from the state's settings.

settings: All the settings. One you leave out keeps its default (not its current value), so start from the state's settings.

FieldTypeRequiredMeaning
find_airplaybooleannoUse paired AirPlay receivers, such as an Apple TV. Default on.
find_chromecastbooleannoUse Chromecasts and Cast groups. Default on.
find_upnpbooleannoUse UPnP, DLNA and Sonos renderers. Default off.
multiroombooleannoOffer the multiroom speakers (they need AirPlay and Chromecast both on). Default on.
find_cast_groupsbooleannoUse the speaker groups made in Google Home too; off, each Chromecast plays on its own. Default off.
find_bluetoothbooleannoPlay to Bluetooth speakers paired with the server's computer. Default off: headphones paired with it would turn up too.
check_updatesbooleannoLook for new versions of Samklang Server, Samklang and the satellites once a day. Default on. Off, nothing is fetched from Samklang's website.

Example

{
  "cmd": "settings",
  "settings": {
    "find_airplay": true,
    "find_chromecast": true,
    "find_upnp": false,
    "find_bluetooth": false,
    "multiroom": true,
    "find_cast_groups": false,
    "check_updates": true
  }
}

Satellites

A satellite is a Raspberry Pi Pico W (or Pico 2 W) that plays Samklang on a Bluetooth speaker. Connected ones are in the state's satellites. A satellite with a speaker is among the devices too.

Its software: version is the firmware's version, and update says whether new firmware waits; update_to is the version it brings. Nothing is installed until a client sends satellite_update.

Its speaker's sound: quality is the setting, and xq whether the speaker plays SBC XQ now. High asks the speaker for SBC XQ (dual channel at bitpool 47, about 551 kbit/s against 328): better sound, from any speaker that takes it, at about 1.5 times the Wi-Fi and Bluetooth; one that doesn't plays standard SBC. Automatic is high on a Pico 2 W and standard on a Pico W, whose Wi-Fi has less to spare.

A satellite can have analog sound: a sound board (an I2S DAC or amplifier) with a headphone jack. Its jack is then a device of its own, named in jack: in the same group as the speaker, or in another playing another song. No board can be detected, so the user turns analog sound on and says which. Two different songs at once need twice the Wi-Fi, which a Pico W may not have.

The boards to offer come in the state's sound_boards. Show them as they come: the server knows the boards (their pins) and sends the satellite the pins, so a board added to the server works without new apps or firmware. One with custom needs the pins from the user: the data pin and the bit clock, the word clock on the pin after.

Getting a new satellite onto the Wi-Fi happens before it reaches the server, over the satellite's own setup network; that part isn't in this API.

Commands: satellite_scan, satellite_use, satellite_quality, satellite_board, satellite_update, satellite_firmware
From the server: satellite_firmware

satellite_scan command

Look for Bluetooth speakers in pairing mode.

They appear in its found.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways satellite_scan.
idstringyesThe satellite's id.

Example

{"cmd":"satellite_scan","id":"28CDC1008DC6"}

satellite_use command

Play on that speaker from now on.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways satellite_use.
idstringyesThe satellite's id.
addrstringyesThe speaker's Bluetooth address, from found.

Example

{"cmd":"satellite_use","id":"28CDC1008DC6","addr":"6C:5C:3D:34:BC:5C"}

satellite_quality command

Its speaker's sound. It connects to the speaker again.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways satellite_quality.
idstringyesThe satellite's id.
qualitystringyeshigh (SBC XQ when the speaker takes it), standard or auto.

Example

{"cmd":"satellite_quality","id":"28CDC1008DC6","quality":"high"}

satellite_board command

Analog sound on, with that board, or off.

The satellite remembers it and restarts. Pins wired by hand must be GP0–22 or GP26–28 (the radio has GP23–25), with the word clock on the pin after the bit clock and the data pin on neither clock: the example is data on GP18, bit clock GP16, word clock GP17.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways satellite_board.
idstringyesThe satellite's id.
boardstringyesAn id from the state's sound_boards; "" (or none) for off.
dataintegernoFor a board wired by hand (custom): the data pin, a GP number.
clockintegernoFor a board wired by hand: the bit clock's pin. The word clock goes on the pin after.

Examples

A known board

{"cmd":"satellite_board","id":"28CDC1008DC6","board":"pimoroni-audio-pack"}

Wired by hand

{"cmd":"satellite_board","id":"28CDC1008DC6","board":"i2s","data":18,"clock":16}

satellite_update command

The user approves the satellite's waiting firmware update.

It starts at once, and music on that satellite stops; the satellite restarts into the new firmware.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways satellite_update.
idstringyesThe satellite's id.

Example

{"cmd":"satellite_update","id":"28CDC1008DC6"}

satellite_firmware command

The firmware to write to a Pico plugged into your computer.

What makes a Pico a new satellite: the firmware this server carries, so it needs no update when it first connects, with no Wi-Fi settings and its old pairings erased. Copy the UF2 to the Pico in its boot mode; it then opens its setup network.

Answered with satellite_firmware.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways satellite_firmware.
chipstringyesrp2040 (Pico W) or rp2350 (Pico 2 W). Anything else counts as rp2040.
refstringnoAny string you like: the answer carries it back, so you can match the two.

Example

{"cmd":"satellite_firmware","chip":"rp2350","ref":"5"}

satellite_firmware answer

The answer to satellite_firmware.

Either image and uf2, or error.

Answers satellite_firmware.

Fields

FieldTypeRequiredMeaning
typestringyesAlways satellite_firmware.
refstringyesThe ref the command carried ("" when it had none).
chipstringyesThe chip it is for. One of rp2040, rp2350.
imagestringnoThe firmware image's id (its CRC-32, in hex).
uf2stringnoThe UF2 file, base64.
errorstringnoWhy there is none (this server carries no firmware for that chip, say).

Example

{"type":"satellite_firmware","ref":"5","chip":"rp2350","image":"9a3c51f0","uf2":"<base64>"}

Calibration

Calibration measures with a microphone how late each speaker really is, and fixes it. Your app only plays the part a microphone can: it records the moment the server says, and sends the recording back. The server finds the chime in it and works everything out.

  1. Start: calibrate with "on":true. The answer lists the speakers.
  2. For each speaker, ask for a chime.
  3. Record from record_from_ms to record_to_ms (server time: use clock), and send it with heard.
  4. Leave a speaker out with calibration_skip; start over with calibration_reset.
  5. See the result with calibration_result.
  6. Apply it with calibration_apply.
  7. End: calibrate with "on":false, and "restore":true to put back the fine-tunes from before.

Send clock now and then while calibrating: it tells the server you are still there. A calibration left alone for two minutes ends.

Commands: calibrate, chime, heard, calibration_skip, calibration_reset, calibration_result, calibration_apply, calibration_note
From the server: calibration, chime, heard, calibration_result, not_calibrating

calibrate command

Start or end a calibration of a group.

Needs auto sync on.

Answered with calibration.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways calibrate.
groupstringyesThe group to calibrate.
onbooleanyesStart (true) or end (false).
restorebooleannoEnding: put back the fine-tunes from before.
refstringnoAny string you like: the answer carries it back, so you can match the two.

Examples

Start

{"cmd":"calibrate","group":"Vardagsrum","on":true,"ref":"1"}

End

{"cmd":"calibrate","group":"Vardagsrum","on":false}

chime command

Play the chime on one speaker.

The answer says when to record.

Answered with chime.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways chime.
groupstringyesThe group being calibrated.
labelstringyesThe speaker, by its label; * plays it on every device at once (not measured).
refstringnoAny string you like: the answer carries it back, so you can match the two.

Example

{"cmd":"chime","group":"Vardagsrum","label":"Kök","ref":"2"}

heard command

Send the recording of a chime.

Record from record_from_ms to record_to_ms of the chime answer (server time: use clock).

Answered with heard or not_calibrating.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways heard.
groupstringyesThe group being calibrated.
labelstringyesThe speaker the chime was for.
start_msnumbernoThe server time of the first sample, in Unix ms.
ratenumbernoThe sample rate in Hz (8000 and up).
pcmstringnoThe recording: 16-bit little-endian mono PCM, base64, up to 15 s at 48 kHz.
missingbooleannoThere is no recording: send this instead of pcm.
notestringnoWith missing: why, for the log. With calibration_note: what the app made of a chime.
refstringnoAny string you like: the answer carries it back, so you can match the two.

Examples

A recording

{
  "cmd": "heard",
  "group": "Vardagsrum",
  "label": "Kök",
  "start_ms": 1793366400750.0,
  "rate": 48000,
  "pcm": "<base64>",
  "ref": "3"
}

No recording

{"cmd":"heard","group":"Vardagsrum","label":"Kök","missing":true,"note":"no microphone","ref":"3"}

calibration_skip command

Leave a speaker out of the result.

No answer, unless the group isn't being calibrated.

Answered with not_calibrating.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways calibration_skip.
groupstringyesThe group being calibrated.
labelstringyesThe speaker.

Example

{"cmd":"calibration_skip","group":"Vardagsrum","label":"Kök"}

calibration_reset command

Forget what was measured so far.

No answer, unless the group isn't being calibrated.

Answered with not_calibrating.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways calibration_reset.
groupstringyesThe group being calibrated.

Example

{"cmd":"calibration_reset","group":"Vardagsrum"}

calibration_result command

See the result so far.

Answered with calibration_result or not_calibrating.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways calibration_result.
groupstringyesThe group being calibrated.
refstringnoAny string you like: the answer carries it back, so you can match the two.

Example

{"cmd":"calibration_result","group":"Vardagsrum","ref":"4"}

calibration_apply command

Set the fine-tunes the calibration found.

Answered like calibration_result.

Answered with calibration_result or not_calibrating.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways calibration_apply.
groupstringyesThe group being calibrated.
refstringnoAny string you like: the answer carries it back, so you can match the two.

Example

{"cmd":"calibration_apply","group":"Vardagsrum","ref":"5"}

calibration_note command

What the app made of a chime, for the server's log.

No answer.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways calibration_note.
labelstringyesThe speaker.
notestringyesWhat the app heard (the log keeps 300 bytes).

Example

{"cmd":"calibration_note","label":"Kök","note":"…"}

calibration answer

The answer to calibrate.

Answers calibrate.

Fields

FieldTypeRequiredMeaning
typestringyesAlways calibration.
refstringyesThe ref the command carried ("" when it had none).
onbooleanyesWhether a calibration runs now.
speakersarray of strings or nullnoStarted: the speakers to calibrate, by label.
target_msintegernoStarted: the group's target delay, in ms.
errorstringnoWhy it didn't start: auto sync is off, no such group, …

Examples

Started

{"type":"calibration","ref":"1","on":true,"speakers":["Kök","Sovrum"],"target_ms":2000}

Refused

{"type":"calibration","ref":"1","on":false,"error":"auto sync is off"}

chime answer

The answer to chime: when to record.

Either the times, or error.

Answers chime.

Fields

FieldTypeRequiredMeaning
typestringyesAlways chime.
refstringyesThe ref the command carried ("" when it had none).
labelstringyesThe speaker.
at_msnumbernoWhen the chime starts at the live point (server time, Unix ms).
target_msintegernoThe group's target delay, in ms.
offset_msintegernoThe speaker's fine-tune, in ms.
record_from_msnumbernoRecord from this server time (Unix ms)…
record_to_msnumberno…to this one.
errorstringnoWhy there is no chime (no such group, everywhere: not calibrating, …).

Example

{
  "type": "chime",
  "ref": "2",
  "label": "Kök",
  "at_ms": 1793366401250.0,
  "target_ms": 2000,
  "offset_ms": 0,
  "record_from_ms": 1793366400750.0,
  "record_to_ms": 1793366407750.0
}

heard answer

The answer to heard.

Answers heard.

Fields

FieldTypeRequiredMeaning
typestringyesAlways heard.
refstringyesThe ref the command carried ("" when it had none).
labelstringyesThe speaker.
heardbooleanyesWhether the chime was found in the recording.
messagestringnoSomething to show the user about it.

Example

{"type":"heard","ref":"3","label":"Kök","heard":true,"message":"…"}

calibration_result answer

The answer to calibration_result and calibration_apply.

Answers calibration_result or calibration_apply.

Fields

FieldTypeRequiredMeaning
typestringyesAlways calibration_result.
refstringyesThe ref the command carried ("" when it had none).
speakersarray of objects or null, fieldsyesEach speaker asked for, in order.
measuredintegeryesHow many speakers were heard.
can_applybooleanyesWhether there is something to apply.
appliedbooleanyesWhether it was applied.

speakers[]: A speaker.

FieldTypeRequiredMeaning
labelstringyesThe speaker.
heardbooleanyesWhether its chime was heard.
detailstringyesskipped, heard, heard the latest or heard N ms early, to show.
fix_msintegernoThe fine-tune change it needs, in ms.

Example

{
  "type": "calibration_result",
  "ref": "4",
  "speakers": [
    {
      "label": "Kök",
      "heard": true,
      "detail": "…",
      "fix_ms": -35
    }
  ],
  "measured": 2,
  "can_apply": true,
  "applied": false
}

not_calibrating answer

The answer to a calibration command for a group that isn't being calibrated.

Its type is the command's name.

Answers heard, calibration_skip, calibration_reset, calibration_result or calibration_apply.

Fields

FieldTypeRequiredMeaning
typestringyesThe command's name. One of heard, calibration_skip, calibration_reset, calibration_result, calibration_apply.
labelstringyesThe command's label ("" when it had none).
errorstringyesAlways not calibrating.
refstringyesThe ref the command carried ("" when it had none).

Example

{"type":"calibration_skip","label":"Kök","error":"not calibrating","ref":""}

Updates

Once a day (unless check_updates is off) the server reads Samklang's signed update file and puts what is new in the state's updates:

"updates": {"server":{"version":"1.1.0","url":"https://samklang.wieoapps.com/setup.html","notes":"…"},
            "app":{"version":"1.1.0","url":"https://apps.apple.com/…"},
            "checked":"2026-11-02T09:00:00Z"}

server is there only when a newer Samklang Server exists: show it with its notes; the server is updated on the computer it runs on. app is Samklang's latest version, for Samklang itself; your own client can ignore it. Satellites' firmware updates are in each satellite's update (see Satellites).

Local only

Clients on the server's own computer (connecting to 127.0.0.1) also get the pairing code and the paired apps, so they can show them (as the Mac's Samklang Server window does), and may unpair an app.

Commands: forget_remote
From the server: pairing, remotes

forget_remote command

Unpair an app; its connections close.

Refused from other computers.

No answer: see the next state.

Fields

FieldTypeRequiredMeaning
cmdstringyesAlways forget_remote.
idstringyesThe app's id, from remotes.

Example

{"cmd":"forget_remote","id":"9c1e…"}

pairing message

The code apps pair with now.

Sent when you connect and whenever the code changes.

Sent to every client on the server's own computer.

Fields

FieldTypeRequiredMeaning
typestringyesAlways pairing.
codestringyesThe 4-digit code.

Example

{"type":"pairing","code":"4719"}

remotes message

The apps paired with the server.

Sent when you connect and whenever it changes.

Sent to every client on the server's own computer.

Fields

FieldTypeRequiredMeaning
typestringyesAlways remotes.
remotesarray of objects, fieldsyesEvery paired app.

remotes[]: A paired app.

FieldTypeRequiredMeaning
idstringyesIts id: what forget_remote takes.
namestringyesThe name it paired with.
paired_atstring (date-time)yesWhen it paired (RFC 3339).
last_seenstring (date-time)noWhen it last connected (RFC 3339; updated at most once an hour).

Example

{
  "type": "remotes",
  "remotes": [
    {
      "id": "9c1e…",
      "name": "Anna's tablet",
      "paired_at": "2026-10-28T09:12:00Z",
      "last_seen": "2026-10-30T13:02:11Z"
    }
  ]
}

A complete example

A Python client that pairs, then prints what is playing and pauses it. No packages needed.

import hashlib, json, socket, ssl, sys

HOST, PORT = sys.argv[1], 7090            # from Bonjour (_samklang._tcp)

ctx = ssl.create_default_context()
ctx.check_hostname, ctx.verify_mode = False, ssl.CERT_NONE   # we pin instead
conn = ctx.wrap_socket(socket.create_connection((HOST, PORT)))
fingerprint = hashlib.sha256(conn.getpeercert(binary_form=True)).hexdigest()
f = conn.makefile("rwb")

def send(msg):
    f.write(json.dumps(msg).encode() + b"\n")
    f.flush()

def receive():
    return json.loads(f.readline())

try:
    saved = json.load(open("samklang-token.json"))
except FileNotFoundError:
    saved = None

if saved:
    assert saved["fingerprint"] == fingerprint, "not the server we paired with"
    send({"cmd": "hello", "token": saved["token"]})
    reply = receive()
else:
    # A new server takes its first client without a code.
    send({"cmd": "pair", "code": "", "name": "My script"})
    reply = receive()
    if reply.get("error") == "code_needed":
        conn = ctx.wrap_socket(socket.create_connection((HOST, PORT)))  # an error ends the connection
        f = conn.makefile("rwb")
        send({"cmd": "pair", "code": input("Pairing code: "), "name": "My script"})
        reply = receive()

if reply["type"] == "error":
    sys.exit("server says: " + reply["error"])
if reply["type"] == "paired":
    json.dump({"fingerprint": fingerprint, "token": reply["token"]}, open("samklang-token.json", "w"))
print("connected to", reply["server"]["name"])

while True:
    msg = receive()
    if msg["type"] != "state":
        continue
    track = msg.get("track")
    print("playing:", f'{track["artist"]} – {track["title"]}' if track else "nothing")
    if msg["playing"] and not msg["paused"] and msg["can_control"]:
        send({"cmd": "transport", "action": "playpause"})
    break

Questions, or something the API should do for your client? Write to support@wieoapps.com.