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:apiis 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
- On entering the play state the client registers the channel, with
minecraft:registeron 1.13+ orREGISTERon older versions. Bukkit only delivers plugin messages to players who registered the channel. - The client immediately sends
Hello. - If the server sees the channel registered but no
Hello, which happens when a proxy replays registrations to a new backend, it sendsHelloRequestand the client answers with a freshHello. - The server answers with
Welcome. Only afterWelcomedoes 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:
frame = varint messageType, field*
field = varint (fieldNumber << 3 | wireType), value
wireType = 0 varint | 1 fixed64 | 2 length-delimited | 5 fixed32The 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 arefixed32. - 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
Chunkframes. - A sender may omit fields that hold their default value.
Handshake
Hello, client -> server, type 0x01:
| # | Field | Type | Example |
|---|---|---|---|
| 1 | protocol | varint | 2 |
| 2 | client | string | teho 0.4.2 |
| 3 | minecraft | string | 1.21.11 |
| 4 | platform | string | fabric, vanilla, forge, legacy-vanilla |
| 5 | capabilities | repeated string | notify:1, waypoint:1, hud:1 |
Welcome, server -> client, type 0x02:
| # | Field | Type | Meaning |
|---|---|---|---|
| 1 | protocol | varint | The highest protocol both sides support. |
| 2 | session | varint | Random per handshake. A new session clears all server state on the client. |
| 3 | enabled | repeated string | The capabilities the server will use. Must be a subset of what the client offered. |
| 4 | server_name | string | The source label on everything the server shows. |
| 5 | grant | string | A private grant, if any. |
| 6 | verified | string | A 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
| Type | Message | Purpose |
|---|---|---|
0x05 | Reply{request_id, ok, error, data} | A generic answer to a message that carried a request_id. |
0x06 | Event{kind, target, value, request_id} | Client -> server events such as hologram.click, rate-limited per kind. |
0x07 | Chunk{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. |
0x0D | ConsentResult{scope, granted, items[]} | How the player answered a consent prompt. |
0x0E | PrivateStatus{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:
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 1More 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:
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.