# Transport & framing Source: https://tehoclient.dev/protocol SDK: 1.0.0 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](https://tehoclient.dev/protocol/lightweight) 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`](#the-schema) 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`: | # | 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](https://tehoclient.dev/modules/private), 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](https://tehoclient.dev/protocol/messages). ## 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](https://tehoclient.dev/protocol/messages). ## 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`](https://tehoclient.dev/modules/verify) when you need proof.