Samklang Server API.
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
| Transport | TLS over TCP, port 7090 (-remote-port). Not HTTP |
| Discovery | Bonjour / mDNS, _samklang._tcp |
| Framing | one JSON object per line (\n), both ways, UTF-8 |
| Protocol version | 1 |
| Security | a 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:
| Key | Example | Meaning |
|---|---|---|
v | 1 | protocol version; skip servers with a version you don't know |
id | 3f2ac81e90b4d7c2 | the first 16 hex digits of the certificate's SHA-256 fingerprint |
ver | 0.9.3 | the server's own version |
os | linux | darwin, 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.
- First time: accept the certificate, compute the SHA-256 of its DER bytes, and check that the first 16 hex digits equal the advertised
id. Keep the whole fingerprint with the token you get from pairing. - Every time after: accept only a certificate with that fingerprint.
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"}
code_needed: the server is set up already; ask the user for its code. Trying without a code never counts as a wrong guess.bad_code: wrong code. Each wrong code takes a second to answer.locked: five wrong codes lock pairing, for a minute at first and doubling each time up to an hour, and the code changes. Try again afterretry_afterseconds.unknown_token: the token was unpaired on the server (or its settings were reset). Pair again.
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
- Ignore what you don't know. New fields, new values and new message types may be added in protocol version 1; they never change the meaning of the old ones. A breaking change gets a new
v. - The state is complete. Each
statereplaces the last, and is only sent when it changed. - Read fast. Each client has a queue of 64 messages. A client that stops reading is disconnected; reconnect and you get the current state again.
- Restarts.
settingsandrenamerestart the server, and so may an update. Reconnect with a short back-off (the apps wait about a second, then longer). - Lines can be long. A cover is a few hundred kB of base64 on one line; a calibration recording up to a few MB the other way. Don't use a small fixed line buffer.
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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always pair. |
code | string | yes | The server's 4-digit pairing code, or "" to try without one (a new server takes its first client without a code). |
name | string | no | Your 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.
Fields
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always hello. |
token | string | no | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always pairing_code. |
ref | string | no | Any 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always paired. |
token | string | yes | Your token: send it in hello from now on. It is secret: whoever has it controls the server. |
server | object, fields | yes | The server. |
server: The server.
| Field | Type | Required | Meaning |
|---|---|---|---|
id | string | yes | The first 16 hex digits of its certificate's SHA-256 fingerprint (Bonjour's id). |
name | string | yes | Its name. |
version | string | yes | Samklang Server's version. |
os | string | yes | Its operating system: darwin, linux or windows. |
proto | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always hello. |
server | object, fields | yes | The server. |
server: The server.
| Field | Type | Required | Meaning |
|---|---|---|---|
id | string | yes | The first 16 hex digits of its certificate's SHA-256 fingerprint (Bonjour's id). |
name | string | yes | Its name. |
version | string | yes | Samklang Server's version. |
os | string | yes | Its operating system: darwin, linux or windows. |
proto | string | yes | The 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.
code_needed: trying without a code never counts as a wrong guess.bad_code: each wrong code takes a second to answer.locked: five wrong codes lock pairing, for a minute at first and doubling each time up to an hour, and the code changes.unknown_token: pair again.
Fields
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always error. |
error | string | yes | code_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_after | integer | no | Seconds 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always pairing_code. |
ref | string | yes | The ref the command carried ("" when it had none). |
code | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always state. |
speaker | string | yes | The multiroom speaker (group) the top-level fields describe: the one playing (the first, when several are), else Everywhere. "" when there is none. |
everywhere | boolean | yes | Whether there is a multiroom speaker. When false, only settings, groups, devices, satellites, sound_boards and updates say anything. |
playing | boolean | yes | Whether something plays on it (a paused stream still counts as playing). |
paused | boolean | yes | Whether it is paused. |
can_control | boolean | yes | Whether the sender (the iPhone or Mac playing to Samklang) takes transport and volume commands. |
volume | number | no | The sender's volume, 0 to 1. Missing until the sender has set one. |
settings | object, fields | yes | What the server looks for and offers. |
groups | array of objects, fields | yes | Every multiroom speaker: Everywhere and the user's groups. |
auto_sync | boolean | yes | Automatic sync is on. |
target_ms | integer | yes | The delay auto sync brings every device onto, in ms. |
track | object, fields | no | What is playing. Missing when nothing is, or when the sender says nothing about it. |
artwork_id | integer | yes | The cover that goes with this state: the id of the last artwork. |
members | array of objects, fields | yes | The devices playing now. |
devices | array of objects, fields | yes | Every device the server knows, online or not. |
satellites | array of objects, fields | no | The Samklang satellites connected, with a speaker or not (one with a speaker is among the devices too). Missing when there are none. |
sound_boards | array of objects, fields | no | The sound boards a satellite can have, to offer. Missing on a server without satellite support. |
updates | object, fields | no | What 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.
| Field | Type | Required | Meaning |
|---|---|---|---|
find_airplay | boolean | yes | Use paired AirPlay receivers, such as an Apple TV. Default on. |
find_chromecast | boolean | yes | Use Chromecasts and Cast groups. Default on. |
find_upnp | boolean | yes | Use UPnP, DLNA and Sonos renderers. Default off. |
multiroom | boolean | yes | Offer the multiroom speakers (they need AirPlay and Chromecast both on). Default on. |
find_cast_groups | boolean | yes | Use the speaker groups made in Google Home too; off, each Chromecast plays on its own. Default off. |
find_bluetooth | boolean | yes | Play to Bluetooth speakers paired with the server's computer. Default off: headphones paired with it would turn up too. |
check_updates | boolean | yes | Look 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.
| Field | Type | Required | Meaning |
|---|---|---|---|
name | string | yes | Its name: the key every group command uses. |
auto_add | boolean | no | Devices seen for the first time join it. |
auto | boolean | yes | True for Everywhere: every device not left out. A user's group has members instead. |
members | array of strings | no | A user's group: its devices, by label. |
playing | boolean | yes | Whether it plays (a paused stream still counts). |
google_groups | boolean | no | Its Chromecasts play through Google Home groups (see google_groups). |
google_fits | array of objects, fields | no | The Google Home groups that would (or do) play in place of its Chromecasts. Missing: the option isn't offered. |
now | object, fields | no | What this group plays, while it plays (or its sender paused and left). |
groups[].google_fits[]: A Google Home speaker group.
| Field | Type | Required | Meaning |
|---|---|---|---|
name | string | yes | The Google Home group's name. |
speakers | array of strings | yes | The speakers it plays in place of. |
groups[].now: What this group plays, while it plays (or its sender paused and left).
| Field | Type | Required | Meaning |
|---|---|---|---|
paused | boolean | yes | Whether it is paused. |
can_control | boolean | yes | Whether its sender takes transport and volume commands. |
volume | number | no | The sender's volume, 0 to 1. Missing until the sender has set one. |
track | object, fields as track | no | What is playing. Missing when nothing is, or when the sender says nothing about it. |
artwork_id | integer | yes | Its cover, sent as group_artwork with this id. |
members | array of strings | yes | The devices it plays on now, by label. |
track: What is playing. Missing when nothing is, or when the sender says nothing about it.
| Field | Type | Required | Meaning |
|---|---|---|---|
title | string | yes | The track's title. |
artist | string | yes | Its artist. |
album | string | yes | Its album. |
duration_ms | integer | no | Its length in ms, when known. |
started_at_ms | integer | no | While playing: when the track began, in Unix ms. Count on from it yourself (against the server's clock: see clock). |
position_ms | integer | no | While paused: where it stopped, in ms. |
members[]: A device playing now.
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | How the server reaches it: airplay, cast, upnp, bluetooth (paired with the server's computer) or sat (a satellite's speaker, or its headphone jack). |
label | string | yes | Its name in Samklang: the key every command uses. |
kind | string | yes | What it is, to show ("Chromecast Audio", "AirPlay"). |
detail | string | no | More about it, to show ("Google Home group"). |
ms | integer | yes | Under auto sync its fine-tune, else its delay: what set changes. |
delay_ms | integer | yes | Its delay. |
lag_ms | integer | no | Its measured lag, when known. |
connected | boolean | yes | Whether it is receiving. |
level | number | yes | Its volume relative to the sender's, 0 to 1. |
timing | string | yes | measured (its lag is measured and corrected) or fixed (known exactly). |
syncing | boolean | no | Auto sync is still bringing it onto the target. |
speakers | array of objects, fields | no | A Cast group's speakers, each with its own volume. |
members[].speakers[]: One speaker of a playing Cast group.
| Field | Type | Required | Meaning |
|---|---|---|---|
label | string | yes | The speaker's label. |
level | number | yes | Its volume relative to the group's, 0 to 1. |
devices[]: A device the server knows, online or not.
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | How the server reaches it: airplay, cast, upnp, bluetooth (paired with the server's computer) or sat (a satellite's speaker, or its headphone jack). |
label | string | yes | Its name in Samklang: the key every command uses. |
kind | string | yes | What it is, to show. |
detail | string | no | More about it, to show. |
included | boolean | yes | Everywhere plays on it (see include). |
group | boolean | no | It is a Cast group. |
speakers | array of strings | no | A Cast group's speakers. |
in_group | string | no | A speaker: the Cast group that plays it. |
online | boolean | yes | Seen now. |
last_seen | string (date-time) | no | When an offline device was last seen (RFC 3339). |
shown | boolean | yes | Its own AirPlay speaker is offered (always true for AirPlay receivers). See show. |
groups | array of strings | no | The user's groups it is in. |
new | boolean | no | Seen 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.
| Field | Type | Required | Meaning |
|---|---|---|---|
id | string | yes | The satellite's id: the key the satellite commands use. |
name | string | yes | Its name. |
state | string | yes | none (no speaker yet), connecting, ready or playing. |
speaker | string | no | The speaker it plays on. |
speaker_addr | string | no | That speaker's Bluetooth address. |
found | array of objects, fields | no | Bluetooth speakers it found after satellite_scan. |
board | string | no | The sound board the user said is attached: an id from the state's sound_boards. Missing for none. |
pins | object, fields | no | A board wired by hand (i2s): its pins. |
quality | string | no | Its speaker's sound as set: auto, standard or high. |
xq | boolean | no | Whether its speaker plays SBC XQ now. |
jack | string | no | With a sound board: its headphone jack's label, a device of its own. |
chip | string | no | rp2040 (Pico W) or rp2350 (Pico 2 W). |
update | string | no | Its 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. |
version | string | no | Its firmware's version. Missing on older firmware, or a developer's build. |
update_to | string | no | The version an update brings. |
satellites[].found[]: A speaker it found.
| Field | Type | Required | Meaning |
|---|---|---|---|
addr | string | yes | Its Bluetooth address: what satellite_use takes. |
name | string | yes | Its name. |
rssi | integer | yes | Its signal strength, in dBm. |
satellites[].pins: A board wired by hand (i2s): its pins.
| Field | Type | Required | Meaning |
|---|---|---|---|
data | integer | yes | The data pin (GP number). |
clock | integer | yes | The bit clock's pin; the word clock is on the pin after. |
sound_boards[]: A sound board a satellite can have.
| Field | Type | Required | Meaning |
|---|---|---|---|
id | string | yes | Its id: what satellite_board takes. |
name | string | yes | Its name, to show. |
detail | string | yes | More about it, to show. |
custom | boolean | no | Wired 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.
| Field | Type | Required | Meaning |
|---|---|---|---|
server | object, fields | no | A newer Samklang Server, when there is one. It is updated on the computer it runs on. |
app | object, fields | no | Samklang's latest version, for Samklang itself; your own client can ignore it. |
checked | string (date-time) | yes | When 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.
| Field | Type | Required | Meaning |
|---|---|---|---|
version | string | yes | Its version. |
url | string | no | Where to get it. |
notes | string | no | What is new, to show. |
updates.app: Samklang's latest version, for Samklang itself; your own client can ignore it.
| Field | Type | Required | Meaning |
|---|---|---|---|
version | string | yes | Its version. |
url | string | no | Where to get it. |
notes | string | no | What 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always artwork. |
id | integer | yes | Its id: the state's artwork_id names it. |
jpeg | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always group_artwork. |
group | string | yes | The group's name. |
id | integer | yes | Its id: the group's now.artwork_id names it. |
jpeg | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always transport. |
action | string | yes | playpause, next or previous. |
group | string | no | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always volume. |
value | number | yes | 0 to 1. |
group | string | no | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always level. |
label | string | yes | The device, by its label; also a speaker inside a playing Cast group. |
value | number | yes | 0 to 1 (outside is clamped). |
group | string | no | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always resync. |
label | string | yes | The device, by its label. |
group | string | no | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always auto_sync. |
on | boolean | yes | On 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always set. |
label | string | yes | The device, by its label. |
ms | integer | yes | Under auto sync the fine-tune, −2000 to 2000; without it the delay, 0 to 5000. Out of range is ignored. |
group | string | no | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always clock. |
t | number | yes | Your own clock, in Unix ms. |
ref | string | no | Any 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always clock. |
ref | string | yes | The ref the command carried ("" when it had none). |
t | number | yes | Your t, sent back. |
server_ms | number | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always include. |
label | string | yes | The device, by its label. |
on | boolean | yes | false 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always show. |
label | string | yes | The device, by its label. |
on | boolean | yes | Show 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always rename. |
label | string | yes | The device, by its label. |
name | string | yes | Its 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always seen_device. |
label | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always forget_device. |
label | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always save_group. |
old | string | no | "" (or left out) to make a group; a group's name to change that group. |
name | string | yes | The group's name. A new name gives it a new AirPlay speaker. |
members | array of strings | yes | Its devices, by label. |
auto_add | boolean | no | New 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always delete_group. |
group | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always group_member. |
group | string | yes | The group's name. |
label | string | yes | The device, by its label. |
on | boolean | yes | In 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always google_groups. |
group | string | yes | The group's name. |
on | boolean | yes | On 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always settings. |
settings | object, fields | yes | All 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.
| Field | Type | Required | Meaning |
|---|---|---|---|
find_airplay | boolean | no | Use paired AirPlay receivers, such as an Apple TV. Default on. |
find_chromecast | boolean | no | Use Chromecasts and Cast groups. Default on. |
find_upnp | boolean | no | Use UPnP, DLNA and Sonos renderers. Default off. |
multiroom | boolean | no | Offer the multiroom speakers (they need AirPlay and Chromecast both on). Default on. |
find_cast_groups | boolean | no | Use the speaker groups made in Google Home too; off, each Chromecast plays on its own. Default off. |
find_bluetooth | boolean | no | Play to Bluetooth speakers paired with the server's computer. Default off: headphones paired with it would turn up too. |
check_updates | boolean | no | Look 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always satellite_scan. |
id | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always satellite_use. |
id | string | yes | The satellite's id. |
addr | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always satellite_quality. |
id | string | yes | The satellite's id. |
quality | string | yes | high (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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always satellite_board. |
id | string | yes | The satellite's id. |
board | string | yes | An id from the state's sound_boards; "" (or none) for off. |
data | integer | no | For a board wired by hand (custom): the data pin, a GP number. |
clock | integer | no | For 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always satellite_update. |
id | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always satellite_firmware. |
chip | string | yes | rp2040 (Pico W) or rp2350 (Pico 2 W). Anything else counts as rp2040. |
ref | string | no | Any 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always satellite_firmware. |
ref | string | yes | The ref the command carried ("" when it had none). |
chip | string | yes | The chip it is for. One of rp2040, rp2350. |
image | string | no | The firmware image's id (its CRC-32, in hex). |
uf2 | string | no | The UF2 file, base64. |
error | string | no | Why 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.
- Start:
calibratewith"on":true. The answer lists the speakers. - For each speaker, ask for a
chime. - Record from
record_from_mstorecord_to_ms(server time: useclock), and send it withheard. - Leave a speaker out with
calibration_skip; start over withcalibration_reset. - See the result with
calibration_result. - Apply it with
calibration_apply. - End:
calibratewith"on":false, and"restore":trueto 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always calibrate. |
group | string | yes | The group to calibrate. |
on | boolean | yes | Start (true) or end (false). |
restore | boolean | no | Ending: put back the fine-tunes from before. |
ref | string | no | Any 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always chime. |
group | string | yes | The group being calibrated. |
label | string | yes | The speaker, by its label; * plays it on every device at once (not measured). |
ref | string | no | Any 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always heard. |
group | string | yes | The group being calibrated. |
label | string | yes | The speaker the chime was for. |
start_ms | number | no | The server time of the first sample, in Unix ms. |
rate | number | no | The sample rate in Hz (8000 and up). |
pcm | string | no | The recording: 16-bit little-endian mono PCM, base64, up to 15 s at 48 kHz. |
missing | boolean | no | There is no recording: send this instead of pcm. |
note | string | no | With missing: why, for the log. With calibration_note: what the app made of a chime. |
ref | string | no | Any 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always calibration_skip. |
group | string | yes | The group being calibrated. |
label | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always calibration_reset. |
group | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always calibration_result. |
group | string | yes | The group being calibrated. |
ref | string | no | Any 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always calibration_apply. |
group | string | yes | The group being calibrated. |
ref | string | no | Any 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always calibration_note. |
label | string | yes | The speaker. |
note | string | yes | What 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always calibration. |
ref | string | yes | The ref the command carried ("" when it had none). |
on | boolean | yes | Whether a calibration runs now. |
speakers | array of strings or null | no | Started: the speakers to calibrate, by label. |
target_ms | integer | no | Started: the group's target delay, in ms. |
error | string | no | Why 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always chime. |
ref | string | yes | The ref the command carried ("" when it had none). |
label | string | yes | The speaker. |
at_ms | number | no | When the chime starts at the live point (server time, Unix ms). |
target_ms | integer | no | The group's target delay, in ms. |
offset_ms | integer | no | The speaker's fine-tune, in ms. |
record_from_ms | number | no | Record from this server time (Unix ms)… |
record_to_ms | number | no | …to this one. |
error | string | no | Why 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always heard. |
ref | string | yes | The ref the command carried ("" when it had none). |
label | string | yes | The speaker. |
heard | boolean | yes | Whether the chime was found in the recording. |
message | string | no | Something 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always calibration_result. |
ref | string | yes | The ref the command carried ("" when it had none). |
speakers | array of objects or null, fields | yes | Each speaker asked for, in order. |
measured | integer | yes | How many speakers were heard. |
can_apply | boolean | yes | Whether there is something to apply. |
applied | boolean | yes | Whether it was applied. |
speakers[]: A speaker.
| Field | Type | Required | Meaning |
|---|---|---|---|
label | string | yes | The speaker. |
heard | boolean | yes | Whether its chime was heard. |
detail | string | yes | skipped, heard, heard the latest or heard N ms early, to show. |
fix_ms | integer | no | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | The command's name. One of heard, calibration_skip, calibration_reset, calibration_result, calibration_apply. |
label | string | yes | The command's label ("" when it had none). |
error | string | yes | Always not calibrating. |
ref | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
cmd | string | yes | Always forget_remote. |
id | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always pairing. |
code | string | yes | The 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
| Field | Type | Required | Meaning |
|---|---|---|---|
type | string | yes | Always remotes. |
remotes | array of objects, fields | yes | Every paired app. |
remotes[]: A paired app.
| Field | Type | Required | Meaning |
|---|---|---|---|
id | string | yes | Its id: what forget_remote takes. |
name | string | yes | The name it paired with. |
paired_at | string (date-time) | yes | When it paired (RFC 3339). |
last_seen | string (date-time) | no | When 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.