# schema.jppgr.am api ## endpoints ### GET /api/health reports whether the 6-hourly refresh is landing. - response: `{ ok: boolean, layers: number, maxLayer: number | null, artifacts: Record, stale?: string[], lastCycle?: { startedAt: string, finishedAt: string, steps: Record }, failedStages?: string[] }` - `artifacts` maps each tracked file (`index`, `meta`, `errors`, `config`, `mtproto`) to its age in milliseconds; `null` means the file is missing - `stale` lists artifacts older than 24h, i.e. several missed cycles rather than a gap between runs - `lastCycle` mirrors `data/status.json`, written by the watcher at the end of every cycle - 503 when anything is stale or a stage failed, 200 otherwise; the body is the same either way ### GET /api/conflicts returns definitions the merge pipeline could not fully vouch for. detection only, nothing here is applied automatically. - params: `layer` (optional number), `kind` (optional, `"unsound"` or `"conflict"`), `nearby` (optional, `"true"`), `limit` (optional, default and max 500) - response: `{ generatedAt: string, totals: { unsound: number, conflicts: number }, unsound?: UnsoundEntry[], conflicts?: ConflictEntry[] }` - `totals` counts everything that matched the filters; the arrays are capped by `limit`, and each entry keeps at most its 5 nearest candidates - `nearby=true` keeps only unsound entries with a sound candidate within 5 layers - 404 until the watcher has generated `data/conflicts.json` #### UnsoundEntry `{ layer: number, name: string, line: string, declaredId: string, computedId: string, candidates: { layer: number, line: string, distance: number }[] }` - a definition whose body does not hash to the id it declares, so a client dispatching on that id would not get this body - `candidates` are sound definitions carrying the same declared id elsewhere, nearest layer first - a candidate is evidence, not an answer: ids are reused across eras, so a distant match is usually an unrelated combinator #### ConflictEntry `{ layer: number, name: string, candidates: { id: string, line: string, sources: string[] }[], resolved: boolean }` - one ctor name that two upstream sources shipped under different ids, both crc-sound, which is the case crc cannot arbitrate - `resolved: true` once a curated override pins that ctor at that layer ### GET /api/crc32 computes the combinator id of a tl definition, and reports whether the id it declares matches. - params: `definition` (one tl definition line) - response: `{ definition: string, normalized: string, id: string, declaredId: string | null, sound: boolean | null }` - `normalized` is the exact string that is hashed, after the id is stripped, `bytes` is folded to `string`, generics are unwrapped and `?true` fields are dropped - `sound` is `null` when the input declares no id, otherwise whether `declaredId` equals `id` ### POST /api/crc32 same, for a whole schema at once. - body: raw tl schema text (`Content-Type: text/plain`) - response: `{ total: number, unsound: number, truncated?: true, returned?: number, results: [...] }` - lines that are not definitions are skipped - at most 2000 results are returned; `total` still counts every definition line, and `truncated` marks a capped response ### GET /api/history returns the layer lifetime of a definition: when it appeared, every id it has carried, and where it is absent. - params: `name` (exact ctor or method name) or `id` (hex, `0x`-prefixed or bare) - with `name`: `{ name: string, firstLayer: number, lastLayer: number, ids: { id: string, from: number, to: number }[], present: [number, number][], missingIn: number[] }` - with `id`: `{ id: string, names: string[], history: DefinitionHistory[] }` — one id can have carried several names - `ids` and `present` are runs over the ARCHIVED layer sequence, so a layer that was never archived does not split a lifetime or read as a removal - `missingIn` lists archived layers between `firstLayer` and `lastLayer` where the definition is genuinely absent - 404 when no layer contains the name or id ### GET /api/field-history returns the layer lifetime of each field of a definition: when it appeared, every type it has carried, and when it was removed. - params: `name` (exact ctor or method name), `field` (optional, one field name) - response: `{ name: string, firstLayer: number, lastLayer: number, fields: { field: string, runs: { type: string, from: number, to: number }[], addedIn: number, removedIn: number | null, everPresent: boolean }[] }` - a run is a contiguous span over the archived layers where the field kept the same raw type, so `message.via_bot_id` reports `flags.11?int` up to 132 then `flags.11?long` from 133 - `removedIn` is the first archived layer where the definition exists but the field does not, `null` while the field is still present - runs are contiguous over archived layers, not raw layer numbers, so the 49 unarchived numbers never read as a removal - 400 without `name`, 404 when no layer contains it; an unknown `field` on a known definition returns `fields: []` ### POST /api/compat reports what breaks in a client's constructor dispatch table at a given layer. - body: `{ target: number | "latest", ids: string[] }` (`Content-Type: application/json`) - `ids` are hex constructor ids, `0x`-prefixed or bare, any case, at most 2000 per request - response: `{ target: number, summary: { ok: number, reassigned: number, removed: number, unknown: number, invalid: number }, results: CompatResult[] }` - 400 on a missing or non-archived `target` (the message names the nearest archived layer), on a non-array `ids`, or on more than 2000 ids - a non-hex entry does not fail the request, it comes back with `status: "invalid"` #### CompatResult `{ id: string, status: "ok" | "reassigned" | "removed" | "unknown" | "invalid", name: string | null, targetId: string | null, lastSeen: number | null }` - `ok` — the id exists at `target` and denotes the same name - `reassigned` — the name still exists at `target` under a different id, given in `targetId`; this is the dangerous case, because a client dispatching on the old id silently stops matching - `removed` — the name existed somewhere in the corpus but not at `target` - `unknown` — the id appears in no archived layer; `invalid` — not parseable as hex - constructor ids are reused across eras, so for an id held by several names the answer resolves to the name that held it most recently at or before `target`; `name` and `lastSeen` are scoped to that resolved name, which for a reused id may be a later unrelated definition rather than the one a caller had in mind ### GET /api/flags returns which flag bit carried which field over time, per flags field of a definition. - params: `name` (exact ctor or method name) - response: `{ name: string, flagFields: { flagField: string, bits: { bit: number, runs: { field: string, type: string, from: number, to: number }[] }[], reuses: { bit: number, from: string, to: string, atLayer: number }[] }[] }` - one bit legitimately gates several fields at once (`message` puts `fwd_from_id` and `fwd_date` both on `flags.2`), so `field` and `type` join the occupants with `+` in declaration order - a run also ends when the type changes under an unchanged name, so the reported type is never stale - `reuses` records only bit transitions where the occupant name set both lost and gained a name, which is the case that silently breaks a client still reading the old field - 400 without `name`, 404 when no layer contains it; a definition with no conditional params returns `flagFields: []` ### GET /api/flags/reused returns every flag bit in the corpus that was reused for a different field, newest first. - params: `limit` (optional, default 100, max 500), `q` (optional, case-insensitive substring on definition name) - response: `{ total: number, reuses: { name: string, flagField: string, bit: number, from: string, to: string, atLayer: number }[] }` - `total` counts every match, `reuses` is capped by `limit` ### GET /api/layers returns list of available layers with line counts, combinator counts, source metadata and approximate release dates. - params: `at` (optional, `YYYY-MM-DD`) - response: `{ layers: { layer: number, lineCount: number, ctorCount: number, methodCount: number, preview?: true, sources?: string[], contentSource: { source: string, commit: string | null, committedAt: string | null } | null, releasedAt: string | null, releaseSource: "tdesktop" | "tdlib" | null }[] }` - `ctorCount` / `methodCount` count the definitions in the layer's `---types---` / `---functions---` sections - `preview: true` when the layer is only available from a single source (beta/unreleased) - `sources` lists which upstream sources have this layer (e.g. `["tgscheme", "core", "corefork"]`) - `contentSource` names the upstream revision the layer's text is taken from; `commit` and `committedAt` are `null` for the scraped docs sites, and the whole field is `null` until the pipeline has resolved the layer - a revision whose ids match another source's neighbouring layer better than its own is ignored as mislabelled; of the rest, one whose every definition another source also ships wins over one that carries definitions nobody else backs, then sources rank tdesktop, weba and webk, then corefork and core, then danog/schemas, then tdlib, then TGScheme/Schema, and the newest commit breaks ties within a rank - `releasedAt` is a day-resolution approximation: telegram publishes no layer dates, so it is the earliest commit date in an upstream client repo whose schema snapshot declares that layer, and `releaseSource` names which repo that was - 159 of the 179 archived layers carry a date; the rest predate the upstream histories and report `null` rather than a fabricated value - with `at`, returns `{ at: string, layer: number, releasedAt: string, releaseSource: string }` for the newest layer released on or before that date, 400 on a malformed date and 404 when it precedes every known release ### GET /api/sources reports which layer each upstream ships, and how far telegram's own documentation trails its own clients. - params: `q` (optional, case-insensitive substring on source name) - response: `{ latestLayer: number, sources: SourceRow[], docsLag: DocsLag | null }` - `docsLag` is computed from the unfiltered source list, so `q` narrows `sources` without moving the headline #### SourceRow `{ source: string, latestLayer: number, latestReleasedAt: string | null, layerCount: number, layersBehind: number, daysBehind: number | null }` - `layersBehind` counts archived layers between that source's latest and `latestLayer`, not raw number subtraction - `daysBehind` compares approximate release dates and is `null` when either is unknown #### DocsLag `{ docsLayer: number, docsReleasedAt: string | null, clientLayer: number, clientReleasedAt: string | null, layersBehind: number, daysBehind: number | null }` - `docsLayer` is the newer of core.telegram.org and corefork.telegram.org; `clientLayer` is the newest of tdesktop, tdlib, tgscheme, weba and webk - telegram documents a schema its own clients have moved past, so this is usually a positive lag rather than zero ### GET /api/layer returns tl schema for a layer. - params: `layer` (number or `"latest"`), `format` (optional, `"json"` or `"pretty"`) - default response: plain text - with `format=json`: `{ layer: number, lines: string[] }` - with `format=pretty`: plain text with each constructor expanded onto multiple lines with annotations ### GET /api/diff returns diff between two layers. - params: `from_layer` (number or `"latest"`), `to_layer` (number or `"latest"`) - response: `{ from_layer, to_layer, added: string[], removed: string[], changed: { old: string, new: string }[], renamed: { old: string, new: string }[] }` - `renamed` pairs a removal and an addition that share a constructor id AND an identical body, i.e. a ctor that moved namespace; without it those read as one deletion plus one unrelated creation - the match is deliberately narrow: a shared id with a different body is id reuse across eras, and a name that survives under a new id is an ordinary change - `classify` (optional, `"true"` / `"1"`) adds `summary` and `classified` to the response; every pre-existing key keeps its shape #### classification `summary` is `{ breaking: number, dangerous: number, safe: number }`, counting definitions by their worst finding rather than counting findings. `classified` is `{ name: string, severity: "breaking" | "dangerous" | "safe", findings: Finding[] }[]`, breaking first, then dangerous, then safe, then by name. `Finding` is `{ kind: string, severity: string, reason: string, field?: string, from?: string, to?: string }`. - breaking — `id-changed` (a dispatch table keyed on the old id stops matching), `definition-removed`, `field-removed`, `field-type-changed`, `field-reordered`, `field-added-unflagged`, `result-type-changed` - dangerous — `field-renamed` (same position and type, so the wire is unchanged but generated bindings break), `renamed` (same id, new name), `union-constructor-added` (a client decoding that union can now meet an id it does not know), `field-flag-relaxed` (an unconditional parameter became flag-gated) - safe — `definition-added`, `field-added-flagged` - tl parameters are positional and untagged, which is why adding an unconditional field is breaking while appending a flag-gated one is not: the protocol is layer-negotiated, so a client never sees a bit its layer did not declare - a change matching no kind is reported as `other` at severity `dangerous` rather than dropped ### POST /api/diff diffs two tl schemas. each side may be raw schema text or a reference to a stored layer. - body: `{ from: DiffSide, to: DiffSide }` (`Content-Type: application/json`) - `DiffSide` is either a string of raw tl schema text, or `{ layer: number | "latest" }` - response: `{ added: string[], removed: string[], changed: { old: string, new: string }[], renamed: { old: string, new: string }[] }` - `from_layer` / `to_layer` are included in the response only for sides given as a layer (a custom-text side omits its key) - mixing is allowed: e.g. `{ from: { layer: 225 }, to: "" }` - `classify: true` in the body behaves exactly as the query param does on `GET` - trailing inline `// ...` comments are stripped from diff output and ignored when comparing lines ### GET /api/hex2object decodes a tl-serialized hex string into a json object. - params: `hex` (string), `layer` (number, `"latest"` or `"auto"`), `hint` (optional layer number, only used with `layer=auto`), `type` (optional tl type to decode as) - `layer=auto` decodes against the newest layer and, on an unknown constructor id, retries the layers that define it — closest to `hint` first, newest first without one, up to 8 candidates - with `layer=auto` the response adds `via` (`"requested"` or `"fallback"`) and `tried` (the layers attempted, in order) - a candidate that decodes but leaves bytes over is not accepted while another candidate might consume the whole payload; if none does, the best partial is returned with `bytesRemaining` - an explicit numeric `layer` never falls back: an unknown constructor id is still a 400 - response: `{ ok: true, result: object, layer: number, via?: string, tried?: number[], bytesRemaining?: number }` or `{ ok: false, error: string }` - `bytesRemaining` is present only when the object decoded without consuming the whole payload; the result is still returned, but the layer or the hex is wrong - `long`, `int128` and `int256` decode to strings; `long` is signed, so a negative `access_hash` reads back negative - `bytes` decodes to an array of byte values - hex with an odd length or non-hex characters is rejected rather than decoded as garbage - a vector whose element count cannot fit in the remaining bytes is rejected - a boxed vector (`0x1cb5c415`) decodes without `type`: its elements carry their own constructor ids, which is what validates the read - a vector of *bare* elements — `Vector`, `Vector` — is byte-identical to a boxed one on the wire, so it needs `type=Vector`. without it the first element is read as a constructor id and the decode fails - `type` reads the payload as that type and still reports `bytesRemaining`. an empty `type=` is treated as absent - a `type` the layer does not define is a 400 (`unknown type: X`) rather than a silent fall back to constructor dispatch, and when the payload decodes to a constructor that is not a member of the named type that is a 400 too — so a wrong hint fails loudly instead of returning a plausible object ### GET /api/object2hex encodes a json object into a tl-serialized hex string. - params: `object` (json string), `layer` (number or `"latest"`) - a top-level json array encodes as a boxed vector, so a value from `hex2object` round-trips back through this endpoint - response: `{ ok: true, hex: string }` or `{ ok: false, error: string }` - pass `long` / `int128` / `int256` values as strings: a bare json number loses precision past 2^53, so `-6100526118417511800` would encode as `-6100526118417511424` - `bytes` values must be arrays of byte values; a bare json number is rejected - payloads are capped at 1 mib ### GET /api/parse parses a layer's tl schema into structured entries. - params: `layer` (number or `"latest"`), `format` (optional, `"pretty"`) - response: `{ layer: number, types: ParsedEntry[], functions: ParsedEntry[] }` - with `format=pretty`: plain text with expanded constructors and annotations ### POST /api/parse parses arbitrary tl schema text into structured entries. - params: `format` (optional, `"pretty"`) - body: raw tl schema text (`Content-Type: text/plain`) - response: `{ types: ParsedEntry[], functions: ParsedEntry[] }` - with `format=pretty`: plain text with expanded constructors and annotations #### ParsedEntry `{ name: string, id: string, params: ParsedParam[], type: string, vector?: true }` #### ParsedParam `{ name: string, type: string, repr: string, optional?: true, vector?: true, flag?: { field: string, bit: number } }` `type` is always the unwrapped leaf type (e.g. `"User"` not `"Vector"`), for bare `vector` as well as boxed `Vector`. `repr` is the raw type string as it appears in the schema (e.g. `"flags.0?Vector"`). `vector` and `optional`/`flag` provide structured access to the metadata. ### GET /api/find-ctor looks up which layers contain a given boxed constructor id. useful when a hex payload can't be decoded against the layer you expected (e.g. a telegram client lags a layer or two behind the latest schema). - params: `id` (hex like `0x3ae56482` or `3ae56482`, or decimal), `hint` (optional layer number; closer layers are listed first) - response: `{ id: string, names: string[], layers: { layer: number, name: string }[] }` - `id` is normalised to `0xXXXXXXXX` hex - `names` lists every distinct constructor name seen for this id across layers (usually 1) - `layers` is sorted by `|layer - hint|` ascending when `hint` is provided (ties broken by lower layer first), otherwise newest-first - empty `layers` means no layer in the index has this ctor id - `prefix` (2 to 8 hex digits, optional `0x`) searches by partial id instead, for when a truncated dump only gave you the first bytes - with `prefix`: `{ prefix: string, total: number, matches: { id: string, names: string[], layers: number[], firstLayer: number, lastLayer: number }[] }`, sorted by `lastLayer` descending - `layers` is capped at the 20 most recent; `firstLayer` / `lastLayer` always describe the full span - `total` counts every matching id before `limit` (optional, default 50, max 500) - `id` and `prefix` are mutually exclusive, and a prefix shorter than 2 digits is rejected because it matches a sixteenth of the corpus ### GET /api/search searches across all layers, either as raw line matching (default) or as a structured query over parsed definitions (`structured=true`). - params: `input` (string, min 2 chars on the raw path), `caseSensitive` (boolean), `regex` (boolean), `wholeWord` (boolean), `structured` (boolean, `"true"` / `"1"`), `limit` (number, default 500) - text response: `{ query: string, results: { layer: number, matches: { lineIndex: number, line: string }[] }[], total: number }` - structured response: `{ query: string, mode: "structured", results: SearchGroup[], total: number, totalLayers: number }` - dispatch is explicit, never sniffed: without `structured=true` the raw line matcher always runs. `regex=true` or `wholeWord=true` force the raw path even when `structured=true` is also set — raw wins - back-compat is total, not probabilistic: every existing caller omits `structured` and gets exactly the bytes it got before, and the text response never carries `mode` - the opt-in exists because six grammar prefixes are also real tl parameter names in the schema — `id:long`, `type:SecureValueType`, `name:string`, `result:BotInlineResult`, `layer:int`, `field:string`. `input=id:long` is a legitimate raw search matching 22,963 lines; auto-detecting grammar would silently turn it into a ctor-id search for `long` and return nothing - `regex=true` compiles `input` as a RE2 pattern (linear-time, redos-safe): usual regex syntax (`.`, `*`, `+`, `?`, `|`, `[...]`, anchors, `\b`) but no backreferences or lookaround; invalid or unsupported patterns return no results - `limit` caps returned line matches on the raw path and returned groups on the structured path - the 2-char minimum applies to the raw path only; the structured path just requires `input` to parse into at least one token, so `is:ctor` and one-character values are accepted. an empty or whitespace-only `input` is a 400 on both #### limits and flags - the structured path 400s when `input` parses to zero tokens, when the query exceeds 12 tokens or 4 text terms, or when `limit` is not a number - text terms are every token except `is:` / `has:` / `fields:` / `layer:` — plain words and field-prefixed tokens like `uses:=InputPeer` alike. those four are exempt from the 4-term cap (they are O(1) predicates) but still count toward the 12; each text term costs a full scan of the index, which is what the cap bounds - structured `limit` is clamped to 1..1000 (default 500). the raw path's `limit` is untouched — `limit=-5` there still returns nothing and a non-numeric `limit` is not an error - `caseSensitive` is a no-op on the structured path: the grammar always matches case-insensitively - `structured` is read leniently (`true` or `1`), but `regex` / `wholeWord` only engage the raw matcher when they are exactly `true` — `regex=1` has never been a regex search. the structured gate reads them leniently, so `structured=1®ex=1` takes the raw path without enabling regex - a repeated query param takes its last value: `?input=a&input=b` searches for `b` - a type slot keeps at most 4 comma-separated tests and silently drops the rest. because the tests AND together, dropping one *broadens* the result set: `field:a:x1,x2,x3,x4,x5` round-trips as `field:a:x1,x2,x3,x4` #### query fields terms are whitespace-separated. plain terms sharing a field are OR-ed, different fields are AND-ed. a **paired** term (`field::<...>` / `uses::<...>`) is its own AND group, so two paired terms on the same field intersect rather than union — `uses:=InputPeer uses:=Chat` is 328 on layer 224 but `uses:=InputPeer:-? uses:=Chat:-?` is 0. a term with no recognised prefix is free text, matched against name, id, result type, parameter names and parameter types. - `name:` — constructor / method name - `result:` (`returns:`, `ret:`, `union:`) — result type - `field:` (`param:`, `arg:`) — parameter name - `type:` — raw parameter type string, `flags.N?` prefix and `Vector<>` included - `uses:` — unwrapped leaf parameter type - `id:` — constructor id hex - `is:` — `ctor` or `method` (`function` accepted for `method`) - `has:` — `flags` or `vector`, scoped to the whole definition and including the result type - `fields:` — parameter count - `layer:` — layer number - `fields:` and `layer:` accept `N`, `>N`, `=N`, `<=N` - `is:` / `has:` / `fields:` / `layer:` are pure predicates: they narrow results but never produce a match reason #### modifiers - `=` — exact instead of substring: `name:=user` matches only `user`, `name:user` also matches `userFull` - `-` — negation: `-is:method`, `is:ctor -inputPeer` - both apply uniformly to `field:` tokens and to bare free-text terms - an exact free-text term is compared against name, id, result type, parameter names and parameter types, and skips the whole-line substring fallback that a non-exact term still gets - `field::` / `uses::` — pairs both halves against the same parameter: `field:peer:InputPeer`; each half takes its own `=` / `-` #### parameter shape a type slot accepts shape tests instead of a type name. two axes, each negatable with a leading `-`, comma-separated to require several at once. because they sit in the pair slot they bind to the same parameter, which a second standalone token could not do. - `?` / `-?` — the parameter is / is not behind a flag bit - `[]` / `-[]` — the parameter is / is not `Vector<>`-wrapped - in a **pair** slot the test binds to one parameter: `field:peer:-?` is a `peer` parameter that is not optional, and `uses:=InputPeer:-[]` is `InputPeer` in any non-Vector form, including `flags.0?InputPeer` - **standalone**, a negated test negates the whole token, so `type:-[]` means "this definition has no Vector parameter" (1888 on layer 224) rather than "has a non-Vector parameter" (1865). `type:[]` (465) and `type:-[]` partition the layer; use the pair form when you mean the per-parameter reading - `uses:=InputPeer:?,-[]` requires both on one parameter - a test cap of 4 per slot bounds the comma list; more than one test per axis is redundant by construction - `has:vector` is not the same question: it is definition-scoped and also true when the *result* type is a vector, so it is broader than `type:[]` - `=` is a no-op on a shape test — `type:=?` and `type:?` are the same query #### type: vs uses: `type:` matches the parameter type exactly as written in the schema, so `type:flags.0?` and `type:Vector<` are meaningful queries. `uses:` matches the leaf type after unwrapping `flags.N?` and `Vector<>`, so `uses:=InputPeer` finds a parameter typed `InputPeer`, `flags.0?InputPeer` or `Vector` alike. `uses:` looks at parameters only and deliberately never matches the result type — `result:=InputPeer` (alias `union:`) answers that question. neither can express "unwrap the flag prefix but keep the vector" on its own; that is what the shape tests above are for. #### SearchGroup `{ name: string, id: string, line: string, isFunction: boolean, namespace: string, layers: number[], firstLayer: number, lastLayer: number, versions: number }` - one group per distinct `name#id`. the combinator id is the crc32 of the normalized definition, so a distinct id is a distinct signature version and an unchanged definition repeated across a hundred layers collapses into a single group - `layers` is ascending; `firstLayer` / `lastLayer` are its bounds - `versions` is how many distinct ids that `name` has across the entire index — total signature history, deliberately NOT narrowed by a `layer:` filter in the query, while `layers` / `firstLayer` / `lastLayer` cover only what the query matched. the asymmetry is intentional: `versions` is a claim about the schema ("this name has N signatures"), so `name:=messages.sendMessage layer:>223` returns `layers: [224]` with `versions: 10` - `line` is the definition text as of `lastLayer` - `total` is the number of groups, `totalLayers` the number of layers scanned - sorted by `lastLayer` desc, then `firstLayer` desc, then `name` asc #### examples - `structured=true&input=uses:=InputPeer` — 15,382 raw line hits across the index collapse into 445 groups - `structured=true&input=name:=inputPeerUser` — 2 groups: `#7b8e7de6` (layers 105..132, `user_id:int`) and `#dde8a54c` (layers 133..224, `user_id:long`) - `structured=true&input=is:method layer:>220` — every method present in a layer above 220 ### GET /api/errors returns telegram api errors with optional filtering. - params: `code` (optional, http code like `400`), `method` (optional, exact tl method name like `messages.sendMessage`), `q` (optional, case-insensitive substring matched against error name and description), `limit` (optional, default 500, max 1000) - `q` also resolves a concrete error against its template, so `q=FLOOD_WAIT_42` finds `FLOOD_WAIT_%d`; the matching entry then carries `matched` (what you passed) and `params` (the numbers extracted) - results are sorted before `limit` is applied, so a limited response is the first N by code and name - a non-numeric `limit` is a 400 rather than an empty result set - response: `{ total: number, results: ErrorEntry[], layer?: number }` - `total` is the unfiltered count of matching entries; `results` is capped by `limit` and sorted by code, then name - `layer` is the schema layer the error database corresponds to (from telegram's `/api/errors.json`); omitted when the cached database predates the field #### ErrorEntry `{ code: number, name: string, description?: string, methods: string[] }` - `name` may contain `%d` placeholders (e.g. `FLOOD_WAIT_%d`) for errors with embedded numbers - `methods` lists every tl method known to produce this error; can be empty for catch-all/network errors - `description` is omitted when no description is available for the error ### GET /api/errors/method returns all errors a specific tl method can produce, plus method classification flags. - params: `name` (string, exact tl method name like `messages.sendMessage`) - response: `{ method: string, flags: MethodFlags, errors: ErrorEntry[], layer?: number }` - 404 when `name` is unknown to the errors database (no errors and no flags) #### MethodFlags `{ user_only?: true, bot_only?: true, business_supported?: true, unauthed_allowed?: true }` - only flags that apply are present; absent flags mean false ### GET /api/methods returns every tl method with the capabilities telegram allows for it. - params: `capability` (optional, `"user"`, `"bot"`, `"business"`, `"unauthed"` or `"any"`, default `"any"`), `q` (optional, case-insensitive substring on method name), `limit` (optional, default 200, max 1000) - response: `{ layer: number | null, total: number, counts: { user: number, bot: number, business: number, unauthed: number, both: number }, methods: { name: string, user: boolean, bot: boolean, business: boolean, unauthed: boolean, errorCount: number }[] }` - the method universe comes from the newest archived layer, so a method in none of telegram's capability lists is still listed with all four flags false - `user_only` and `bot_only` are exclusion lists rather than grants, so `user` is false only for a `bot_only` method and `bot` is false only for a `user_only` one - `layer` is the stamp on `errors.json`, which can lag the schema layer the method list came from - `total` counts every match before `limit`, and `counts` is over all matches rather than the returned page - 404 when the errors database is missing ### GET /api/config returns telegram's machine-readable api constants and push notification templates (mirrored from the official `/api/config.json`). - params: `group` (optional, exact constant group name like `config description`), `q` (optional, case-insensitive substring) - default response: `{ layer: number, constants: Record, push: Record, push_arguments: Record | string[]> }` - with `group`: `{ layer: number, group: string } & ConfigConstant`, or 404 when the group is unknown - with `q`: `{ layer: number, query: string, constants, push }` filtered to constant groups (by name/description/entry key/entry value) and push templates (by key/template) matching the substring - 404 when the config database has not been fetched yet - `layer` is the stamp telegram puts at the top of its own `config.json`; it labels that docs dump and does not version the config, which is layer-independent #### ConfigConstant `{ description: string, type: string, entries: Record | string[] }` - `type` is `"map"` (keyed `entries` object) or `"list"` (`entries` array); `entries` shape follows `type` - `description` is the markdown blurb telegram attaches to the constant group - every response also carries `appconfig` when the live snapshot exists (see below); it is absent when the appconfig watcher is not deployed #### AppConfigView `{ fetchedAt: string, values: Record, undocumented: string[] }` - live `help.getAppConfig` values pulled from telegram over mtproto, not scraped from the docs site - `values` is scoped to what the response already returned: the whole map with no params, the group's keys with `group`, and the rows matching the needle with `q` - `undocumented` names live keys telegram serves but documents nowhere - `rich_message_max_blocks` and friends are only reachable this way - a documented key with no live value simply has no entry in `values`: telegram gates parts of the config per account and region, so `values` is one account's view - values telegram derives from the fetching account (`phone_country_iso2`) are stripped before publishing, so the key stays documented but carries no live value - the snapshot is fetched by an account, and 51 keys (every `*_default` / `*_premium` limit pair) are withheld from unauthorized callers, so an unauthenticated fetcher cannot produce it ### GET /api/mtproto returns mtproto schema metadata. - response: `{ tdesktop?: { lines: string[], lineCount: number }, tdlib?: { lines: string[], lineCount: number } }` ### GET /api/mtproto/raw returns raw mtproto schema text. - params: `source` (`"tdesktop"` or `"tdlib"`) - response: plain text ### GET /api/feed returns an atom feed of layer releases, newest first, one entry per archived layer. - params: `limit` (optional, default 20, max 100) - response: `application/atom+xml` - each entry diffs its layer against the previous archived layer, never `layer - 1`, and summarises as `+14 added, -2 removed, 8 changed, 3 re-ided` - a re-id is a definition that kept its name but changed constructor id, which is the breaking case a client notices as an unknown ctor - entry ids are stable urns (`urn:jppgram:layer:229`) so readers dedupe correctly, and `updated` carries the layer's approximate release date - `name` (optional, exact ctor or method name) switches to a per-definition feed: one entry per archived layer in which that definition changed, so a client maintainer can watch a single type instead of every layer - per-definition entries carry their own urn (`urn:jppgram:definition:message:133`) so a reader subscribed to both feeds does not collapse them, and the summary names the id change, the fields added or removed and any type change - the oldest entry is the layer the definition first appeared in, titled `added in layer N` rather than `changed` - 404 when `name` matches no archived layer ### GET /api/dump describes the downloadable sqlite dump of the whole corpus: every archived layer, definition, parameter, doc string, error and method capability in one queryable file. - response: `{ generatedAt: string, bytes: number, gzipBytes: number, sha256: string, layers: number, maxLayer: number, tables: Record, columns: Record, url: string, gzipUrl: string }` - `tables` maps each table name to its row count, so a caller can size the import before spending the bandwidth - `columns` maps each table name to its column names in declaration order, read out of the shipped file when it was built, so it cannot drift from the schema the download actually has - `url` and `gzipUrl` are absolute; the file is served raw at `/dump.sqlite` and pre-gzipped at `/dump.sqlite.gz`, at the site root rather than under `/api`, and a request for the plain path with `Accept-Encoding: gzip` gets the pre-gzipped body too - `generatedAt` advances only when the content changed, so a mirror polls this one endpoint and re-downloads only on a new `sha256` - 404 with `{ error: "dump is not available" }` until the dump has been built #### dump schema the file body is a pure function of the corpus and the sqlite version that wrote it, so an unchanged corpus rebuilt on the same image produces a byte-identical file. - `dump_meta(key, value)` — `schema_version`, `layers`, `max_layer` and `newest_release`, and deliberately no build timestamp, which is what keeps the bytes stable - `layers(layer, line_count, ctor_count, method_count, released_at, release_source, preview)` — one row per archived layer, carrying the same counts and approximate dates `/api/layers` reports - `sources(layer, source)` — which upstreams ship each layer - `definitions(layer, name, id, return_type, is_function, namespace, line)` — one row per definition per layer, so a definition unchanged across a hundred layers still has a row in each - `definitions.line` is the definition's 0-based position in that layer's file, not its text: it shifts whenever anything above it changes, so body identity is `id`, the crc32 of the definition, and `COUNT(DISTINCT line)` measures position churn rather than signature churn - `params(layer, name, param, type, position, flag_field, flag_bit)` — one row per parameter, ordered by `position`; `flag_field` and `flag_bit` are null for a parameter that is not behind a flag bit - one flag bit can gate several parameters at once, so `(layer, name, flag_field, flag_bit)` is not unique; a self-join looking for renamed fields must match on `position` too or it cross-multiplies - `docs(kind, name, param, description)` — `kind` is `type`, `constructor` or `method`; `param` is null for the entity's own description and the field name for a per-field one - `errors(code, name, description)` and `error_methods(code, name, method)` — the errors database, joined on `(code, name)` - `method_capabilities(method, user_only, bot_only, business_supported, unauthed_allowed)` — telegram's per-method capability lists - sqlite has no boolean type, so `preview`, `is_function` and the four capability columns are 0 or 1 - indexes cover `definitions(name)`, `definitions(id)`, `params(name, flag_field, flag_bit, position)`, `params(flag_field, flag_bit)`, `docs(name)` and `error_methods(method)` - a sqlite version bump in the base image rewrites the file with no corpus change, so a new `sha256` means re-download rather than proof the schema moved - nothing derived is precomputed: id reuse, flag-bit reuse and first-shipped-in queries are one `GROUP BY` or self-join over `definitions` and `params` ### POST /api/sql runs one read-only query against the same sqlite file `/dump.sqlite` serves, for corpus-wide questions no dedicated endpoint answers. - body: `{ sql: string, limit?: number }` (`Content-Type: application/json`) - `limit` defaults to 200 and is capped at 1000 - response: `{ columns: string[], rows: unknown[][], rowCount: number, truncated: boolean, ms: number }` - `rows` are arrays positionally matching `columns`, not objects keyed by column name - `truncated` is true when the row cap or the 4 MB serialized-size cap stopped the walk, so the rows returned are a prefix of the answer rather than the whole of it - one statement per request, and it must be a `SELECT` or a `WITH`; recursive ctes are allowed - 400 with `{ error }` for a malformed body, or for a rejected statement: `only a single statement is allowed`, `only SELECT and WITH queries are allowed`, `ATTACH, DETACH, PRAGMA and VACUUM are not allowed` - 429 with `{ error: "too many concurrent queries" }`, 504 with `{ error: "query timed out" }` after 10 s, 503 with `{ error: "sql runner is unavailable" }` where the executor is not deployed - 503 with `{ error: "dump has not been built yet" }` until the `dump` pipeline stage has written the artifact, the same condition under which `/api/dump` answers 404 - writes are impossible rather than refused: the database is opened read-only, so sqlite itself answers `attempt to write a readonly database` - the tables, columns and indexes available are the ones under `#### dump schema` above - the schema is also introspectable through sql itself: `SELECT name FROM sqlite_master WHERE type = 'table'` lists the tables, `SELECT name, type FROM pragma_table_info('params')` lists one table's columns and `SELECT sql FROM sqlite_master WHERE name = 'params'` returns its full ddl - a result is reproducible locally by downloading the same artifact from `/dump.sqlite` and running the query against it - rate limited per ip at the edge, which answers 429 with `{ error: "too many requests" }`; the two 429 bodies differ so a caller can tell being throttled from the executor being saturated ### GET /llms.txt returns an [llmstxt.org](https://llmstxt.org) index of the site: a titled summary plus linked, described endpoints, for agents that would otherwise crawl the spa. - response: plain text markdown - layer counts and source names are derived from the live index, so the file cannot drift from what it describes - served at the site root, not under `/api`, because that is where agents look for it ### GET /api returns this document as plain text. ### GET /SKILL.md, GET /skill return the agent skill file (`SKILL.md`) as markdown. ### GET /method/{name}, GET /constructor/{name}, GET /type/{name}, GET /error/{name} server-rendered html pages, one per tl definition, type and error, for readers and search engines that do not run the spa. - response: `text/html` - a definition page shows the newest signature, parameters with their descriptions, the return type or owning type, the errors a method can return, and every id the name has carried across layers - a type page lists its constructors, the methods returning it and the definitions taking it as a parameter - an error page lists its codes, description and the methods documented to return it - the url scheme mirrors core.telegram.org, and a wrong kind or case answers 301 to the canonical url, so `/constructor/messages.sendMessage` lands on `/method/messages.sendMessage` - error urls spell `%d` as `X` (`/error/FLOOD_WAIT_X`), and a concrete error such as `/error/FLOOD_WAIT_420` answers 301 to its template - a name that never existed answers 404 with a noindex page - served at the site root, not under `/api` ### GET /sitemap.xml lists every crawlable page: the spa's top pages, every archived layer and consecutive layer diff, and every definition, type and error page. - response: `application/xml` - `lastmod` is set only where a harvested release date exists ### GET /robots.txt allows crawling everything except the sqlite dump, and links the sitemap. - response: plain text ### GET /api/notice returns the provenance and attribution notice: what is original to this project, every upstream schema source with its license, and which artifacts reproduce telegram documentation text. - response: plain text - 404 when `NOTICE` is missing from the deployment ### GET /api/license returns this project's own license, or a retained upstream license text. - params: `name` (optional, `"gpl-3.0"` or `"bsl-1.0"`) - response: plain text - without `name` it returns the terms covering this project's own code; with `name` it returns the upstream text the shipped layer data depends on, since definitions come from tdesktop (GPL-3.0) and tdlib (BSL-1.0) - 400 when `name` is neither upstream, 404 when the text is missing from the deployment ### expand options (for format=pretty) the following query params control which annotations are shown: - `constructor_ids` (default: true) — decimal constructor id - `param_count` (default: false) — number of params - `flag_bits_used` (default: false) — how many bits a flag field uses - `bit_number` (default: true) — flag field and bit number - `paired_fields` (default: true) — fields sharing the same flag bit - `type_descriptions` (default: true) — constructor/method description from docs - `field_descriptions` (default: false) — per-field description from docs ## sources and licensing this api is an unofficial index of telegram's tl schema, not affiliated with telegram messenger inc. - schemas are merged from eight upstreams: tdesktop (GPL-3.0 with openssl exception), telegram-tt (weba, GPL-3.0), tweb (webk, GPL-3.0), tdlib (BSL-1.0), danog/schemas, corefork.telegram.org, core.telegram.org and TGScheme/Schema - 181 layers ship in total and most are corroborated by several sources; per-layer provenance, including which upstream revision each layer's text came from, is in `data/meta.json` and surfaced by `/api/layers` and `/api/conflicts` - combinator ids are computed here as the crc32 of the normalized definition rather than copied from any source, so `/api/crc32` reproduces any of them from the definition text alone - type, field, error and config descriptions are documentation text written by telegram and reproduced from core.telegram.org and corefork.telegram.org; every config group carries a link back to the page it came from - live `help.getAppConfig` values are factual data telegram serves to any requesting client, with account-derived values stripped before publishing - per-artifact provenance is served at `/api/notice`, this project's own terms at `/api/license`, and the retained upstream texts at `/api/license?name=gpl-3.0` and `/api/license?name=bsl-1.0`