Harness protocol reference
Every message between jambonz and a custom harness (protocol version 1).
Most harnesses use @jambonz/sdk/harness, which speaks this protocol for you. This
page is for writing a harness without it, in any language.
Connection
- jambonz opens a WebSocket to the harness URL for each call, with subprotocol
harness.jambonz.org. Usewss://;ws://is accepted only for a private address on the same network as jambonz. - When the agent has a secret, the upgrade request carries
Authorization: Bearer <secret>. With none, there is noAuthorizationheader. A harness that requires one should refuse the upgrade with HTTP 401. - Every message is a JSON text frame with a
type. Messages from jambonz carrycall_sid. There are no binary frames: audio never goes to the harness. - Ignore message types and fields you do not recognize. New ones are added without changing the protocol version.
session:startcarriesprotocol: 1. A harness that cannot speak it closes the socket with code 4000 and a reason.
A whole call
jambonz → harness
harness → jambonz
Ids (reply_id, call_id, command_id) are yours to choose and must be unique within the call.
Replies
A reply with a turn_id answers that turn, and is accepted only after that turn’s turn:start; one sent earlier is
refused with an error event. A reply without a turn_id is the harness speaking on its own initiative, for example
after a silence event. jambonz speaks one reply at a time; a reply that starts while another is playing is queued
behind it.
Replies the harness starts on its own:
- The floor is free (nothing playing, no turn open, the caller not speaking): spoken at once.
- The floor is busy: held, and spoken as soon as the floor is free, so it never plays over the caller or over the
turn’s reply. One unprompted reply is held at a time; a newer one replaces it (
reply_dropped, reasonreplaced). interrupt: trueon its first chunk: whatever is playing is cut off and this reply is spoken now. A caller in mid-utterance is still never talked over.
Tool calls inside chunks (delta.tool_calls) are ignored. To run one of the agent’s tools, send tool:call. Tools of
the harness’s own stay between it and its model.
Events
Sent as {type: "event", name, …}.
Commands
Sent as {type: "command", command_id, name, args}; each gets a command:result.
play, and listen with hints, are not supported yet: they get command:result with ok: false.
Timeouts and failures
Each failure is in the feature-server log, the turn’s record and the verb’s actionHook (completion_reason: "harness_failure" with a harness_error).
Starting early (optional)
speech:start and transcript let a harness start work before the turn ends: retrieval, routing, or a draft reply from
a final segment. Send nothing for the turn until its turn:start, then compare the turn’s text with what you started
from: if it matches, stream the draft at once; if the caller said more, redo it. jambonz does no speculation of its own
for a harness. A harness that does not want to start early ignores both messages.
The turn record
turn_record on reply:done is your account of how the harness handled the turn. Every field is optional, and
anything else you add is kept as given. Up to 16 KB; do not put caller secrets in it, since it is stored.
A minimal harness without the SDK
Everything a real harness adds (retrieval, routing, checks, its own tools) happens inside the turn:start case; the
protocol stays the same.