.dev

Protocol4 min read

Transport & framing

The teho:api channel, the handshake and the wire format.

You only need this page if you are not using a Teho SDK: a non-Java server, a custom proxy, or a bot. The Java SDK and the Node, Python and Rust libraries do all of this for you.

Channel
teho:api
Protocol
2
Encoding
Protobuf wire format
Max frame
32,000 bytes

Channel

One plugin-message channel, teho:api, on every Minecraft version.

  • 1.13 and later require namespaced channel ids, and teho:api is one.
  • 1.7.10 to 1.12.2 accept any name up to 20 characters, so the same name works.
  • ViaVersion leaves channel names that already contain a colon unchanged, so cross-version servers see the same channel.

Detection

  1. On entering the play state the client registers the channel, with minecraft:register on 1.13+ or REGISTER on older versions. Bukkit only delivers plugin messages to players who registered the channel.
  2. The client immediately sends Hello.
  3. If the server sees the channel registered but no Hello, which happens when a proxy replays registrations to a new backend, it sends HelloRequest and the client answers with a fresh Hello.
  4. The server answers with Welcome. Only after Welcome does the client accept feature messages.

If the player turned the API off for your server, the client registers nothing and sends nothing.

Framing

Every plugin message is exactly one frame:

Text
frame    = varint messageType, field*
field    = varint (fieldNumber << 3 | wireType), value
wireType = 0 varint | 1 fixed64 | 2 length-delimited | 5 fixed32

The body is protobuf's wire format, so code generated by protoc from teho_api.proto reads and writes it.

  • Unknown fields are skipped by wire type, and unknown message types are ignored. That is the whole forward-compatibility story.
  • Strings are UTF-8, at most 1,024 bytes (8,192 for grants and certificates). Booleans and enums are varints. Signed values use zig-zag (sint32). Colours are fixed32.
  • A frame is at most 32,000 bytes, under the smallest custom-payload limit from 1.7.10 to 26.3. Anything larger is split into Chunk frames.
  • A sender may omit fields that hold their default value.

Handshake

Hello, client -> server, type 0x01:

#FieldTypeExample
1protocolvarint2
2clientstringteho 0.4.2
3minecraftstring1.21.11
4platformstringfabric, vanilla, forge, legacy-vanilla
5capabilitiesrepeated stringnotify:1, waypoint:1, hud:1

Welcome, server -> client, type 0x02:

#FieldTypeMeaning
1protocolvarintThe highest protocol both sides support.
2sessionvarintRandom per handshake. A new session clears all server state on the client.
3enabledrepeated stringThe capabilities the server will use. Must be a subset of what the client offered.
4server_namestringThe source label on everything the server shows.
5grantstringA private grant, if any.
6verifiedstringA verified-server certificate, if any.

HelloRequest (0x03) is empty and asks for a fresh Hello. Reset (0x04) clears all server-applied state without a new handshake.

The client clears all server state on disconnect and on every new Welcome. Never send anything for a capability that is not in enabled; the client drops it regardless.

Requests, events and chunks

TypeMessagePurpose
0x05Reply{request_id, ok, error, data}A generic answer to a message that carried a request_id.
0x06Event{kind, target, value, request_id}Client -> server events such as hologram.click, rate-limited per kind.
0x07Chunk{stream, index, total, data}A piece of a frame over 32,000 bytes, in 30,000-byte parts. At most 4 MB per stream and 8 open streams.
0x0DConsentResult{scope, granted, items[]}How the player answered a consent prompt.
0x0EPrivateStatus{capabilities[], error, server}The result of checking a private grant.

Message type ids

Ids are grouped in blocks of 16 per capability: 0x00–0x0F core, 0x10 notify, 0x20 waypoint, 0x30 modules, 0x40 staff, 0x50 hud, and so on. Ids above 0x7F simply take two varint bytes. The full list is on the message reference.

Worked example

Notify{id="a", title="T", body="B", accent=0x4DA9FF, duration_ms=3000, sound=true} encodes to:

Text
10                    messageType 0x10 (Notify)
0a 01 61              field 1, length 1, "a"
12 01 54              field 2, length 1, "T"
1a 01 42              field 3, length 1, "B"
25 ff a9 4d 00        field 4, fixed32 0x004DA9FF
28 b8 17              field 5, varint 3000
30 01                 field 6, varint 1

More vectors, including a Hello, a WaypointSet with a negative coordinate and a frame full of unknown fields to skip, are in server-sdk/protocol/test-vectors.md. Every implementation must decode them.

The schema

server-sdk/protocol/teho_api.proto is the reference schema for all 108 framed messages and their nested types. It starts like this:

Protobuf
syntax = "proto3";

package teho.api.v1;

message Hello {
  uint32 protocol = 1;
  string client = 2;
  string minecraft = 3;
  string platform = 4;
  repeated string capabilities = 5;
}

message Welcome {
  uint32 protocol = 1;
  uint64 session = 2;
  repeated string enabled = 3;
  string server_name = 4;
  string grant = 5;
  string verified = 6;
}

The schema has no type ids, since protobuf has no notion of them. Prefix each encoded message with its varint type from the message reference.

Trust

The handshake is not authenticated. A modified client can claim to be Teho, and Teho can claim capabilities it lacks. Treat the handshake as UX only and use assert:1 and attest:1 when you need proof.