# Glotto > Glotto keeps your developer surface — mobile-first SDKs in 13 languages, docs, and an MCP server — provably correct and in lockstep with your spec, forever. ## Overview ### Glotto — SDKs, docs, and MCP from one API spec Source: https://glotto.dev/ **Spec → SDKs · Docs · MCP** **Spec in. Best-in-class SDKs, docs, and an MCP server — out.** Glotto keeps your entire developer surface — idiomatic SDKs in 13 languages (mobile-first), a docs site you own, and a multi-mode MCP server — provably correct and in lockstep with your spec, forever. The authored input is `petstore.openapi.yaml`: `operationId: listPets` names the call, and `nextCursorPath: next_cursor` tells the generated paginator where to find its next cursor. #### Built for what the others skip **The wedge** Where Stainless, Speakeasy, and Fern stop, Glotto keeps going. ##### Machine-guaranteed correctness Every regeneration is compile-verified and contract-tested across each language, drift-gated, and preserves your custom code — provably in lockstep with your spec. ##### Mobile-first SDKs A React Native / Expo spearhead plus Kotlin Multiplatform and native Android/iOS — the mobile targets no rival ships first-class. ##### Deploy on your terms Self-host or single-tenant, with managed release PRs across GitHub, GitLab, and Bitbucket — not a GitHub-cloud-only box. #### Three things, from one spec **The output** Every push regenerates all three, in lockstep with your API. ##### SDKs 13 languages, mobile-first, with a hand-crafted feel and machine-guaranteed correctness — not generated boilerplate. ##### Docs A standalone Astro docs site you own — multi-language snippets, search, and an MCP-ready index. ##### MCP server Dual-mode (Code Mode + per-operation) so AI agents can drive your API safely. #### Push a spec. Merge to publish. Then keep being right. **How it works** Five steps take your spec to published SDKs. The sixth is the one that matters: every push after the first re-runs the whole pipeline, proves the result still compiles and still honours your contract in every language, and preserves the code you wrote by hand. 1. **API spec** — Point Glotto at an OpenAPI, AsyncAPI, or GraphQL document — a file, a URL, a git ref in another repo, or your own exporter command. 2. **Glotto IR** — Your spec normalizes to one canonical, byte-stable intermediate representation, so every language generates from the same reading of your API rather than from its own. 3. **Codegen** — Every declared target emits at once — SDKs across 13 languages, your docs site, and the MCP server — from that single IR. 4. **Verify** — Before anything opens, each SDK is compiled in its own real toolchain and run against a shared contract suite, so "it generated" and "it works" are not the same claim. 5. **Release PR** — A reviewable pull request lands on GitHub, GitLab, or Bitbucket — your repos, your review, your history. Nothing is force-pushed and nothing publishes itself. 6. **Every push after** — The loop. Regeneration is drift-gated against the committed output, your hand-written code survives via a three-way merge, and a breaking change is reported before it ships, not after. #### Migrating from Stainless? **Switching costs, near zero** Stainless is winding down its hosted platform. Glotto picks up where it leaves off — a superset of its SDK languages, the same Astro docs approach, and a multi-mode MCP server including Code Mode and dynamic tools. One command converts your `stainless.yml`. - [Read the migration guide →](/docs/migrate-from-stainless) #### Ship the SDKs your users deserve. One spec. 13 languages. Docs and MCP included. - [Get started](/docs/getting-started) - [Read the docs](/docs) ## Docs ### Documentation Source: https://glotto.dev/docs/ Configuration reference, generated SDK guides, and the glotto.yml schema. ### Agent primitives Source: https://glotto.dev/docs/agent-primitives/ Token counting, context budgeting, vector math, structured decode and budget-aware retry — emitted with your SDK. When your API has AI-shaped endpoints, Glotto promotes those *shapes* into *behavior*: the generated SDK ships a small cohort of helpers for the work every agent loop repeats — accumulating streamed tool calls, counting tokens, budgeting a context window, comparing embeddings, pulling a JSON object out of model prose, and retrying without blowing the budget. They are part of the SDK, not a companion library. Every helper is **pure, dependency-free, deterministic and never-throwing**, with identical semantics in all 13 languages — no runtime to install, no vendor data table, nothing to keep in sync. #### What your SDK emits The cohort is gated per family, on the shape of your API's **responses** — so you get the helpers that make sense for the endpoints you actually have: | Helper | Emitted when your API has | | --- | --- | | `ToolCallAccumulator` | an SSE streaming operation | | `estimateTokens`, `Tokenizer`, `TokenBudget`, `RetryBudget` | a response carrying a token-usage shape (a `usage` object, or token-named numeric fields) | | `VectorMath`, `NearestMatch` | a response carrying an embedding vector (`embedding` / `embeddings` / `vector`) | | `StructuredOutput` | any of the above | **Streaming is not a precondition.** A non-streaming API whose responses report token usage emits the budgeting helpers with no SSE runtime at all; a non-streaming embeddings API emits the vector helpers the same way. Only `ToolCallAccumulator` is tied to SSE. Conversely, an API with no AI shape at all emits none of them, and its generated SDK is byte-identical to one generated before these helpers existed. #### Counting tokens Two helpers, deliberately: a cheap estimate and a real tokenizer. ```ts estimateTokens('hello world'); // 3 — chars/4 heuristic, O(1) work Tokenizer.countTokens('hello world'); // 2 — character-class pre-tokenizer + subword model ``` `estimateTokens` is the flat ≈4-characters-per-token approximation. Reach for it when you want a rough number in a hot loop and precision does not matter. `Tokenizer.countTokens(text)` is the one to use when the number drives a decision — whether the next message fits, how much history to trim. It is a real tokenization algorithm: a regex-free character-class pre-tokenizer that groups letters, digits, whitespace and symbols into runs and costs each run with a calibrated subword model. That puts it materially closer to a real BPE count than chars/4, while shipping **no vocabulary file** — nothing model-family-specific to download, version, or keep current. It never throws (empty input is `0`) and it is deterministic — the same string always costs the same, in every language. > It is an estimate, not a billing oracle. If you need counts that match a specific vendor's > tokenizer exactly, call that vendor's tokenizer; `countTokens` exists so you don't need a > dependency to make a *sizing* decision. #### Budgeting a context window `TokenBudget` does exact accounting against a ceiling you set, seeded from what the wire actually reported: ```ts const budget = new TokenBudget(128_000); budget.add(response.usage.total_tokens); // seed from the response budget.remaining(); // 128_000 − consumed, clamped at 0 budget.fits(4_000); // does the next call still fit? budget.exceeded(); // over the ceiling? ``` Pair it with the tokenizer to decide *before* you spend: `budget.fits(Tokenizer.countTokens(prompt))`. #### Budget-aware retry `RetryBudget` composes `TokenBudget`, so a retry loop stops when either the attempt count **or** the remaining tokens are exhausted — the failure mode where a loop retries itself out of context while still under its attempt limit: ```ts const retry = new RetryBudget(3, budget); if (retry.shouldRetry(attempt, Tokenizer.countTokens(prompt))) { await sleep(retry.nextDelayMs(attempt, 500, 30_000)); } ``` Backoff is deterministic and capped — same inputs, same delay, so a retry path is testable. #### Embeddings and vectors `VectorMath` covers the retrieval-loop math, so a similarity search doesn't pull in a numerics library: ```ts const hits = VectorMath.topKNearest(queryEmbedding, docEmbeddings, 5); // [{ index, score }, …] — sorted by score, ties broken by index ``` Also `cosineSimilarity`, `dotProduct`, `magnitude` and `normalize`. Mismatched, empty or zero-magnitude vectors degrade to `0.0` rather than throwing — a ranking loop over ragged data returns a usable ordering instead of a stack trace. #### Structured output Models return JSON wrapped in prose, or in a fenced block, or truncated. `StructuredOutput.decode` pulls the first balanced JSON object out and parses it: ```ts const { ok, value, json } = StructuredOutput.decode(message.content); if (ok) use(value); // parsed object; otherwise inspect the raw `json` ``` It never throws. When the model returns malformed or partial JSON, `ok` is `false` and the raw bytes stay on `json` so you can log, repair or retry. #### Accumulating streaming tool-call deltas `ToolCallAccumulator` reassembles a tool call whose arguments arrive as partial JSON fragments across many SSE events. Because it is tied to the streaming runtime, it is documented with the rest of streaming — see [Streaming](/docs/streaming#agent-helpers-accumulating-streaming-tool-call-deltas). #### Naming in your language Each engine spells the cohort its own way while keeping the semantics identical — `countTokens` in TypeScript and React Native, `count_tokens` in Python and Ruby, `CountTokens` in Go, and the equivalent idiom elsewhere. The behavior, the gate, and the numbers do not vary by language. ### Authentication Source: https://glotto.dev/docs/authentication/ Bearer tokens, API keys, OAuth2 client credentials, and basic auth: how every generated SDK takes credentials, with env-var fallback and typed scopes. Every generated client takes its credentials at construction. For a bearer-token API, pass `token` — or set the environment variable the SDK is generated to read: **TypeScript** (regenerated + byte-diffed in CI `a0f8af4b462c`) ```ts import { Client } from 'glotto-sdk'; const client = new Client({ token: '' }); const result = await client.pets.createPet({ name: 'Biscuit', species: 'cat' }); ``` **React Native** (regenerated + byte-diffed in CI `a1fefe7d3458`) ```ts import { Client } from 'glotto-sdk'; const client = new Client({ token: '' }); const result = await client.pets.createPet({ name: 'Biscuit', species: 'cat' }); ``` **Python** (regenerated + byte-diffed in CI `2242160f8958`) ```python from glotto_sdk import Client, PetCreate, PetCreateSpecies client = Client(token="") result = client.pets.create_pet(body=PetCreate(name='Biscuit', species=PetCreateSpecies.CAT)) ``` **Go** (regenerated + byte-diffed in CI `f0c565cc46f8`) ```go package main import ( "context" "fmt" sdk "example.com/glotto-sdk-go" ) func main() { ctx := context.Background() client := sdk.NewClient(sdk.WithToken("")) result, err := client.Pets.CreatePet(ctx, sdk.PetCreate{Name: "Biscuit", Species: "cat"}) if err != nil { panic(err) } fmt.Println(result) } ``` **Java** (regenerated + byte-diffed in CI `12c05e098737`) ```java import com.glotto.Client; import com.glotto.models.PetCreate; import com.glotto.models.PetCreateSpecies; public class Snippet { public static void main(String[] args) throws Exception { Client client = Client.builder().token("").build(); var result = client.pets().createPet(PetCreate.builder().name("Biscuit").species(PetCreateSpecies.CAT).build()); System.out.println(result); } } ``` **Kotlin** (regenerated + byte-diffed in CI `0f5e53ae2522`) ```kotlin import com.glotto.Client import com.glotto.models.PetCreate import com.glotto.models.PetCreateSpecies import kotlinx.coroutines.runBlocking import kotlinx.coroutines.flow.collect fun main() = runBlocking { val client = Client(token = "") val result = client.pets.createPet(PetCreate(name = "Biscuit", species = PetCreateSpecies.CAT)) println(result) } ``` **C#** (regenerated + byte-diffed in CI `c5013ce0e88b`) ```csharp using Glotto; using PetCreate = Glotto.Models.PetCreate; using PetCreateSpecies = Glotto.Models.PetCreateSpecies; var client = new Glotto.Client(""); var result = await client.Pets.CreatePet(new PetCreate("Biscuit", PetCreateSpecies.Cat)); Console.WriteLine(result); ``` **PHP** (regenerated + byte-diffed in CI `1acd16081b46`) ```php '); $result = $client->pets->createPet(new Glotto\Models\PetCreate('Biscuit', Glotto\Models\PetCreateSpecies::Cat)); var_dump($result); ``` **Ruby** (regenerated + byte-diffed in CI `a2b854f6593f`) ```ruby require 'glotto' client = Glotto::Client.new(token: '') result = client.pets.create_pet(body: { name: 'Biscuit', species: 'cat' }) puts result ``` **Rust** (regenerated + byte-diffed in CI `716e9ec181d1`) ```rust let client = Client::default().with_token(""); let result = client.pets().create_pet(PetCreate { name: "Biscuit".to_string(), species: PetCreateSpecies::Cat, extra_fields: Default::default() }).await?; ``` **Swift** (regenerated + byte-diffed in CI `0cc357352baf`) ```swift import Foundation import GlottoSdk let client = Client(token: "") let result = try await client.pets.createPet(body: PetCreate(name: "Biscuit", species: PetCreateSpecies.cat)) ``` **Dart** (regenerated + byte-diffed in CI `67567d6df435`) ```dart import 'package:glotto_sdk/glotto_sdk.dart'; final client = Client(token: ""); try { final result = await client.pets.createPet(PetCreate(name: "Biscuit", species: PetCreateSpecies.cat)); } finally { client.close(); } ``` **Elixir** (regenerated + byte-diffed in CI `b97af402f1d7`) ```elixir client = Glotto.new(token: "") {:ok, result} = Glotto.Pets.create_pet(client, %Glotto.PetCreate{name: "Biscuit", species: "cat"}) ``` If you omit `token`, the client falls back to its configured env var (e.g. `PETSTORE_TOKEN`). The scheme and env var come from `glotto.yml#/client_settings/auth`. #### Other schemes The constructor options match the API's declared security scheme: - **API key** — `apiKey`, with `apiKeyName` (default `X-API-Key`) and `apiKeyIn` (`header` or `query`, default `header`). - **OAuth2** — `accessToken`, or `clientId` / `clientSecret` / `tokenEndpoint` / `scope` for the client-credentials flow (the client caches the token and de-dupes in-flight refreshes). - **Basic** and **custom** schemes are generated when the spec declares them. #### Typed scope constants When an OAuth2 scheme in your spec declares scopes, every generated SDK also **exports them as typed constants** — a module-level scope enumeration (sorted scope → description), so consumers reference the API's scopes by a checked name instead of a hand-typed string. In TypeScript: ```ts export const OAuth2Scopes = { 'admin:settings': 'Administrative settings access', read: 'Read access', write: 'Write access', } as const; export type OAuth2Scope = keyof typeof OAuth2Scopes; ``` Each language gets its native idiom: | Language | Emitted shape | | :-- | :-- | | TypeScript, React Native | `OAuth2Scopes` const map (`as const`) + `OAuth2Scope` key type | | Python | `class OAuth2Scope(StrEnum)`, `UPPER_SNAKE` members | | Go | `type OAuth2Scope string` + a sorted `const` block (`OAuth2ScopeAdminSettings`) | | Java | `public enum OAuth2Scopes`, each constant carrying scope + description | | Kotlin | `enum class OAuth2Scope(val scope: String, val description: String)` | | C# | `public static class OAuth2Scopes` of `const string` fields | | PHP | `enum OAuth2Scope: string` (a backed enum) | | Ruby | an `OAuth2Scopes` module of string constants | | Rust | `pub const OAUTH2_SCOPES: &[(&str, &str)]` | | Swift | `public enum OAuth2Scope: String` | | Dart | `class OAuth2Scope` of `static const String` fields | | Elixir | a `Glotto.OAuth2Scope` module (`scopes/0`) | Methods advertise what they need, too: an operation whose security requirement carries required scopes gets a `Required OAuth2 scopes: …` note in its generated doc comment (JSDoc, docstring, Javadoc, and so on), so the requirement shows up in your editor at the call site. Both emissions appear only when the spec declares scopes — a scope-less spec generates exactly the same SDK as before. And they are metadata + docs only: the client attaches the token as configured and never gates a call on scopes. #### OAuth2 token-endpoint errors When the token endpoint rejects a request, the SDK raises its usual typed error — the same `ApiError` (or per-status subclass) you already catch — and additionally attaches the [RFC 6749 §5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2) reason as a typed `OAuthErrorResponse` carrying `error`, `error_description`, and `error_uri`. That lets you branch on the cause instead of hand-parsing the raw body: ```python try: client.pets.list_pets() except ApiError as err: if err.oauth_error and err.oauth_error.error == "invalid_grant": reauthenticate() # the user's grant expired or was revoked elif err.oauth_error and err.oauth_error.error == "invalid_client": raise ConfigError(err.oauth_error.error_description) # bad deployment credentials ``` It is populated on both the in-client client-credentials fetch and the standalone `exchange_code_for_token` / `refresh_access_token` helpers. The decode is best-effort: a token endpoint that returns a non-JSON, non-object, or `error`-less body simply leaves the field unset rather than failing differently, and the error's status, raw `body`, and headers are unchanged either way. | Language | Accessor | | :-- | :-- | | Python | `err.oauth_error` (`OAuthErrorResponse`) | | Ruby | `err.oauth_error` | | PHP | `$err->oauthError` | | Java | `err.oauthError()` | | Kotlin | `err.oauthError` | | C# | `err.OauthError` | Go and TypeScript are not in this list yet: their token fetches still surface a plain `error` / `Error` rather than the typed `ApiError`, so there is no typed error to attach the reason to. On React Native, the bearer token can be backed by secure storage — see the [React Native guide](/docs/react-native). Every other SDK language accepts an optional pluggable token store on the same idea: pass a `token_store` (an object exposing `get_token`, plus `set_token` for OAuth2 — idiomatic casing and shape per language: a TypeScript `tokenStore` option, a Python `token_store` kwarg, a Go `WithTokenStore` option, a Rust `with_token_store` builder, a Dart `tokenStore` parameter, an Elixir `:secure_token_store` option, and the constructor knobs on Ruby, PHP, C#, Java, and Kotlin) to resolve the bearer token from your own secure backend per request, and to persist the OAuth2 client-credentials token cache across process restarts. On APIs with per-endpoint security (a `security_schemes` registry), every one of those clients takes one **read-only** store per bearer-style scheme instead of the single knob, resolved before that scheme's static credential — a Ruby `token_store_` kwarg, a PHP `$tokenStore` parameter, a C# `TokenStore` option, Java and Kotlin `tokenStore` constructor params, a TypeScript and React Native `tokenStore` option, a Python `token_store_` kwarg, a Go `WithTokenStore` option, a Rust `with_token_store_` builder, a Dart `tokenStore` parameter, and an Elixir `:secure_token_store_` option. The store is read-only there because a multi-scheme client attaches static credentials and never runs a token fetch, so it has no cache to persist. No storage backend is bundled — bring your vault, OS keyring, or encrypted file store. ### Breaking-change detection Source: https://glotto.dev/docs/breaking-changes/ Every pull request that changes your spec is classified against the last build — which changes break a caller who upgrades, and which do not. A spec change that looks small on the wire can delete a method from thirteen SDKs. Renaming an `operationId`, tightening a type, making an optional parameter required — each one compiles fine on your side and breaks every caller who upgrades. So Glotto classifies the change before you merge it. Every pull request that touches your spec gets a **preview build**, and that build carries an API-surface comparison against the last one: what changed, and for each change, whether a caller who upgrades keeps working. #### What you get On the pull request, in the preview-build comment: | Target | Status | Files | Changes vs. base | API surface | | --- | --- | --- | --- | --- | | `typescript` | ✅ built | 42 | 3 changed | ⚠️ 1 breaking | | `python` | ✅ built | 39 | 3 changed | ⚠️ 1 breaking | The **API surface** column is the classification. `⚠️ N breaking` means at least one change would break a caller who upgrades; `N non-breaking` means the surface moved but nobody's code stops working; and a clean comparison says so rather than staying silent. Expanding the section beneath the table lists every change, **breaking first**, with the operation or model it belongs to. That ordering is the point: the breaking ones are the whole reason to read the list, and they are what a reviewer needs before approving. #### What counts as breaking The classification is about **your callers**, not about the diff: - **Breaking** — a method or model disappears, is renamed, or changes shape in a way an existing call site cannot survive. A removed operation, a renamed `operationId`, a parameter that becomes required, a response field that changes type. - **Non-breaking** — the surface grew or was refined without invalidating existing code. A new operation, a new optional parameter, a new response field, a widened type. Two things are deliberately **not** breaking, because they are the mechanisms for not breaking: - **An [alias](/docs/glotto-yml-api-surface#aliases--deprecated).** Renaming `createRecord` to `upsertRecord` and declaring `aliases: { createRecord: upsertRecord }` keeps the old method name in every SDK, routed to the new operation — so the rename re-occupies the surface it vacated, and the classification says non-breaking. That is the honest answer: no caller breaks. - **A [transform](/docs/transforms).** Corrections you apply to the spec before generation are part of the input, so the comparison sees the corrected surface on both sides. #### What it compares against **The previous build of the same target.** Glotto stores the spec revision every preview build was generated from, and the next build classifies against it — so the comparison always describes the change *this* pull request makes, not the accumulated drift since some fixed point. Two consequences worth knowing rather than discovering: - **A target with no previous build has nothing to compare against**, so its first preview reports no API surface rather than reporting everything as new. A brand-new target is not a breaking change to a caller who does not have it yet. - **Targets on different baselines get their own sections.** A target added later has a different last-build than its siblings, and merging the two comparisons would produce a diff that accurately describes neither. #### Making it impossible to miss ```yaml # glotto.yml settings: detect_breaking_changes: true ``` With this set, a breaking change is called out **at the top of the preview comment**, naming how many there are, instead of living in one table cell and a collapsed section. Leave it unset and the classification is still there — it is just quieter. Set it once your first release is out and callers exist. Before that, everything is breaking and nothing is. **This is emphasis, not enforcement.** Glotto does not set a commit status or a check run on your pull request, so nothing here can be made a required check and nothing blocks a merge. If you want that, make it a branch protection rule on your own side; what Glotto guarantees is that the information is on the pull request, correct, and impossible to overlook. #### What this does not do - **It does not decide your version number.** The classification is an input to that decision, not a substitute — a breaking change with an intentional major bump is a normal release, and Glotto does not guess which one you meant. - **It does not compare against an arbitrary point in history.** The baseline is the last build, by design: a comparison against a ref you name would answer a different question, and answering it well needs a route we do not offer today. - **It does not classify behaviour, only surface.** An operation that keeps its signature and changes what it returns at runtime is invisible here. That is what your [contract tests](/docs/verification-report#real-compile--contract-statuses-the-checks-loop) are for. ### CLI reference Source: https://glotto.dev/docs/cli/ Every command in the published glotto CLI — init, schema, workspace, generate, mcp, verify-attestation, login, logout — with flags and exit codes. The Glotto CLI is invoked as `glotto `. Run `glotto` with no arguments to print the command list. Every command exits `0` on success, `1` on a run failure, and `2` on a usage or load error (an unknown flag, a missing `glotto.yml`). **This page documents the CLI you can install, and only that.** Every command below is in the published `@glotto/cli`; there are no others, and nothing here requires a plan, a flag, or an invitation to unlock. Glotto also delivers most of these capabilities **without** your running anything — keeping your committed SDKs verified, detecting drift, publishing to registries — on your pull request and your dashboard; those routes are described by what they produce and where you collect it, under [What Glotto also runs for you](https://glotto.dev/docs/cli/#what-glotto-also-runs-for-you) below. Most commands discover the config automatically: with no `--config`, Glotto looks for `.glotto/workspace.json`, then `glotto.yml`, then `glotto.yaml` — first in the current directory, then in each parent directory up to the filesystem root. So commands work from anywhere inside your project, not only from the directory holding the config. See [Getting started](/docs/getting-started) for the end-to-end workflow and the [glotto.yml reference](/docs/glotto-yml) for the configuration these commands consume. #### The workspace file A `.glotto/workspace.json` declares which project a directory belongs to. You only need one when a single repository holds **more than one** `glotto.yml`, or when you want an SDK written somewhere other than `sdks//`. A repository with one config at its root needs no workspace file — the upward walk already finds that config from any subdirectory, which is why `glotto init` does not write one. ```json { "config": "api/glotto.yml", "targets": { "typescript": "clients/typescript", "python": "clients/python" } } ``` - **`config`** (required) — path to the `glotto.yml` this workspace binds to. - **`targets`** (optional) — where each language's SDK is written. A language you leave out is written to `sdks//` as usual. Drift detection reads the same map, so it compares each language against the directory declared here — there is no second root to keep in step with the layout. Both are relative to the workspace root — the directory containing `.glotto/` — and must stay inside it. Absolute paths and `..` escapes are rejected. [`glotto workspace`](https://glotto.dev/docs/cli/#glotto-workspace) writes, checks, and explains the file: `glotto workspace show` prints the binding for the directory you are standing in, `glotto workspace validate` checks the file on its own, and `glotto workspace init` writes one with the `config` path filled in for you. Two keys you might expect are deliberately **not** accepted, because `glotto.yml` already owns those values and a second copy would drift out of step: `project` (that is `hosted.project`) and `openapi_spec` (that is `openapi.source`, resolved relative to the config). Setting either one reports [`GLOTTO_WORKSPACE_KEY_OWNED_ELSEWHERE`](/docs/diagnostics-fatal#the-workspace-file) naming where the value belongs. **Nearest declaration wins.** In a monorepo, give each project its own workspace file (or just its own `glotto.yml`): a command run inside `services/billing/` binds to the nearest declaration above it, not to the repository root. Where output lands follows the same rule — with a workspace file in effect, a relative output path is resolved against the **workspace root** rather than wherever you happened to run the command, so `glotto generate` writes to the same place from every directory. An explicit `--out` is always resolved against the current directory. #### How you get each command The published `@glotto/cli` on npm is a **thin client**: code generation runs on Glotto's infrastructure, so the codegen engines never ship in the installed binary. Sixteen commands are in it — `login`, `logout`, `init`, `migrate`, `schema`, `workspace`, `generate`, `mcp`, `breaking-changes`, `verify`, `verify-compile`, `verify-contract`, `verify-upload`, `verify-attestation`, `publish`, and `code-owners`. That list is complete. `glotto ` exits `2` with `unknown command`, and every section below carries the same label because every section below is a command you can run: - **Available in the published CLI** — `npm i -g @glotto/cli` gives you this command. **Thin client does not mean everything is remote.** Which side a command runs on follows from what it needs, not from a policy. `verify-compile` and `verify-contract` invoke your committed SDKs' own toolchains and test suites, and `publish` pushes with your own registry credentials — all three run entirely on your machine, and nothing about them reaches Glotto. `generate`, `verify` and `breaking-changes` need the code-generation and spec-normalisation engines, which stay server-side, so those three need `glotto login` or a `GLOTTO_API_TOKEN`. Each section below says which it is. What you will not find here is a reference section for a command you cannot obtain. Documenting one would mean printing a flag table and a shell sample under a note saying you cannot run it, which is not a caveat so much as an instruction that fails — so the one capability with no command left (`glotto migrate fern`, which needs the spec pipeline) is described by its outcome instead. See [What Glotto also runs for you](https://glotto.dev/docs/cli/#what-glotto-also-runs-for-you). #### glotto init > **Available in the published CLI** — `npm i -g @glotto/cli` Scaffold a new `glotto.yml` and `spec/openapi.yaml` in the current directory, then print the next steps. Without `--force` it refuses to overwrite an existing `glotto.yml` or `spec/openapi.yaml`. - `--force` — overwrite existing files without prompting. ```sh glotto init # scaffold glotto.yml + spec/openapi.yaml glotto init --force # overwrite an existing scaffold ``` #### glotto migrate > **Available in the published CLI** — `npm i -g @glotto/cli` Convert a competitor's config into a `glotto.yml`, and print a **migration report** saying what happened to every key — what was carried, what was normalized, what was dropped and why, and which values are placeholders you have to fill in. Nothing already on disk is overwritten without `--force`, and your OpenAPI document is never modified in place. - `--in ` — input file (default: `stainless.yml`). - `--out ` — output file (default: `glotto.yml`). - `--force` — overwrite an existing `--out`. - `--report ` — also write the migration report to a file. - `--openapi ` — read `x-stainless-naming` / `x-stainless-param` into `naming`, and resolve every `transforms` target against your real document so the translation is exact rather than inferred. See [Migrate from Stainless](/docs/migrate-from-stainless). - `--openapi-out ` — also write that document back out with the Terraform attribute-shaping extensions translated to their `x-glotto-*` equivalents. ```sh glotto migrate stainless # stainless.yml -> glotto.yml glotto migrate stainless --openapi openapi.yaml # exact transform + naming translation glotto migrate stainless --report migration-report.txt # keep the report alongside the config ``` **Fern.** Glotto also converts a `fern/generators.yml` (and `fern/docs.yml`), but that conversion reads your API surface through the spec pipeline, which stays server-side — so it is **not** something the published CLI runs. `glotto migrate fern` exits `2` and says so. What it converts is documented in [Migrate from Fern](/docs/migrate-from-fern); to have us run it, email [hello@glotto.dev](mailto:hello@glotto.dev). #### glotto workspace > **Available in the published CLI** — `npm i -g @glotto/cli` Inspect and author the [workspace file](https://glotto.dev/docs/cli/#the-workspace-file). Every subcommand answers a question about **which project the current directory belongs to**, so none of them takes `--config` — that flag is what bypasses discovery, and these commands exist to report what discovery does. ##### glotto workspace show Print the resolved binding for the current directory: the workspace root, the config it binds to, which file declared it and how far up the tree that was, the root relative output is anchored to, and where each enabled target's SDK is written. Exits `2` when no config is found anywhere above the current directory, or when the config it found cannot be parsed (the binding is still printed). - `--json` — emit the same facts as a JSON object instead of prose. ```sh glotto workspace show # "which project am I in, and where does my output go?" glotto workspace show --json # the same, for a script ``` ``` workspace root: /repo config: /repo/api/glotto.yml declared by: .glotto/workspace.json (in this directory) output root: /repo targets: python /repo/clients/python (declared) typescript /repo/sdks/typescript ``` The directories it reports are the directories `glotto generate` writes to — both commands resolve them through the same code, so the report cannot drift from the writer. `(declared)` marks a directory that came from the workspace file's `targets` map rather than the `sdks//` default. ##### glotto workspace validate Validate the nearest `.glotto/workspace.json` at or above the current directory, without generating anything and without requiring `glotto.yml` to be valid. Prints nothing and exits `0` when the file is clean, so it drops into a pre-commit hook or CI step. Exits `1` with the [diagnostic code](/docs/diagnostics-fatal#the-workspace-file) when the file is wrong, and `2` when there is no workspace file to check (an ambient `glotto.yml` is not a declaration and is never reported as one). - `--json` — emit the outcome as a JSON object. The exit code is unchanged. ```sh glotto workspace validate # silent + exit 0 when the file is clean glotto workspace validate --json # {"ok": false, "code": "GLOTTO_WORKSPACE_CONFIG_MISSING", …} ``` ##### glotto workspace init Write a `.glotto/workspace.json` in the current directory, which becomes the workspace root. The `config` path is derived rather than typed — from `--config` when you pass one, otherwise from the config found by walking up — and written relative to the root with `/` separators, so the committed file is portable. A config that does not exist, or that sits outside the workspace root, is refused rather than written for a later command to reject. `targets` is never written: it declares an output layout only you can choose. Add it by hand when you want one, and `glotto workspace validate` will check it. - `--config ` — bind to this config (relative to the current directory) instead of the discovered one. - `--force` — overwrite an existing `.glotto/workspace.json`. ```sh glotto workspace init --config api/glotto.yml # writes {"config": "api/glotto.yml"} glotto workspace init --force # overwrite an existing declaration ``` Note that `glotto init` deliberately does *not* write a workspace file — a fresh single-config project does not need one. Reach for `glotto workspace init` when a repository grows a second project, or when you want SDKs written somewhere other than `sdks//`. #### glotto schema > **Available in the published CLI** — `npm i -g @glotto/cli` Emit the JSON Schema for `glotto.yml` (the same schema core-config exports as `glottoConfigJsonSchema()`). With no flags it prints to stdout; reference it from your config with a `# yaml-language-server: $schema=…` header for editor completion and validation. - `--out ` — write the schema to a file (relative to the current directory) instead of stdout. ```sh glotto schema # print the schema to stdout glotto schema --out glotto.schema.json # write it, then reference it from glotto.yml: # # yaml-language-server: $schema=./glotto.schema.json ``` #### glotto generate > **Available in the published CLI** — `npm i -g @glotto/cli` Validate the config, then generate SDKs from the spec across enabled targets. Writes all files atomically: nothing is written if the run *fails*. A target an engine *refuses* is different — it is dropped, the remaining targets are written normally, and the command exits non-zero naming the target it could not build (see [Reserved model names](/docs/diagnostics-sdk-generation#reserved-model-names)). Each SDK's customer-owned `lib/` extension files are written only if absent, and managed files with local edits are preserved (or, with `--merge`, three-way-merged) unless `--force` is passed — see [Custom code](/docs/custom-code). - `--config ` — use this config instead of auto-discovery. - `--out ` — output directory, relative to the current directory (default `sdks`, resolved against the [workspace root](https://glotto.dev/docs/cli/#the-workspace-file) when a workspace file is in effect). Overrides any `targets` path a workspace file declares. - `--target ` — restrict to a target; repeatable. Omit to generate all configured targets. - `--version ` — stamp the generated SDKs with this version (`X.Y.Z` or `X.Y.Z-pre`). - `--dry-run` — print the files that would be generated without writing. - `--force` — overwrite managed files that have local edits. - `--merge` — three-way-merge managed files that have local edits with the regenerated output (conflicts are written with git conflict markers), keeping a pristine baseline under `.glotto/baseline/` (gitignore it). Without the flag, edited files are preserved untouched. - `--format` — run each language's native formatter over the generated source (`gofmt`, `rustfmt`, `dart format`, `swift-format`, `mix format`, `rubocop`, `php-cs-fixer`, `google-java-format`, `ktfmt`). A formatter that isn't installed degrades with a warning — that language is emitted unformatted; point `GLOTTO_` (e.g. `GLOTTO_RUSTFMT`) at a binary to override resolution. ```sh glotto generate # generate every configured target into ./sdks glotto generate --target typescript # one target only (repeat --target for more) glotto generate --out build/sdks --version 1.4.0 glotto generate --dry-run # preview the file list, write nothing ``` #### glotto mcp > **Available in the published CLI** — `npm i -g @glotto/cli` Work with the [MCP server](/docs/mcp-server) artifact. Three subcommands: - `glotto mcp generate` — emit **only** the MCP server from `glotto.yml`, without the SDKs or the docs site. Runs server-side, like `generate`, so it needs `glotto login`. - `--out ` — write the server here instead of the configured output directory. - `--config ` — use this `glotto.yml` instead of auto-discovery. - `--dry-run` — print the file list, write nothing. - `--force` — overwrite managed files that have local edits. - `glotto mcp serve` — run an emitted MCP server over stdio, so an MCP client can launch it directly. Local only: it spawns the artifact already on disk and generates nothing. - `--dir ` — the emitted server to run (default `sdks/mcp`). - Any remaining flags are forwarded to the server unchanged. - `glotto mcp annotate` — author tool descriptions into `glotto.yml`'s `mcp.operations` block. Runs server-side and needs `glotto login`. - `--tool ` — annotate one tool; repeatable. - `--config `, `--dry-run`, `--force` — as above. ```sh glotto mcp generate # emit just the MCP server (runs server-side) glotto mcp serve # run the emitted server over stdio glotto mcp serve --dir build/mcp # run one from a non-default directory glotto mcp annotate --tool listPets # author one tool's description into glotto.yml ``` #### glotto breaking-changes > **Available in the published CLI** — `npm i -g @glotto/cli` Classify what changed between your current spec and a baseline: which changes break a caller, which do not, and what each does to the generated SDK surface. With `--gate` it fails the run when a breaking change lands without a major version bump, which is what the CI workflow Glotto emits for your repository does on every pull request. The comparison builds two IRs through Glotto's spec pipeline and diffs them, so **it runs on Glotto's infrastructure** and needs `glotto login` or a `GLOTTO_API_TOKEN`. Both spec documents are read from your machine and sent with any configured companion spec documents. The published CLI accepts local file sources. URL, git, command and introspect sources are refused: fetch, check out or export those documents first, then point the config at local files. - `--against ` — the baseline spec document to compare against. A local file. - `--provider ` with `--repo ` and `--branch ` — fetch the baseline from a repository instead. `--spec-path ` overrides which file is fetched. - `--gate` — exit `1` when breaking changes were introduced without a major version bump. Also enabled by `settings.detect_breaking_changes` in your `glotto.yml`. - `--format json` — emit the classified report as JSON instead of text. - `--emit-workflow ` — print the CI workflow that runs this gate. `--against-ref ` is available in the first-party build only: it names a git object, which needs the spec loader that clones repositories. Use `--against` with a checked-out file, or `--provider`. The emitted workflows require a `GLOTTO_API_TOKEN`: configure an Actions secret on GitHub, a masked CI/CD variable on GitLab, a secured repository variable on Bitbucket, or a secret pipeline variable on Azure. The GitHub and Azure templates map that secret into the gate's environment. For a self-hosted control plane, follow the template's `GLOTTO_API_URL` instructions; the default is `https://api.glotto.dev`. ```sh glotto breaking-changes --against baseline.openapi.yaml glotto breaking-changes --against baseline.openapi.yaml --gate ``` #### glotto verify > **Available in the published CLI** — `npm i -g @glotto/cli` Verify committed SDK output against a fresh regeneration and write the [verification report](/docs/verification-report): per target, whether the committed files drifted from what your spec produces, whether any checksum-stamped managed file was hand-edited, and — when you feed it a check-results document — the real compile and contract status of each target. This is the gate step of the CI workflow Glotto emits for your repository. **It regenerates, so it runs on Glotto's infrastructure** and needs `glotto login` or a `GLOTTO_API_TOKEN`. The comparison itself is local: your committed files are read and diffed on your machine, and only the spec is sent. - `--against ` — the committed output tree to verify. `--against-default sdks` yields to a `.glotto/workspace.json` when one is in effect, and falls back to `sdks/`. - `--provider ` with `--repo ` and `--branch ` — verify the output committed on a branch, with no checkout. `--prefix ` names the subpath it lives under. - `--checks ` — fold in a check-results document produced by `verify-compile` / `verify-contract`, so the report carries real compile and contract statuses instead of "not run". - `--out ` — write `verification-report.json` and `verification-report.md` here. ```sh glotto verify --against-default sdks --out glotto-verification glotto verify --against-default sdks --checks glotto-verification/check-results.json --out glotto-verification ``` #### glotto verify-compile > **Available in the published CLI** — `npm i -g @glotto/cli` Compile every committed SDK **in place**, with that SDK's own toolchain, and write the check-results document `glotto verify` folds into its report. This is the first produce step of the CI workflow Glotto emits for your repository. **It runs entirely on your machine** and needs no Glotto account: it regenerates nothing and sends nothing. What it does need is your targets' toolchains installed in the job — it invokes them. Each target's command comes from `targets..verify.compile` in your `glotto.yml`, or a built-in default for the languages where compiling a committed SDK is unambiguous (TypeScript, React Native, Go, Python, Rust). A target with neither is skipped rather than guessed at, and the document records the omission. - `--against ` / `--against-default sdks` — the committed output tree to compile. - `--out ` — where to write `check-results.json`. ```sh glotto verify-compile --against-default sdks --out glotto-verification ``` #### glotto verify-contract > **Available in the published CLI** — `npm i -g @glotto/cli` Run the integration test suite each committed SDK ships — the self-contained suite Glotto emits beside the client, which boots an in-process mock and round-trips the generated code against it — and **merge** the contract statuses into the same check-results document `verify-compile` wrote. **It runs entirely on your machine**, like `verify-compile`, and needs each target's dependencies installed as well as its toolchain: the emitted suite imports the built SDK. - `--against ` / `--against-default sdks` — the committed output tree to test. - `--out ` — the directory holding the document to merge into (and to write). ```sh glotto verify-contract --against-default sdks --out glotto-verification ``` #### glotto verify-upload > **Available in the published CLI** — `npm i -g @glotto/cli` Publish a check-results document to the control-plane run for a commit, so the console's surface-health panel, the run history and the [verification attestation](/docs/verification-report) carry your real compile and contract statuses instead of "not run". This is the **dashboard** step of the CI workflow Glotto emits for you; leave it out and the report still reaches your pull request as a job summary and an artifact. It is pure HTTP: it reads the produced document and posts it. It runs no generation and needs no toolchain. - `--checks ` — the check-results document to publish. Required. - `--project ` — the control-plane project. Resolves `--project`, then `GLOTTO_PROJECT_ID`, then `hosted.project` in your `glotto.yml`. - `--commit ` — the commit the run belongs to. Read from the CI environment when omitted. `GLOTTO_API_TOKEN` authenticates the upload. **Verified provenance is optional and turn-key**: set `GLOTTO_CI_OIDC_TOKEN` to a pre-minted CI-run OIDC token (GitLab, Bitbucket, Azure Pipelines or self-hosted), or grant GitHub Actions `id-token: write` — the dashboard then marks the run *verified*, meaning it can prove which workflow produced the statuses, not merely that a valid token uploaded them. The token's audience must equal your API URL, which must match the server's `CI_OIDC_AUDIENCE` (default `https://api.glotto.dev`). ```sh glotto verify-upload --checks glotto-verification/check-results.json ``` #### glotto verify-attestation > **Available in the published CLI** — `npm i -g @glotto/cli` Verify a downloaded [verification attestation](/docs/verification-report) — the signed, self-contained DSSE envelope that binds one verification report to the run that produced it. This is the command a buyer's compliance reviewer runs: it turns a JSON file into a verdict without requiring anyone to construct DSSE pre-authentication bytes by hand. Exits `0` when the envelope is verified, `1` when it is not, and `2` on a usage or configuration error — every usage error is reported *before* any network call, so a mistake never costs a round trip and never looks like a failed verification. - `--key ` — a public key file: one SPKI PEM, or a bundle of several concatenated. **Repeatable.** Keys are matched to signatures by their own derived fingerprint, never by filename or the order you pass them. - `--offline` — never contact the API. With `--key` material covering every key the envelope names, verification is completely self-contained — which is the point of filing an attestation for years. - `--api-url ` — control-plane base URL (env `GLOTTO_API_URL`; default `https://api.glotto.dev`). Used only for the keys `--key` did not resolve. - `--allow-revoked` — report a signature made by a revoked key as a warning instead of a failure. - `--require-countersignature` — fail unless at least one verified **non-issuing** signature is present (see below). - `--countersign ` — co-sign this envelope with your own Ed25519 key. Verification runs **first**: an envelope that failed is never counter-signed. Your private key is never printed, logged, or uploaded — only the public half and the signature. - `--upload` — send the co-signature to the run the attestation itself names (needs `GLOTTO_API_TOKEN`). There is deliberately no `--project`/`--run` flag: the signature can only be filed against the run it describes. - `--json` — machine-readable verdict. ```sh # Verify against the key the API serves for this envelope glotto verify-attestation attestation.json # Fully offline, with a key you archived alongside the artifact glotto verify-attestation attestation.json --key glotto-attestation.pem --offline # Co-sign it with your own key, so the artifact no longer rests on Glotto's alone glotto verify-attestation attestation.json --countersign my-key.pem --upload ``` **What the verdict means.** The first signature is Glotto's; any that follow are second-party counter-signatures. A counter-signature that fails against a key you hold is **tampering** and fails the whole verification; one whose key you simply do not hold is reported and does not fail anything — holding fewer keys should not make an artifact look worse. If a key resolves as `revoked`, the command tells you both facts separately: the signature is genuine, *and* the key is not to be trusted. Under `--offline` no key carries a status at all, so the command says revocation was not checked rather than implying a clean bill of health. #### glotto publish > **Available in the published CLI** — `npm i -g @glotto/cli` Publish your generated SDKs to their language package registries — npm, PyPI, crates.io, Hex, Maven Central, NuGet, pub.dev and RubyGems. This is the single step of the release CI workflow Glotto emits for your repository, which runs when a release pull request merges. **It runs entirely on your machine, with your own registry credentials.** Nothing is sent to Glotto, and Glotto never holds a registry token — each publisher reads the credential its registry expects from the environment, exactly as you would publishing by hand. - `--all` — publish every configured target. - `--lang ` — publish one target. - `--dir ` — the committed output tree to publish from. - `--version ` — the release version. Read from the committed manifest when omitted. - `--dry-run` — print the plan without publishing. - `--emit-workflow ` — print the release CI workflow. ```sh glotto publish --all --dry-run glotto publish --all ``` #### glotto code-owners > **Available in the published CLI** — `npm i -g @glotto/cli` Apply your `glotto.yml`'s `code_owners` block to a repository's review configuration. This exists because review ownership is a file on only half the forges Glotto manages: GitHub and GitLab read the `CODEOWNERS` file `glotto generate` emits, so this command reports the path and writes nothing. Bitbucket and Azure Repos have no such file — their equivalents are default reviewers and a required-reviewers branch policy, both API configuration — so on those two this is the only way a `code_owners` block takes effect at all. **It runs on your machine** and talks to your forge, not to Glotto. - `--provider ` — required. - `--repo ` — `owner/name`, or `org/project/repo` for Azure Repos. Required. - `--host ` — self-managed GitLab or Azure DevOps host. - `--config ` — path to `glotto.yml`. Credentials come from `GLOTTO_VCS_TOKEN`, or `GITHUB_TOKEN` / `GITLAB_TOKEN` / `BITBUCKET_TOKEN` / `AZURE_DEVOPS_TOKEN`. ```sh glotto code-owners apply --provider bitbucket --repo acme/payments-sdks ``` #### glotto login > **Available in the published CLI** — `npm i -g @glotto/cli` Authenticate to the Glotto control plane via the device-authorization (RFC 8628) flow and store the resulting token under `~/.glotto/`. The published thin-client CLI requires this — `glotto generate` dispatches codegen to the control plane. - `--api-url ` — control-plane base URL (env `GLOTTO_API_URL`; default `https://api.glotto.dev`). - `--scope ` — OAuth scope to request; repeatable. - `--refresh` — renew the stored session instead of starting a new device flow. ```sh glotto login # device-flow login to api.glotto.dev glotto login --api-url https://api.example.com glotto login --refresh # renew the stored session in place ``` A session token is valid for 30 days. `--refresh` renews it without a browser round-trip: the old token is revoked and a replacement is stored, so a leaked copy stops working as soon as you renew. The renewal inherits the existing token's scopes (so `--refresh` cannot be combined with `--scope`), and it never falls back to an interactive flow. Renewals are capped at 90 days from the original login — past that, `--refresh` says so and you sign in again with `glotto login`. #### glotto logout > **Available in the published CLI** — `npm i -g @glotto/cli` Remove the stored control-plane credential for the resolved API URL, leaving other URLs untouched. Idempotent — exits `0` whether or not a credential was present. - `--api-url ` — control-plane base URL (env `GLOTTO_API_URL`; default `https://api.glotto.dev`). ```sh glotto logout glotto logout --api-url https://api.example.com ``` #### What Glotto also runs for you Most of the capabilities above are also delivered without your running anything. Glotto produces them on its own infrastructure or inside the CI workflow it emits for your repository, and puts the result where you already look — a pull request, a job summary, your dashboard. The command and the delivery are two routes to the same output, not two different features: - **[Verification report](/docs/verification-report)** — what `glotto verify` writes, also produced for your release pull request and your dashboard, with a [signed attestation](/docs/verification-report#as-a-signed-attestation) you can check offline using `glotto verify-attestation`. - **[Drift detection](/docs/drift-detection)** — the specific finding that committed SDK output no longer matches the spec it was generated from, including which files and why. - **[Breaking-change detection](/docs/breaking-changes)** — every pull request that changes your spec is classified against the last build, whether or not you run `glotto breaking-changes` yourself. - **[The release flow](/docs/multi-vcs-release)** — the release pull request that precedes the `glotto publish` step above. - **Migration** — converting an existing project into a `glotto.yml`, non-destructively: [from Stainless](/docs/migrate-from-stainless) (`glotto migrate stainless`), [from Fern](/docs/migrate-from-fern), or [one endpoint at a time](/docs/endpoint-migration). One capability is still delivery-only: the **Fern** conversion needs the spec pipeline, which runs server-side, so `glotto migrate fern` refuses by name in the published CLI rather than half-running. The boundary is which of Glotto's engines would have to ship to your machine, not which capabilities we are willing to offer. ### C# Source: https://glotto.dev/docs/csharp/ An idiomatic .NET client that takes the HttpClient you supply, with resource accessors, a CancellationToken on every method, and await foreach paging. The C# SDK is an idiomatic .NET client emitted from the same `GlottoIR` as every other target. It uses file-scoped namespaces, `using`-imported `System.*` types, and XML `///` docs generated from the operation prose in the spec. #### Quickstart ```bash dotnet add package Petstore ``` ```csharp using Petstore; var client = new Petstore.Client(token: Environment.GetEnvironmentVariable("PETSTORE_TOKEN")); // every method takes a CancellationToken var pet = await client.Pets.CreatePet(newPet, cancellationToken); // paging is an IAsyncEnumerable await foreach (var pet in client.Pets.ListPets()) { Console.WriteLine(pet.Name); } ``` #### Injectable HttpClient The `Client` takes an `HttpClient` you supply rather than constructing (and leaking) one per instance, so it slots into `IHttpClientFactory`, Polly resilience handlers, and test handlers the standard .NET way — no socket-exhaustion anti-pattern. #### Resource accessors Operations are PascalCase methods on resource accessors — `client.Pets.Get(id)`, `client.Pets.Photos.Add(...)` — and every method accepts a `CancellationToken` so calls honor cooperative cancellation. #### Typed errors Non-2xx responses throw a typed `ApiError` carrying the parsed error body; discriminated-union response bodies resolve to the right concrete type. See [Errors](/docs/errors). #### Pagination List methods return an async stream (`await foreach`) that walks every page, advancing the cursor for you. See [Pagination](/docs/pagination). #### Retries & backoff Transient failures (`5xx`, `429`, transport errors) retry with exponential backoff and jitter, configurable per client. See [Retries & timeouts](/docs/retries), [Streaming](/docs/streaming), and [Authentication](/docs/authentication). #### Unknown response fields Your API can add a response field without it being a breaking change — but a generated record has no member to put it in, and `System.Text.Json` discards what it does not recognize. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized. ```csharp var pet = await client.Pets.CreatePet(body); // A field your API started returning after this SDK was generated. if (pet.ExtraFields.TryGetValue("species", out var species)) { Console.WriteLine(species.GetString()); } // Re-encoding preserves it — a read-modify-write never silently drops it. var json = JsonSerializer.Serialize(pet); ``` `ExtraFields` is an `IReadOnlyDictionary`, so nested objects and arrays survive intact, and retention is recursive. Your existing construction still compiles: the retention storage is a record *body* member, not a positional parameter, so the record's constructor signature is unchanged. The retained fields are read-only by design. To *send* a field your spec doesn't model yet, use the per-call extra-body escape hatch rather than writing to the retained bag. #### Pages, request controls, and files Paginated methods also expose [manual pages](/docs/pagination#fetch-one-page), so you can inspect one response and request its successor explicitly. Request overrides apply to the initial call and to each requested next page; see [Retries & timeouts](/docs/retries) for precedence and cancellation. [Binary downloads](/docs/file-transfers) return an owned byte response with metadata, a bounded read helper, and incremental consumption. Close the response when you stop early. Your generated README demonstrates these operations with your API's names and the language's native calling conventions. ### Custom code Source: https://glotto.dev/docs/custom-code/ Add your own code to a generated SDK and keep it across regenerations — customer-owned extensions and patch preservation. Generated SDKs cover the API, but real projects need a little hand-written code too — a convenience wrapper, an extra helper, a tweak to a generated method. Glotto is built so your code **survives regeneration**. There are two mechanisms: 1. The **customer-owned extension directory** — `lib/` in most SDKs and `Sources//Custom/` in Swift. Shipped today. 2. **Patch preservation** — three-way merge of edits to *generated* files, in the release PR and (opt-in) locally. For anything you write from scratch, prefer that extension directory: it's where preservation is guaranteed. #### The customer-owned extension directory Every generated SDK has an extension directory that is yours. Most engines use **`lib/`**; Swift uses **`Sources//Custom/`**, inside its conventional SwiftPM target. The contract is enforced by `glotto generate` and drift detection the same way for every language: - **Customer-owned, write-if-absent.** Glotto seeds the directory once with an entry-point stub, then **never overwrites that customer-owned file**. Every managed file is regenerated normally, but your extension is left exactly as you left it. - **Excluded from drift.** Drift detection ignores customer-owned files on both sides, so an edit there never shows up as drift or blocks a release — the extension is not part of the "regenerate must match" contract. - **A stable import target.** The entry-point stub (TypeScript's `lib/index.ts`, Python's `lib/__init__.py`, Swift's `Sources//Custom/Custom.swift`, …) gives you a fixed place to export from, and its header comment tells you how code you add there reaches the SDK's surface in that language. **The half of the tree Glotto never writes twice** One TypeScript target, and one real generate run over it. The stub below is the file Glotto writes once and then never writes again; the method beside it is regenerated on every run, under the checksum that decides which of the two you are editing. Derived from `examples/custom-code-lib/inputs` — every byte below is sliced from that demo or from one real generate run over it. **Your glotto.yml — one TypeScript target, and not a word about custom code** (`examples/custom-code-lib/inputs/glotto.yml` `targets`) ```yaml targets: typescript: {} ``` **glotto generate** **What Glotto seeds ONCE — src/lib/index.ts, yours from then on** (`typescript/src/lib/index.ts` `whole file`) ```ts // Custom code — never overwritten by `glotto generate`. // // This file, and everything else under `lib/`, is yours. `glotto generate` // writes it once and then leaves it untouched on every subsequent run, and // drift detection ignores `lib/` entirely — so helpers, overrides, and // extensions you add here survive regeneration. Re-export them from this entry // point to keep a single import surface for your custom code. export {}; ``` sha256 `066b46389fdd95f09343610f9fdb02d97e8c50f27626cd164f919fe40f029ab8` **What Glotto owns — regenerated every run, under the checksum that guards your edits** (`typescript/src/resources/widgets.ts` `WidgetsResource.createWidget`) ```ts createWidget(params: NewWidget, options?: RequestOptions): Promise { return this.core.request('POST', '/widgets', { ...options, body: params, contentType: 'application/json' }); } ``` generated-checksum `405b8140007456df5543f12bcfa55e05b969d62b5cefcbe186d4f484ee1bd247` Put helpers, wrappers, and hand-authored code in the extension directory, and it is preserved on every regenerate. Anything you expose from the stub reaches the SDK's public surface according to that language's normal package conventions. > **Upgrading an existing Swift SDK:** if Glotto finds the former `lib/Custom.swift` and no file at > `Sources//Custom/Custom.swift`, the next `glotto generate` copies its contents exactly to > the SwiftPM target and retains the legacy file as a backup for you to remove after review. If both > files already exist, Glotto leaves both untouched and prints the paths to reconcile; it never > chooses one customer-owned copy over the other. > **Every language, on the SDK's surface:** the `lib/` machinery (write-if-absent + drift > exclusion) is engine-agnostic, every SDK language seeds an entry-point stub, and custom code > joins the SDK's public surface per language: > TypeScript and React Native re-export it as `lib`; Swift compiles `Custom/` as part of its module; > Java, Kotlin, Rust, C#, Dart, and Elixir compile `lib/` into the SDK artifact; Ruby loads it with > `require "glotto"`; and the Go, Python, and PHP stubs document the exact import path to use. #### Editing generated files Sometimes you need to change a **generated** file directly. Glotto's rule is simple: **it never silently overwrites your edits.** Every generated file carries a checksum comment over the bytes Glotto produced — the `generated-checksum` stamped under the emitted pane above is the real one for that file, read out of the run that produced it rather than written here. On the next regenerate, Glotto compares that checksum to the file's current contents. If they match, the file is pristine and is regenerated freely. If they differ, you've hand-edited it — so Glotto performs a **three-way merge** (your version against the old and new generator output) and, on any conflict, writes standard conflict markers and **surfaces them in the release pull request** for you to resolve, rather than clobbering your change. > **How it runs today:** the checksum marker ships on every generated SDK language, the > release-PR flow does the full three-way merge automatically, and locally `glotto generate` > preserves your edited file untouched — or truly merges it when you opt in with > [`--merge`](/docs/cli#glotto-generate). > Even the generated `README.md` is covered — its checksum rides in an HTML comment that's > invisible in the rendered README, so badges and prose you add survive regeneration. > The customer-owned extension remains the simplest home for hand-authored code — no merge to > think about at all. #### Which should I use? | You want to… | Use | | --- | --- | | Add new helpers, wrappers, or utilities | The customer-owned extension — always preserved | | Re-export or extend generated types | The customer-owned extension, importing generated types normally | | Change the behavior of a generated method | Edit the generated file (preserved; merged in the release PR or with `--merge`) — or wrap it in the extension | See [Drift detection](/docs/drift-detection) for how customer-owned files are excluded from the drift gate. The [CLI reference](/docs/cli) documents the commands that enforce this contract — [`glotto generate`](/docs/cli#glotto-generate) (whose `--force` overwrites managed files with local edits) and [drift detection](/docs/drift-detection). ### Dart Source: https://glotto.dev/docs/dart/ An idiomatic Dart package with fromJson/toJson models, Stream-based pagination and SSE, plus async token storage and connectivity-aware retries for Flutter. The Dart SDK is an idiomatic package emitted from the same `GlottoIR` as every other target. Operations under a declared resource hang off an accessor — `client.pets.listPets()` — while an operation you leave ungrouped stays a method on `Client`. Request/response shapes are Dart classes with `fromJson`/`toJson`; binary download responses expose byte streams and bounded byte reads. Manual pages expose typed items, full response metadata and explicit continuation. `RequestOptions` controls headers, deadlines, retries, extra parameters and cancellation. Binary operations return an owned `BinaryDownload` with bounded `readAll`, `pipe`, byte chunks, response metadata and `close`. Close the client when finished to release SDK-owned HTTP connections; injected clients remain caller-owned. See [pagination](/docs/pagination), [request retries and controls](/docs/retries), [streaming](/docs/streaming), and [file transfers](/docs/file-transfers). #### Quickstart ```bash dart pub add petstore ``` ```dart import 'package:petstore/petstore.dart'; final client = Client(token: ""); try { final result = await client.pets.createPet( PetCreate(name: "Biscuit", species: PetCreateSpecies.cat), ); } finally { client.close(); } ``` #### Typed models Each `GlottoIR` model becomes a Dart class with `fromJson`/`toJson`; operations decode and return the typed response and accept a typed request body. Discriminated unions resolve to the right variant. #### Typed errors Operations throw an `ApiError` carrying the parsed error body, with an `ApiErrorKind` enum so callers `switch (error.kind)` / compare `ApiErrorKind.notFound` rather than matching status codes. See [Errors](/docs/errors). #### Pagination Paginated list methods return a `Stream` that walks every page as you listen, advancing the cursor for you. See [Pagination](/docs/pagination). #### Retries & backoff Transient failures (`5xx`, `429`, transport errors) retry with exponential backoff and jitter, configurable per client. See [Retries & timeouts](/docs/retries). #### SSE streaming Server-sent-event endpoints return a `Stream` of typed events decoded from the `text/event-stream` framing. See [Streaming](/docs/streaming) and [Authentication](/docs/authentication). #### Unknown response fields Your API can add a response field without it being a breaking change — but a generated class has no field to put it in, and its `fromJson` factory reads only the keys it declares. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized. ```dart final pet = await client.pets.createPet(body); // A field your API started returning after this SDK was generated. final species = pet.extraFields['species']; // Re-encoding preserves it — a read-modify-write never silently drops it. final json = jsonEncode(pet.toJson()); ``` `extraFields` is a getter returning `Map`, so nested objects and lists survive intact, and retention is recursive. Your existing construction still compiles: the retention parameter is optional and named. The retained fields are read-only by design. To *send* a field your spec doesn't model yet, use the per-call extra-body escape hatch rather than writing to the retained bag. #### File uploads `multipart/form-data` operations build the multipart body from their fields for you; an `application/octet-stream` operation takes a positional `List body` sent raw with the right `Content-Type`. #### Secure token storage The client takes an optional `tokenStore` parameter. Supply one and the bearer token is read from it per request (falling back to the static `token`); on OAuth2 client-credentials APIs the token cache is also persisted through it, so a restarted app reuses a still-valid token instead of minting a new one. See [Authentication](/docs/authentication). Because Flutter is a mobile platform, the contract is asynchronous — the shape a platform-backed store can actually implement: ```dart abstract class TokenStore { Future getToken(); // OAuth2 client-credentials APIs also get: // Future setToken(String value); } ``` That means you can back it with [`flutter_secure_storage`](https://pub.dev/packages/flutter_secure_storage), which keeps the credential in the iOS Keychain or Android Keystore: ```dart class SecureTokenStore implements TokenStore { final FlutterSecureStorage storage; SecureTokenStore(this.storage); @override Future getToken() => storage.read(key: 'api_token'); } ``` ##### Generating the adapter instead Rather than writing that yourself, opt in and the SDK ships it: ```yaml targets: dart: secureStorage: true ``` Your client then carries a `FlutterSecureStorageTokenStore`, so wiring the platform store is one expression: ```dart const storage = FlutterSecureStorage(); final client = Client(tokenStore: FlutterSecureStorageTokenStore(read: storage.read)); ``` Pass `key:` to choose the storage entry (useful when an API has several bearer-style schemes, each with its own store). On OAuth2 client-credentials APIs the adapter also takes `write:` — pass `storage.write` — so the token cache is persisted. **No storage backend is bundled and no dependency is added.** The adapter takes the store's *methods*, not the store itself — Dart function types are structural, so `storage.read` fits whether it comes from `flutter_secure_storage`, an encrypted store of your own, or a test double. Your `pubspec.yaml` is unchanged either way, and leaving the flag off emits exactly the SDK you have today. #### Connectivity- and lifecycle-aware retries Two opt-in seams stop a mobile app burning its retry budget against a radio that is down or while it is backgrounded — the same pair the React Native, Kotlin-Android and Swift SDKs ship. Enable either, both, or neither: ```yaml targets: dart: connectivity: true # pause retries while the device is offline lifecycle: true # pause backoff while the app is backgrounded ``` A default Dart SDK is unchanged. With the flags on, the client takes two more optional parameters and consults them before **every** attempt — a known-offline state waits and re-checks rather than spending the request, and a backgrounded app pauses backoff **without consuming a retry attempt**: ```dart abstract class ConnectivityMonitor { Future isOnline(); } abstract class AppLifecycle { Future isForeground(); } ``` Both reads are asynchronous, because the Flutter APIs behind them are — `connectivity_plus` exposes `Future> checkConnectivity()`. A synchronous contract would be unimplementable by the standard package, the same way a synchronous `TokenStore` was for `flutter_secure_storage`. No package is bundled and nothing imports Flutter, so the SDK stays a plain Dart package. Adapters take a closure, so wiring a real app is one expression per seam: ```dart import 'package:connectivity_plus/connectivity_plus.dart'; final lifecycle = MutableAppLifecycle(); final client = Client( 'https://api.example.com', connectivity: CallbackConnectivityMonitor(() async => (await Connectivity().checkConnectivity()) .any((r) => r != ConnectivityResult.none)), lifecycle: lifecycle, ); ``` Flutter reports lifecycle by callback rather than by query, so `MutableAppLifecycle` is the shape a `WidgetsBindingObserver` pushes into: ```dart class _AppState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { lifecycle.setForeground(state == AppLifecycleState.resumed); } } ``` There is also a `CallbackAppLifecycle` if you have a lifecycle source you can poll instead. ### Deployment & single-tenant Source: https://glotto.dev/docs/deployment/ Run Glotto as the shared SaaS, or as a dedicated single-tenant cell in your own region/VNet running the same control-plane code. Glotto runs as a managed **SaaS** by default. For enterprises with data-residency or isolation requirements, Glotto also offers a **single-tenant cell**: a dedicated instance in a customer-specified region/VNet, running the **same control-plane code** as the SaaS — behind the same proprietary cloud (there is no free self-host distribution). #### How a cell is provisioned A cell is described by a `CellSpec` (region, VPC, cell name) and stamped out by a parameterized **Terraform module** (`templates/single-tenant-cell/`) driven through a `CellProvisioner` abstraction (`@glotto/core-provision`). The concrete IaC backend (Terraform/AWS) stays behind the interface — it's hermetically fake-tested in CI, while a live `terraform apply` is a documented, human-run step (never a CI dependency). The result: ingest a customer spec into the dedicated cell and deliver SDKs, just like the shared tier — with the cell isolated to the customer's account/region. ### Diagnostics configuration Source: https://glotto.dev/docs/diagnostics-config/ Tune SDK-readiness rules and resolve every glotto.yml validation diagnostic. #### Tuning diagnostics The ruleset above is the default. A `diagnostics` block in [`glotto.yml`](/docs/glotto-yml) tunes it without touching the rules engine — it is a pure post-pass over the findings: ```yaml # glotto.yml diagnostics: rules: no-error-response: error # promote a warning to a release-blocking error mutation-no-idempotency-key: off # suppress a rule entirely max_warnings: 10 # fail (exit 1) when warnings exceed this budget ``` - **`diagnostics.rules`** maps a rule id to a severity override — `off` suppresses the rule, while `warn` / `error` remap its severity. Promoting a `warning` to `error` makes it release-blocking; setting a rule to `off` drops its findings (and they no longer count toward `max_warnings`). An unknown rule id is almost certainly a typo, so it's reported as a non-fatal warning on stderr and otherwise ignored. - **`diagnostics.max_warnings`** is a release-gating threshold: after overrides are applied, the remaining warning count exceeding this number fails the gate — even with zero errors. Start loose and ratchet it toward `0` as you approach `1.0` (see [Release-gating guidance](/docs/diagnostics#release-gating-guidance)). Both keys are optional and additive — a config with no `diagnostics` block lints exactly as the defaults above. The valid rule ids are the ones enumerated on this page, plus two that tune a **config** diagnostic rather than a spec rule: - **`config-entry-matched-nothing`** — the severity of `GLOTTO_CONFIG_ENTRY_MATCHED_NOTHING` (below). `off` silences it, `error` makes it blocking. It's tunable because a keyed entry naming an operation your spec no longer carries is *legal by design* — a `glotto.yml` is meant to outlive a spec change — so a team deliberately carrying one can say so here instead of dropping `--strict`. - **`config-auth-scheme-inert`** — the severity of the two *inert* halves of `GLOTTO_CONFIG_AUTH_SCHEME_REFUSED` (below): a top-level `client_settings.auth.header_prefix` on an API whose security schemes cannot format one, and a `client_settings.auth.schemes` block on an API that declares a single security scheme. Both are the same kind of *legal by design* — a `glotto.yml` outliving a change to your spec's auth **shape** — so one off-switch covers them. It deliberately does **not** cover the third half, a `header_prefix` on an `apikey`, `basic` or `custom` scheme: no spec change makes that field applicable to those kinds, so there is nothing deliberate to declare. The two are **independent switches**: they say different things (*"my config outlives my spec's operations"* versus *"my config outlives my spec's auth shape"*), so silencing one leaves the other reporting. The remaining config warnings report a directive Glotto structurally declined, which has no legal-by-design reading, and aren't overridable. #### Config diagnostics The rules above lint your **OpenAPI spec**. Problems in your **`glotto.yml`** are reported by a separate gate that validates the config against the schema, and it runs first — an invalid config stops the pipeline before any spec rule is evaluated. `glotto generate` reports these on stderr. Unlike the spec rules, they are keyed by a stable **code** and carry a source position (`file:line:col`) into your `glotto.yml`: | Code | Severity | Meaning | | --- | --- | --- | | `GLOTTO_CONFIG_REQUIRED_FIELD_MISSING` | error | A required key is absent (e.g. `organization.name`). | | `GLOTTO_CONFIG_INVALID_TYPE` | error | A value has the wrong type (e.g. a string where an object is expected). | | `GLOTTO_CONFIG_INVALID_ENUM_VALUE` | error | A value is outside the allowed set (e.g. `react_native.secure_storage`, `mcp.modes`). | | `GLOTTO_CONFIG_INVALID_VALUE` | error | A value fails a field-specific rule (e.g. `max_delay` < `initial_delay`; an operationId in both `skip` and `only`, or in both `exclude` and `only`). | | `GLOTTO_CONFIG_EXCLUDE_ONLY_CONTRADICTION` | error | An [`exclude`](/docs/glotto-yml-api-surface#exclude) entry and an [`only`](/docs/glotto-yml-api-surface#skip--only) key address the **same** operation using the two different spellings `exclude` accepts — e.g. `exclude: ["post /pets"]` alongside `only: { createPet: … }`. Contradictory intent, exactly as naming it the same way in both is: `only` restricts the operation to the listed targets, `exclude` withholds it from every artifact. `exclude` is applied first, so the operation is withheld everywhere and the `only` entry has no effect. Reported at generate time, where the spec is available to resolve the positional spelling. | | `GLOTTO_CONFIG_INPUT_SOURCE` | error | Not exactly one of `openapi` / `asyncapi` / `graphql` is present. | | `GLOTTO_CONFIG_OPENAPI_SOURCE_NOT_FOUND` | error | A local `openapi.source` path doesn't resolve on disk. | | `GLOTTO_CONFIG_OPENAPI_COMMAND_OUTPUT_PATH` | error | A `command`-source file output is absolute or escapes its working directory. Reported for a `command` source at **any** input site — `openapi.source`, `asyncapi.source`, `graphql.source`, `graphql.operations`, and each `docs.versions[]` snapshot — with the message naming the offending path; the code keeps its historical `OPENAPI` spelling so a config matching on it keeps working. | | `GLOTTO_CONFIG_INTROSPECT_SOURCE_OPENAPI_ONLY` | error | An [`introspect`](/docs/glotto-yml#input-source--exactly-one-of-openapi--asyncapi--graphql) source was written somewhere other than `openapi.source`. Introspection reads a framework's source and synthesizes an **OpenAPI** document, so it cannot be what an `asyncapi` or `graphql` key means. Point the key at the document it describes, or move the `introspect` source to `openapi.source`. | | `GLOTTO_CONFIG_DEFAULT_ENVIRONMENT` | error | [`default_environment`](/docs/glotto-yml#default_environment) names an environment that isn't declared under [`environments`](/docs/glotto-yml#environments). The message lists the names you did declare. This is an error rather than a fall-back-and-warn because there is no fallback that keeps what you meant: resolving it any other way would point your SDK, your docs playground and your MCP server at a host you wrote the key specifically to avoid. Fix the spelling, or add the environment. | | `GLOTTO_CONFIG_DEFAULT_ENVIRONMENT_INFERRED` | warning | Your config declares two or more [`environments`](/docs/glotto-yml#environments), none of them named `production`, and no [`default_environment`](/docs/glotto-yml#default_environment) — so Glotto used the **first by sorted name**, and the message says which. That rule exists so generated code never depends on the order you typed your YAML in; it is deliberately not a guess at which host you meant. Set `default_environment` to the one your SDKs, their README quickstarts, your docs playground and your MCP server should all point at. You will not see this for a single environment (nothing to choose between) or when one is named `production` (that one wins). | | `GLOTTO_CONFIG_DOCS_VERSIONS` | error | A `docs.versions` entry has a non-route-safe slug, a duplicate slug, more than one `default: true`, or declares other than exactly one snapshot source (`openapi` / `asyncapi` / `graphql`). A slug is also rejected when the URL layer would rewrite it — the generated layout finds the current version by comparing the slug against a percent-encoded pathname segment, so a slug containing a space, a `%`, or a non-ASCII character never matches its own pages and every page under it silently falls back to the default version. Characters a URL path keeps verbatim (letters, digits, and `-._~!$&'()*+,;=:@`) are fine. | | `GLOTTO_CONFIG_DOCS_REDIRECTS` | warning | A `docs.redirects` entry the generator will refuse to emit: an empty or non-string `from`/`to`, a `from` that is not site-absolute (it must start with `/`), a `from` or `to` containing whitespace (the emitted `_redirects` rule file is whitespace-delimited, so a host would read only the fragment before the space — percent-encode it), a `from` equal to its own `to` (a no-op loop), or a `from` an earlier entry already claims (the later one is unreachable). Also raised when your `docs.deploy.target` names a host whose own redirect config cannot express a `from` exactly — only a **trailing** `*` has an exact equivalent on Vercel, so an interior or repeated one is left out of the emitted `vercel.json` rather than rewritten into a rule that would match different URLs than you wrote. **Only that entry is dropped** — your other redirects still emit. | | `GLOTTO_CONFIG_DOCS_STRUCTURED_DATA` | warning | A customer-authored [`docs.structured_data.extra_nodes`](/docs/glotto-yml-project-settings#docs) / `extra_nodes_by_route` JSON-LD node the generator will refuse to carry: one that isn’t a JSON object, is an empty object, has no `@type` (a consumer dispatches on the type, so an untyped node is read by nothing), has a `@type` that isn’t a non-empty string — nor a non-empty array of them, JSON-LD’s multiple-type form — has an `@id` that isn’t a non-empty string or a `@context` that is neither a non-empty string nor an object, carries a nested `@graph` (that makes it a *document* rather than a node, and its members would be read as siblings of the derived ones), or exactly duplicates an earlier node in the same list. **Only that node is dropped** — your other nodes, and every node Glotto derives from your spec, still emit. This is a **structural** check: a misspelled `@type`, or a property that doesn’t belong to the type it sits on, is not validated here. | | `GLOTTO_CONFIG_DOCS_OG_TEMPLATE` | error | A [`docs.og_images.template.colors`](/docs/glotto-yml-project-settings#docs) value isn't a colour the OG-card renderer can paint. Cards are rasterized by `pureimage` during your site's `astro build`, not by a browser, so the accepted forms are `#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`, `rgb(r,g,b)` or `rgba(r,g,b,a)` with **comma-separated** numbers, and CSS named colours (`rebeccapurple`). `hsl()`, `color()`, gradients, `var(--x)`, and the space-separated `rgb(1 2 3)` form are not supported — they either fail your build or paint a colour you didn't ask for. | | `GLOTTO_CONFIG_DOCS_DEPLOY_DOMAIN` | error | [`docs.deploy.custom_domain`](/docs/glotto-yml-project-settings#docs) isn't a bare DNS hostname. It becomes the authority of your docs site's canonical URLs *and* the domain handed to your deploy provider's binding, so write the hostname alone — `docs.acme.com`, not `https://docs.acme.com`, and no path, port, wildcard, or trailing dot. Labels are letters, digits, and hyphens, at most 63 characters each and 253 overall. Internationalized domains are supported in their punycode form (`xn--caf-dma.example`). | | `GLOTTO_CONFIG_MCP_PACKAGE_NAME` | error | [`mcp.package_name`](/docs/glotto-yml-project-settings#mcp) isn't a valid npm package name. It becomes the generated `package.json` `"name"` and — with any `@scope/` stripped — the `bin` command name, so it must be at most 214 characters, lowercase, and made of letters, digits, and `-._~`, optionally with a single `@scope/` prefix whose scope and name are each non-empty. Leave it unset to derive `-mcp` instead. | | `GLOTTO_CONFIG_CODE_OWNERS` | error | A [`code_owners`](/docs/glotto-yml-project-settings#code_owners) rule the emission would have to guess about: a block that is declared but names no rules, a blank pattern, a rule with an empty owner list, an empty owner string, or an owner the forge your target's repo lives on could not resolve. The message names the offending provider and the spellings it accepts — GitHub takes `@login`, `@org/team-slug` (exactly two segments) or an email address; GitLab additionally allows `_` and `.` inside a segment and nests `@group/subgroup/…` without limit; Bitbucket and Azure Repos resolve an owner against your workspace directory, so anything non-empty is accepted here and checked when you apply it. This is an error rather than a dropped-and-warn because a CODEOWNERS line the forge cannot resolve is **ignored in full** — the path is left unowned, the file still exists, and nothing anywhere says so. | | `GLOTTO_CONFIG_CODE_OWNERS_NOT_APPLIED` | warning | You declared [`code_owners`](/docs/glotto-yml-project-settings#code_owners), and this target does not receive a CODEOWNERS file. Two causes, both about the forge rather than your config. **Bitbucket and Azure Repos read no such file** — their equivalents are default reviewers and a required-reviewers branch policy, so Glotto applies the block through their APIs instead of emitting a file. **A target with a `repo_path`** lives in a subtree of a shared repo, and GitHub and GitLab read CODEOWNERS only from the repository **root** — a file emitted into the subtree would never be opened, so none is emitted; declare the ownership in the CODEOWNERS at that repository's root instead. Reported by `glotto generate`. | | `GLOTTO_CONFIG_DOCS_AUTH` | error | A [`docs.auth`](/docs/glotto-yml-project-settings#docs) block omits a field its tier requires — `password` needs `secret_env`, `sso` needs both `provider` and `login_url` — or sets a non-positive `session_hours`. Supply the missing field (or drop the block to leave the docs site public). | | `GLOTTO_CONFIG_DOCS_AUDIENCES` | error | A `docs.audiences` block names an audience in `scopes` or `default` that isn't declared in `members`, or its `members` list is empty or has duplicates. Declare every audience in `members` first, and make `default` one of them. | | `GLOTTO_CONFIG_DOCS_PERSONALIZATION` | error | A `docs.personalization` block has an empty `fields` allow-list, or a `prefill_key_field` that isn't one of `fields`. Only fields on the allow-list are ever rendered into a page, so the key field has to be among them. | | `GLOTTO_CONFIG_DOCS_I18N` | error | A `docs.i18n` block has a locale token that isn't route-safe, a `locales` list that is empty or has duplicates, or a `locales` list that omits `default_locale`. Every locale becomes a URL segment, and the default has to be one of the locales you build. | | `GLOTTO_CONFIG_CUSTOM_CASINGS` | error | A [`custom_casings`](/docs/glotto-yml-client-behavior#custom_casings) key is not a lowercase alphanumeric word (identifier words are lowercased before matching, so `API: API` could never match — write `api: API`), or its rendering is not a pure re-casing of the key. Only letter case may differ; to change the word itself use `naming` (model members) or `parameter_naming` (method parameters). | | `GLOTTO_CONFIG_RENAME_NOT_APPLIED` | warning | A [`naming`](/docs/glotto-yml-client-behavior#naming--parameter_naming) member-rename targets an engine that doesn't apply member renames — TypeScript and React Native (their DTOs are transparent, so the interface *is* the wire shape) or Python in the default non-pydantic mode — or an `enums` nominal-shaping directive targets an engine with no type-alias construct. The directive is accepted but the emitted member keeps its wire name. Drop it for that target, or use `parameter_naming`, which those engines do honor. | | `GLOTTO_CONFIG_NAMESPACE_NOT_APPLIED` | warning | A [`targets..namespace`](/docs/glotto-yml#targets) is set on one of the four SDKs whose **published package name *is* its code identity** — TypeScript and React Native (the npm package is the module you import), Dart (the pub package is the library you import), and Swift (the SwiftPM package is the module you import). There is no second name underneath it for `namespace` to set, so the key has no effect and the SDK keeps the identity `package_name` gives it. Set [`package_name`](/docs/glotto-yml#targets) for that target instead — for these four it *is* the code identity, and it also stays the name you publish under. `namespace` is honored for `csharp`, `elixir`, `go`, `java`, `kotlin`, `php`, `python`, `ruby`, and `rust`, where the two identities are genuinely separate (the Python import package vs. the PyPI distribution name; the Rust `[lib] name` vs. Cargo's `[package] name`). | | `GLOTTO_CONFIG_CASING_NOT_APPLIED` | warning | `custom_casings` is applied to model members, method parameters, and enum constants, but **not yet** to method names, class/type names, or resource accessors — those keep their default casing. Informational: no config change resolves it, and the warning clears when the remaining surfaces land. | | `GLOTTO_SPEC_UNION_DEGRADED` | warning | A discriminated union won't generate as a narrowed union (ADR-0041 eligibility); the SDK falls back to an open type. | | `GLOTTO_CONFIG_MODEL_SHAPING_REFUSED` | warning | A [`models`](/docs/glotto-yml-model-shaping#models) directive was declined — a rename onto a name another model holds, or an `inline: true` on a self-referencing model or a union member. The model stays as it was. | | `GLOTTO_CONFIG_AUTH_SCHEME_REFUSED` | warning | A [`client_settings.auth`](/docs/glotto-yml-client-behavior#client_settings) directive was declined. Either a `header_prefix` was set on a scheme whose kind cannot carry one (`apikey`, `basic`, `custom` — only `bearer` and `oauth2` format an `Authorization` value), or a top-level `header_prefix` was set on an API whose schemes are all of those kinds so it formats nothing, or a per-scheme `schemes:` block was written for an API that has a **single** security scheme, which is configured through `client_settings.auth` directly. A scheme name your spec no longer defines is *not* reported — a `glotto.yml` outlives a spec change. The last two are tunable with [`diagnostics.rules.config-auth-scheme-inert`](https://glotto.dev/docs/diagnostics-config/#tuning-diagnostics); the kind mismatch is not, because no spec change makes that field applicable. | | `GLOTTO_CONFIG_MODEL_RENAMED` | warning | One of your models emits under a **different type name for one target**, because the name it would otherwise take is one that target's own generated SDK source refers to — a schema named `Data` in a Swift SDK would land in the same module as the client's own `Data` references and take them over, and Swift has no import-qualification escape for a same-module declaration. The generated name appends the target's model suffix, and adds a number if that is taken too: `Data` → `DataModel` → `DataModel2`. Nothing is missing from the output and every reference to the model follows the new name; **only the named target is renamed**, so your other SDKs are unaffected. To pick the name yourself, set the key the warning opens with — a [`name.`](/docs/glotto-yml-model-shaping#models) entry under `models`. An explicit name is always honored and this warning stops. The key it names is the one **you** wrote, even when another `models` entry has already renamed that model, so it is always one your `glotto.yml` can carry. | | `GLOTTO_CONFIG_SOFT_REQUIRED_SPLIT` | warning | A [`soft_required`](/docs/glotto-yml-model-shaping#soft_required) `body_fields` promotion generated a `Request` variant, because the operation's request body is a model your API also **returns** — demanding the field on it would make it mandatory when decoding a response too. The shared model is unchanged; only this operation's body uses the variant. | | `GLOTTO_CONFIG_AUTO_POPULATE_REFUSED` | warning | An [`auto_populate`](/docs/glotto-yml-model-shaping#auto_populate) position was declined — its schema doesn't permit exactly one value, its one value has no sendable wire form, or the request body isn't a JSON object. The input stays in the method signature. All four parameter locations (`path`, `query`, `header`, `cookie`) are supported. | | `GLOTTO_CONFIG_DUAL_MODE_REFUSED` | warning | A [`streaming.dual_mode`](/docs/streaming#dual-mode-endpoints-stream-true) entry was declined, so the endpoint was **not** split into buffered and streaming methods — the operation isn't in your spec, the spec never marks it as streaming, it takes no JSON-object request body, `param_discriminator` names a field that body doesn't have, `stream_event_model` names a model your spec doesn't define, the per-event type couldn't be resolved (name it with `stream_event_model`), or the variant's method name is already taken by a real operation (pick a different `method_suffix`). The operation keeps the single method it had. | | `GLOTTO_CONFIG_ENTRY_MATCHED_NOTHING` | warning | An entry in one of the six operationId-keyed blocks — `skip`, `only`, `exclude`, `parameter_naming`, `client_methods`, `aliases` — names an operation your spec doesn't carry, so it had no effect. Usually a typo; sometimes an operation renamed upstream since the config was written. The entry is still ignored rather than rejected (a `glotto.yml` outlives a spec change), so this is a warning you can tune or silence with [`diagnostics.rules.config-entry-matched-nothing`](https://glotto.dev/docs/diagnostics-config/#tuning-diagnostics). | | `GLOTTO_CONFIG_POSITIONAL_PARAMS_REFUSED` | warning | A [`positional_params`](/docs/glotto-yml-client-behavior#positional_params) entry was declined — it names a parameter the operation doesn't have, lists one twice, or places the `$body` token where it can't be honored (the operation has no body, the body isn't positional, taking it positionally would drag query parameters along, an optional body would precede a path parameter, or the token collides with a reserved name). The order is refused **whole**, so the method keeps its derived argument order — a partly-applied order would be a signature you never reviewed. | | `GLOTTO_CONFIG_RESOURCE_METHOD_NAME` | error | A [`resources..methods.`](/docs/glotto-yml#resources) declaration of the emitted method name could not be honored, so `glotto generate` stops. Either the endpoint beside it **matches no operation** in your spec (the verb and path must match exactly, placeholder names included — when exactly one operation serves the same route under a *different placeholder name*, `get /pets/{id}` against `paths: /pets/{petId}`, the message names the spelling your spec uses, which is usually a one-character fix), or the name it declares is **already emitted by another operation on the same resource** — in which case both operations are named and neither is renamed. Not suppressible: the alternative to stopping is shipping an SDK whose method names are not the ones you asked for, and a refused declaration would otherwise look exactly like one you never wrote. | | `GLOTTO_SPEC_README_UNKNOWN_OPERATION` | warning | A [`readme.example_requests`](/docs/glotto-yml-project-settings#readme) slot names an operation that is not an `operationId` in your spec. The README example falls back to the default selection. Usually a typo, or an operation renamed upstream. | | `GLOTTO_SPEC_README_UNKNOWN_PARAM` | warning | A `readme.example_requests..params` key matches no wire parameter of the operation it names — neither a path/query parameter nor a request-body field. The key is omitted from the rendered example. | | `GLOTTO_CONFIG_UNKNOWN_TARGET` | error | A [`targets`](/docs/glotto-yml#targets) entry names no codegen engine, so `glotto generate` would refuse the config. The message lists the engines that exist. The usual cause is an emitted artifact that is **not** a target: `mcp`, `docs` and `mock` are configured by a top-level block of that name — see [the MCP server guide](/docs/mcp-server#generating-the-server) — and the message says so for those three. Publishing is a different surface from the codegen `targets:` map: Glotto publishes the generated MCP server to npm for you, and that surface does accept `mcp` where `targets:` does not. | | `GLOTTO_SPEC_README_SLOT_KIND_MISMATCH` | warning | A `readme.example_requests` pagination or streaming slot names an operation of the wrong kind (a non-paginated operation in the pagination slot, a non-streaming one in the streaming slot). That example falls back to the default selection. | A config `error` blocks generation; the warnings don't, but a strict gate turns them (and any warning) into a non-zero exit — as does setting `config-entry-matched-nothing: error`, the one severity you can raise on its own. Every warning below `GLOTTO_SPEC_UNION_DEGRADED` is computed from your spec rather than from the config text, so they name the config path (`models.Address.name`, `soft_required.updateInvoice.body_fields.note`, `parameter_naming.createPett`) instead of a `file:line:col` position, and `glotto generate` prints all of them on stderr as well — you don't need a separate `validate` run to see a directive that didn't take. See the [`glotto.yml` reference](/docs/glotto-yml) for the configuration surface itself. ### Fatal diagnostics Source: https://glotto.dev/docs/diagnostics-fatal/ Fatal configuration, workspace, spec-ingestion, source-retrieval, and transform diagnostics. #### Fatal errors The diagnostics above are *reported* — collected, sorted, and printed, with generation continuing unless a config `error` blocks it. The codes below are different: they are **thrown**, so the command stops at the first one and exits non-zero. They carry no severity because there is only one — fatal. ##### Configuration loading Raised before validation, when the config file itself cannot be read. | Code | Meaning | | --- | --- | | `GLOTTO_CONFIG_NOT_FOUND` | No `glotto.yml` (or `glotto.yaml`) exists at the resolved path. Run the command from the directory holding your config, or point at it with `--config ./path/to/glotto.yml`. | | `GLOTTO_CONFIG_YAML_PARSE` | The config file is not well-formed YAML. The error carries the `line` and `column` of the parse failure — a tab used for indentation and an unquoted `:` inside a value are the two usual causes. | ##### The workspace file Raised while reading a [`.glotto/workspace.json`](/docs/cli#the-workspace-file) found by walking up from the current directory. A broken declaration is always reported — never quietly skipped in favour of some further-up project, which would run the command against an API you did not name. | Code | Meaning | | --- | --- | | `GLOTTO_WORKSPACE_INVALID` | The workspace file is not well-formed JSON, or does not match its schema — a missing `config`, a non-string path, or an unknown key. Unknown keys are rejected rather than ignored, so a typo cannot silently read as configured-and-working. | | `GLOTTO_WORKSPACE_KEY_OWNED_ELSEWHERE` | The file sets `project` or `openapi_spec`. Neither is a workspace key in Glotto: `glotto.yml` already owns those values as `hosted.project` and `openapi.source`. The message names the key to move the value to. Most often seen when translating a Stainless `.stainless/workspace.json` by hand. | | `GLOTTO_WORKSPACE_PATH_ESCAPE` | A declared `config` or `targets.` path is absolute, or escapes the workspace root with `..`. Declared paths are workspace-root-relative and must stay inside it. | | `GLOTTO_WORKSPACE_CONFIG_MISSING` | The workspace file's `config` names a file that does not exist. Both the declared path and the resolved absolute path are named. | ##### Editor-only notes Surfaced by the Glotto language server in your editor. These never affect a CLI exit code. | Code | Meaning | | --- | --- | | `GLOTTO_WORKSPACE_FOREIGN_CONFIG` | Information. The `glotto.yml` you have open is not the config declared by the `.glotto/workspace.json` above it — commands run from that workspace root will act on a different project. Add a workspace file beside this config to bind its own directory. Not a warning: the file still generates correctly. | ##### Spec parsing and ingestion Raised while reading your API description, before any IR is built. | Code | Meaning | | --- | --- | | `GLOTTO_SPEC_UNSUPPORTED_VERSION` | The document declares an OpenAPI version Glotto does not support; 3.0.x and 3.1.x are supported. Swagger 2.0 is detected and auto-converted, so this names a genuinely unsupported version rather than an old one. | | `GLOTTO_SPEC_PARSE` | The document could not be parsed. For OpenAPI the error carries the `line` and `column`; for GraphQL it carries the parser's own message verbatim. | | `GLOTTO_SPEC_INVALID` | The document parsed but is not usable: a required field is missing, an input handed to the GraphQL ingest is not a GraphQL document, or the document nests deeper than the supported limit (a guard against adversarially nested input). The message names which. | | `GLOTTO_SPEC_SWAGGER2_CONVERT` | A Swagger 2.0 document was detected but the in-process 2.0 → 3.0 conversion failed. Convert it to OpenAPI 3.x first — with `swagger2openapi`, or the editor.swagger.io "Convert to OpenAPI 3" command — and retry. | ##### Spec source retrieval Raised while **fetching** the spec, when [`openapi.source`](/docs/glotto-yml#input-source--exactly-one-of-openapi--asyncapi--graphql) is not a plain local file. Each names the source it failed on. | Code | Meaning | | --- | --- | | `GLOTTO_SPEC_SOURCE_NOT_FOUND` | The spec source file does not exist at the given path. | | `GLOTTO_SPEC_SOURCE_HTTP_STATUS` | A `url` source responded with a non-success HTTP status. The message names the final URL (after redirects) and the status. | | `GLOTTO_SPEC_SOURCE_RESPONSE_TOO_LARGE` | A `url` source served a document larger than the 16 MiB response ceiling. The read stops at the limit rather than truncating, so nothing partial is ever parsed — host a smaller document, or point the source at a local file. | | `GLOTTO_SPEC_SOURCE_AUTH_ENV_MISSING` | A source header references an environment variable that is not set in the running environment. Export it, or remove the header. | | `GLOTTO_SPEC_SOURCE_FORBIDDEN_ADDRESS` | A `url` source was refused **before any connection was made**: its scheme is not `http(s)`, or its hostname resolves to a private, loopback, link-local, unique-local, or unspecified address. Checked on the initial URL and on every redirect hop. Use a publicly resolvable URL, or a local file source. | | `GLOTTO_SPEC_SOURCE_GIT` | A `git` source operation failed. The message names the ref, the repository, and the underlying git error. | | `GLOTTO_SPEC_SOURCE_COMMAND_SPAWN` | A `command` source binary could not be started — typically not on `PATH`. The message names the command. | | `GLOTTO_SPEC_SOURCE_COMMAND_TIMEOUT` | The exporter command exceeded its wall-clock limit and was killed. | | `GLOTTO_SPEC_SOURCE_COMMAND_OUTPUT_LIMIT` | The exporter command produced more output than the cap allows and was killed. | | `GLOTTO_SPEC_SOURCE_COMMAND_EXIT` | The exporter command ran to completion but exited non-zero. The message carries the exit status and a bounded tail of its stderr. | | `GLOTTO_SPEC_SOURCE_COMMAND_OUTPUT_PATH` | A `command` source's file-output `path` is absolute or escapes its working directory via `..`. Refused without reading outside the working directory; use a relative path inside it. | | `GLOTTO_SPEC_SOURCE_COMMAND_NO_OUTPUT` | The exporter command succeeded but wrote no file at the declared output path. | | `GLOTTO_SPEC_SOURCE_INTROSPECT_PATH` | An `introspect` source `path` does not exist or is not a directory. Refused before any analysis runs. | ##### Transform engine Raised while applying the [`transforms`](/docs/transforms) block to your spec. Each error carries the `transform` name, its `transform_index` in your list, and the `target` it was resolving — so a failure in a long transform chain names the exact entry. | Code | Meaning | | --- | --- | | `GLOTTO_IR_TRANSFORM_TARGET_NOT_FOUND` | The transform's `target` matched nothing in the spec. Usually a typo or a schema renamed upstream. | | `GLOTTO_IR_TRANSFORM_TARGET_AMBIGUOUS` | The `target` matched more than one candidate, so the transform will not guess. Narrow it until it names exactly one. | | `GLOTTO_IR_TRANSFORM_INVALID_ARGUMENT` | An argument is not valid for that transform — the message names the argument and what it expected. | | `GLOTTO_IR_TRANSFORM_SPEC_DEPTH_EXCEEDED` | The transform's walker hit the nesting-depth cap while traversing the spec (the same guard as `GLOTTO_SPEC_INVALID`'s depth check, applied during transformation). | | `GLOTTO_IR_TRANSFORM_TARGET_INVALID_EXPRESSION` | An [`apply_overlay`](/docs/transforms#apply_overlay) action's JSONPath `target` is malformed or violates a rule the supported RFC 9535 subset refuses — for example, comparing a many-node query directly or using regex syntax outside I-Regexp. The message names the construct and its character offset. It is deliberately *not* reported as "matched nothing" — a target we cannot parse is a broken target, not a changed spec. | | `GLOTTO_IR_TRANSFORM_OVERLAY_SOURCE_UNREADABLE` | An [`apply_overlay`](/docs/transforms#apply_overlay) or [`merge_document`](/docs/transforms#merge_document) entry references a document (`source:`) that could not be loaded — a missing file, an unreachable or blocked URL, a git ref that will not clone. The message carries the underlying reason. Check the path is relative to your `glotto.yml`, and that a URL host is publicly reachable. | | `GLOTTO_IR_TRANSFORM_OVERLAY_DOCUMENT_INVALID` | A referenced overlay document loaded but is not an [Overlay Object](https://spec.openapis.org/overlay/latest.html) — unparseable YAML/JSON, a non-object root, or an `actions` that is missing, not an array, or empty. An empty overlay is treated as a broken reference rather than as "apply nothing", for the same reason a target matching nothing stops the build. | | `GLOTTO_IR_TRANSFORM_MERGE_DOCUMENT_INVALID` | A [`merge_document`](/docs/transforms#merge_document) entry's document loaded but is not usable — unparseable YAML/JSON, a root that is not an object, or an empty object. A merge document is a *partial OpenAPI document*, so any key is legal, but it must be an object and it must declare at least one key: an empty one is a broken reference rather than "merge nothing". | | `GLOTTO_IR_TRANSFORM_MERGE_DOCUMENT_INERT` | A [`merge_document`](/docs/transforms#merge_document) entry changed nothing — the spec already asserts every value the document declares, so the correction is dead. Usually this means your upstream spec has caught up with the override. Delete the entry, or point it at the document you meant. The check is per **entry**, not per key, so a document with one key that still bites is fine. | | `GLOTTO_IR_TRANSFORM_MERGE_DOCUMENT_STRICT_VIOLATION` | A [`merge_document`](/docs/transforms#merge_document) entry set `strict: true`, which asserts the document only *corrects* and never *adds* — and at least one of its leaves names a path your spec does not have. The message lists **every** offending path, not just the first, so one run clears one round of typos. Usually a misspelled key (`descriptoin`), which without `strict` would have been silently added as a new node. Fix the path, or drop `strict` if the entry is genuinely meant to extend the spec. | | `GLOTTO_IR_TRANSFORM_OVERLAY_EXTENDS_MISMATCH` | An overlay's `extends` names a different document than the URL your `openapi.source` points at, so its corrections were written for another API. The message carries both spellings. If the overlay does belong to this API, delete the optional `extends` field — Glotto always overlays the configured input source and never fetches `extends`; otherwise point `openapi.source` at the document the overlay was written for. Only checked when `openapi.source` is a URL: against a local file there is nothing comparable to check against, and the [verification report](/docs/verification-report) records that as `unverifiable` rather than implying a check that did not run. | | `GLOTTO_IR_ALLOF_CONFLICT` | IR construction could not merge an `allOf` composition. Either two members declare the same property with structurally different types (the message names the property and both types), two members disagree on `type`, or the `allOf` chain is cyclic (the message names the cycle). Glotto refuses rather than picking one, because which member won would depend on declaration order in a way nothing in your generated SDK would reveal. Reconcile the two declarations in your spec, or split the conflicting property into distinct names. | | `GLOTTO_CONFIG_ENUM_NAMING_ILLEGAL_IDENTIFIER` | An `enum_naming` entry in `glotto.yml` names an identifier that cannot compile in the language it is aimed at — it is not a legal identifier there, it is a reserved word in that language (`new` in Dart, Java, Kotlin, C#, PHP or Swift; a leading digit or punctuation anywhere), or it does not begin with an uppercase letter where that is what makes it a member at all — a Ruby name that does not start `[A-Z]` is a local variable rather than a constant, and a lowercase Go constant is unexported, so both compile or parse while declaring nothing your callers can reach. The message names the exact path so you can find it in your `glotto.yml`: `enum_naming...` when you pinned that language explicitly, and `enum_naming..` when you wrote one logical name — there is no per-language key in your file in that case, so changing the logical name is the fix. Glotto refuses rather than quietly adjusting the name, because an identifier you wrote explicitly is one your callers read: silently emitting `new_` would give your SDK a public constant you never chose. Pick an identifier that compiles in that language, or drop the per-language entry and let Glotto derive the name. The check is per target, so a spelling that is legal in Python and reserved in Dart is refused only for Dart. | | `GLOTTO_CONFIG_ENUM_NAMING_DUPLICATE_IDENTIFIER` | Two `enum_naming` entries for the same enum name the same identifier for the same target language, so the emitted enum would declare that member twice — `error[E0428]` in Rust, `CS0102` in C#, a duplicate `case` elsewhere. Each identifier is perfectly legal on its own, which is why this is a separate code from `GLOTTO_CONFIG_ENUM_NAMING_ILLEGAL_IDENTIFIER`: nothing here fails to compile in isolation. The message names the path of the second entry and the value that already holds the spelling. Glotto refuses rather than bumping one to `InProgress_`, because both names are ones you wrote and an explicit name is never auto-renamed. Pick a different identifier for one of them. The check is per target, so the same spelling for two different languages is not a collision. | | `GLOTTO_IR_UNSUPPORTED_SCHEMA` | IR construction structurally rejected a schema shape it cannot represent. Not a transform failure — it is raised by the IR build itself, and the message names the offending schema. OpenAPI 3.1 type arrays may contain one concrete type plus `null`; use an explicit `oneOf` or `anyOf` for multiple distinct non-null types. | ### Migration diagnostics Source: https://glotto.dev/docs/diagnostics-migrations/ Every diagnostic emitted while migrating from Stainless or Fern, with the required follow-up. #### Migration diagnostics — from Stainless [Migrating from Stainless](/docs/migrate-from-stainless) translates a `stainless.yml` into a `glotto.yml`. It is **non-destructive and never silently drops anything**: every key it could not carry over is reported, so the migration report is the checklist of what still needs a human. | Code | Severity | Meaning | | --- | --- | --- | | `GLOTTO_MIGRATE_STAINLESS_UNMAPPED` | warning | A key in your `stainless.yml` has no `glotto.yml` equivalent, so it was **dropped rather than guessed at**. The path names the key (nested paths are reported in full). Review each one against the [migration guide](/docs/migrate-from-stainless) — some are Stainless-specific and safely gone, others have a different Glotto mechanism worth re-expressing by hand. | | `GLOTTO_MIGRATE_STAINLESS_PLACEHOLDER` | warning | A **required** `glotto.yml` field could not be derived from the Stainless input, so the migrator wrote a placeholder value to keep the output well-formed. Replace every placeholder before running `glotto generate` — the config is not usable until you do. | Both are warnings rather than errors because the migration itself succeeded; they describe work remaining, not a failure. Together they are the first Glotto diagnostics most migrating teams see. #### Migration diagnostics — from Fern [Migrating from Fern](/docs/migrate-from-fern) translates a `fern/generators.yml` into a `glotto.yml`. Where the Stainless migrator's report is binary — carried over, or dropped — this one gives **every** input key one of four verdicts, because several Fern *knobs* are Glotto *guarantees*: reporting those as a loss would say you are giving up a property at the moment you are gaining a stronger version of it. | Code | Severity | Meaning | | --- | --- | --- | | `GLOTTO_MIGRATE_FERN_MAPPED` | info | The key was translated. The message names the `glotto.yml` key it became. | | `GLOTTO_MIGRATE_FERN_GUARANTEED` | info | **You are not losing this.** Glotto provides the property unconditionally, so there is nothing to configure — the message names the guarantee (open enums, unknown-field preservation, the emitted test suite). | | `GLOTTO_MIGRATE_FERN_NOT_HONORED` | info | A Fern *emission-shape* knob Glotto answers differently on purpose (`noSerdeLayer`, the `pydantic_config` block, `union: v0\|v1`, …). Honoring it would mean re-implementing Fern's emitter, so it is a deliberate non-goal rather than a gap. | | `GLOTTO_MIGRATE_FERN_UNMAPPED` | warning | Genuinely no `glotto.yml` home, reported at its precise dotted path. This is the list that needs your attention. | | `GLOTTO_MIGRATE_FERN_PLACEHOLDER` | warning | A **required** `glotto.yml` field your Fern config cannot supply (your organization details live in `fern.config.json`, your environments in the API definition). Replace every placeholder before running `glotto generate`. | | `GLOTTO_MIGRATE_FERN_IGNORE` | info | A `.fernignore` entry. Fern *freezes* a listed file; Glotto [three-way-merges](/docs/custom-code) instead, so the entry is reported with the construct that replaces it and no file is written. | | `GLOTTO_MIGRATE_FERN_NAME_UNPINNED` | warning | Under `--preserve-names`, an emitted member name that could not be pinned to the Fern spelling — a reserved word (pinning it would emit code that does not compile), an ambiguous acronym split, the client class name, or a transparent-DTO engine. Names **both** spellings so nothing is left silently divergent. | | `GLOTTO_MIGRATE_FERN_GROUP_AMBIGUOUS` | error | Your project declares several generator groups and no `default-group`, so there is nothing to migrate without a choice. Re-run with `--group `; no file is written. | | `GLOTTO_MIGRATE_FERN_DOCS_PLACEHOLDER` | warning | The docs half's sibling of `GLOTTO_MIGRATE_FERN_PLACEHOLDER`: a **required** field of the migrated `docs` block your `fern/docs.yml` cannot supply. Today that is a `docs.versions[]` spec snapshot — Fern's `versions[].path` names that version's *docs config*, not a spec — so each entry is written with a clearly-marked `TODO` source for you to replace. | | `GLOTTO_MIGRATE_FERN_DEFINITION_INPUT` | error | This is a Fern **Definition** project, not an OpenAPI one. Export the API first (`fern api export --openapi`) and re-run against the exported document. | The two `error` codes stop the migration and write no output — half-migrating either case would produce a config that validates but generates the wrong SDKs. ### Runtime diagnostics Source: https://glotto.dev/docs/diagnostics-runtime/ Diagnostics raised by generated SDKs at application runtime. #### Runtime diagnostics (raised by your generated SDK) One code is different from every other on this page: it is emitted **into** your SDK and raised by your own application at call time, not by `glotto` while generating. If you are looking for it in `glotto generate` output you will not find it there — it appears in your application's logs or as an exception at the call site. | Code | Meaning | | --- | --- | | `GLOTTO_RESTRICTED_HEADER` | You passed a per-call header whose name the SDK's HTTP transport controls, so the SDK cannot send your value. The message names the header. Rather than dropping it silently — leaving you to discover from a packet capture that it never went out — the SDK refuses the call before any request is sent, so it has no server-side effect. | **Which names are refused depends on the language**, because the transports genuinely differ and we would rather not take a capability away from one SDK to match another's limitation: | SDK | Refused | Sent normally | | --- | --- | --- | | Java, Kotlin (JVM) | `Content-Length`, `Host`, `Connection`, `Upgrade`, `Expect` | — | | Go | `Content-Length`, `Host` | `Connection`, `Upgrade`, `Expect` | | All other languages | — | all of the above | The Java and Kotlin list is `java.net.http`'s own restricted set. That JDK exposes a `jdk.httpclient.allowRestrictedHeaders` system property which can re-enable individual names; the generated SDK deliberately does **not** consult it, because an SDK whose accepted headers changed with a JVM flag would be harder to reason about than one that is consistently strict. **Setting one of these headers is usually unnecessary.** `Content-Length` is computed from the body you pass; `Host` follows from your configured base URL. If you need to reach a different host, change the client's base URL rather than the header. You can also see this code at **generation** time. If your spec declares one of these names as a required header parameter, or sets one as an `auto_populate` constant, generation refuses instead — emitting an SDK whose every call throws would not be something a caller could fix. ### SDK generation diagnostics Source: https://glotto.dev/docs/diagnostics-sdk-generation/ Fatal SDK-generation diagnostics, including reserved and colliding model names across every target. #### SDK generation Raised while a codegen engine emits an SDK, when the engine can tell in advance that the source it would write cannot compile. Generation stops rather than writing it, so the failure names your spec instead of the SDK's own runtime. **These stop the one target, not the whole run.** When an engine refuses, `glotto generate` drops that target and still writes every other one — a refused Swift SDK does not cost you your TypeScript and Python SDKs. The run exits non-zero and names the target it could not build, so a CI pipeline that asked for thirteen SDKs and received twelve fails rather than passing quietly. `glotto generate` reports these before anything is written, so you can see the problem without waiting for a build. ##### Reserved model names The `*_RESERVED_MODEL_NAME` codes below are one problem in thirteen languages: a schema whose type name collides with a type the generated SDK itself uses. Left alone, your model silently takes over the SDK's own references and the build fails with errors pointing at the SDK rather than at the collision — or, in Elixir, does not fail at all and quietly replaces your model. **Generation resolves this for you.** The affected SDK emits the model under a deterministic alternative — `Data` becomes `DataModel`, and a second collision becomes `DataModel2` — and generation reports every rename it applied. Only that language's type name changes; your other SDKs keep the original name. To choose the name yourself, pin it per target with `models: { Data: { name: { swift: MyName } } }`, which always wins over the automatic rename and leaves your other SDKs alone; a [`rename_schema` transform](/docs/transforms#rename_schema) renames it in *every* target instead. **You will only see one of these diagnostics if the rename cannot be applied** — it names every colliding schema and the identifier each one shadows. In practice that means one thing: you pinned the name yourself, with `models: { Data: { name: { swift: … } } }`, onto an identifier that language reserves. A pin is treated as a decision rather than a suggestion, so generation will not quietly substitute a different name for the one you asked for — it refuses that target and tells you. Choose another name, or remove the pin and let the automatic rename resolve it. Two things hold for all thirteen. Names that merely *look* built-in are not affected: each engine reserves only what its generated code actually names without qualification, which is why every *safe* column below has entries in it. And the **fixed** half of each reserved set — the language builtins and the SDK's own scaffolding — already covers every type that SDK **can** use, not only the ones your current endpoints reach, so turning on streaming, pagination or a new auth scheme cannot widen *that* half under a build that used to work. The **derived** half is where it can, and that is the next paragraph's whole subject. **One case cuts across every language, and it is a fact about your spec rather than about the engine.** Alongside the fixed vocabulary above, every SDK also declares names *derived* from your own IR keys — the shared baseline is one class or module per resource: an `orders` resource makes the Swift SDK declare `public final class OrdersResource`, and a nested `pets`/`photos` pair makes `PetsPhotosResource`. Because a derived name is a function of your own resource, operation and model keys rather than of the language, it can appear on a spec that **used to build** — unlike the fixed vocabulary, which a spec either always or never reaches. Adding a resource, turning on SSE for an operation you already had, or growing a discriminated union is what creates the collision, not anything Glotto changed between generations. The remedy is the same either way: rename the model, or rename the resource, operation, event or discriminator value that produced the name it collided with — and the diagnostic always names which one. Most engines add one or more families beyond that shared resource case, and they differ enough to be worth reading rather than assumed. Throughout — and in the table below — `` is the PascalCase join of a resource key path (a nested `pets`/`photos` gives `PetsPhotos`), `` a method key, `` a discriminated-union model key, `` one of that union's discriminator values, `` a key of an operation's `event_types` map, `` a declared OAuth2 scope, and `` an optional query parameter. - **Swift** and **Rust** add one `StreamEvent` enum per streaming operation with event types — so turning on SSE for an operation you already had is enough to create a collision that a previous generate certified absent. - **Dart** and **C#** add that same enum *and* one type per mapped event name, so a new key in an operation's `event_types` reserves a name of its own: a sealed class plus its variants in Dart, an `abstract record` plus its per-event variants in C#. - **TypeScript** and **React Native** derive the same five families — `Resource`, a `ResourceWithRawResponse` sibling wherever that resource has buffered operations, `StreamEvent`, one `` per discriminated-union member, and the `Unknown` catch-all. They also share a behavior worth knowing about even when the *build* stays green: TypeScript merges a same-named `interface` declaration into an existing one rather than rejecting it, so the compiler can accept the collision while your model's type silently means the SDK's instead. - **PHP** and **Ruby** add one `Request` class per operation whose request body is an inline JSON or multipart object. - **Kotlin** adds that same request-body family plus the stream-event pair (`StreamEvent` and one `StreamEvent` per mapped event) — and is worth knowing about even on a green build for a different reason: the Kotlin SDK emits one file per top-level type, so two declarations claiming one path means one of them never reaches disk, with no compiler error at all. - **Java** collects all four of Kotlin's families *and* the discriminated-union pair, because it puts every type in one package. Two of its failure modes are silent, and they are **not** the same: a collision with a class nested in the generated client compiles clean, because `javac` binds the SDK's own reference to its own type and your schema is simply never used; and, separately, where two files would claim one path, one is dropped before the compiler runs — which is Kotlin's case. - **Python**'s resource family is the largest of the thirteen: **four** names per node, where TypeScript's and React Native's are two and everyone else's is one. It emits a sync and an async accessor class, plus a `WithRawResponse` sibling for each where the node has at least one buffered method, so a single `orders` resource can reserve all four spellings at once. It adds a `Unknown` catch-all and no stream-event family at all — its SSE events are emitted as the model types your spec already carries, so there is no per-method union type to collide with. - **Elixir** has the resource case with **no suffix** (an `orders` resource emits `defmodule Glotto.Orders`, so the reserved identifier is a bare ``) plus the discriminated-union pair; see below for the failure it shares with Ruby, and the one thing only it has. - **Go** derives the widest set of the thirteen: the resource accessors, a stream-event trio, a discriminated-union trio, a per-operation option type with one constructor per optional query parameter, and one member per declared OAuth2 scope — all of them sharing the one package-level namespace with the fixed half, because a Go package has no module boundary to keep them apart. Its *fixed* half, by contrast, holds nothing from the language at all: Go's builtins and stdlib types are reached under a package qualifier or spelled in lower case, so a PascalCase schema name can never shadow one. Every row below follows one convention. **Collides with** lists the fixed vocabulary first (language and runtime builtins the emitted code references or declares, unaffected by your spec), then the SDK's own declared types (abbreviated to the handful worth recognizing, since several engines' full lists run past thirty), then **every** derived family that engine has, phrased as `one per in your spec`. The abbreviation applies to the first two lists and never to the third — and that is a checked property rather than an editorial promise: our own CI derives the third list by running each engine's derivation, and runs every identifier in the last two columns through the same resolver `glotto generate` calls, so a row that understated what its engine reserves fails our build rather than reaching you. Two diagnostics in this family are not about *our* names at all. `GLOTTO_REACT_NATIVE_COLLIDING_MODEL_NAMES` and `GLOTTO_TYPESCRIPT_COLLIDING_MODEL_NAMES` cover the case where two of **your own** schemas emit the same type name — `foo_bar` and `fooBar` both become `FooBar` — which would declare that type twice in one module. TypeScript merges two declarations of one name rather than rejecting them, so the usual outcome is a green build shipping a public type that is your two schemas folded together; only a mismatched pair (an interface beside a type alias) is a `Duplicate identifier` error. **You will normally never see either code**, because this is resolved the same way every other collision is: one of the schemas keeps the name and the rest are renamed with the `Model` suffix and a numeric tail — `FooBar`, `FooBarModel`, `FooBarModel2` — and every rename is announced when you generate. The schema that keeps the name is the first of the group in sorted order, which depends only on your own keys, so it does not change when you add or rename an unrelated schema. The code is emitted in one case: you pinned `models..name.` on the schema that would have been renamed. Renaming over an explicit pin would give you a type name you did not ask for, so Glotto refuses that target instead — every other target you configured still generates — and the message names the sibling that keeps the name. Pin the other schema instead, drop the pin and let the automatic rename run, or rename one of them everywhere with `rename_schema`. | Code | Language | Collides with | Safe despite appearances | | --- | --- | --- | --- | | `GLOTTO_SWIFT_RESERVED_MODEL_NAME` | Swift | `String`, `Int`, `Data`, `Codable`, `Sendable`, `Locale`, `UUID`, `Task`; the SDK's own `Client`, `APIError`, `RequestOptions`, `RawResult`; `BinaryResponse`, `DownloadResult`, `BinaryByteSource`, `TransportBinaryByteSource` and `URLSessionBinaryByteSource` when binary downloads are declared; SDKs with binary downloads or WebSockets also reserve `StreamingTransport`, `StreamingTransportConfigurationError`, `StreamingHTTPBody`, `StreamingHTTPResponse`, `StreamingWebSocket`, `StreamingWebSocketMessage`, `StreamingWebSocketResponse`, `NIOStreamingTransport`, `GlottoNIOAbort`, `GlottoNIOConnection`, `GlottoNIOHTTPHandler`, `GlottoNIOInflater`, `GlottoNIOOpening`, `GlottoNIOSocketAbortHandler`, `GlottoNIOUpgradeValidator`, `GlottoNIOWebSocketHandler`, `InboundIn` and `OutboundIn` across platforms; and one `Resource` per resource plus one `StreamEvent` per streaming operation in your spec | `Result`, `Error`, `Item`, `Pet` | | `GLOTTO_TYPESCRIPT_RESERVED_MODEL_NAME` | TypeScript | `Response`, `Headers`, `Promise`, `Record`, `Blob`, `WebSocket`; the SDK's own `Client`, `ApiError`, `TelemetryContext`, `RetryBudget`, `Tokenizer`; and one `Resource` per resource, a `ResourceWithRawResponse` per resource with buffered operations, one `StreamEvent` per streaming operation, one `` per discriminated-union member, and one `Unknown` catch-all per eligible union in your spec; `BinaryResponse` when your spec declares binary responses | `Error`, `Date`, `JSON`, `Math`, `Symbol` | | `GLOTTO_RUST_RESERVED_MODEL_NAME` | Rust | `Result`, `Option`, `String`, `Vec`, `Box`; the SDK's own `Client`, `ApiError`, `RequestOptions`, `RawResult`, `TelemetryContext`, `HttpClient`, `Rng`; `BinaryResponse` and `DownloadResult` when binary downloads are declared; and one `Resource` per resource plus one `StreamEvent` per streaming operation in your spec | `Error`, `Clone`, `Debug` | | `GLOTTO_DART_RESERVED_MODEL_NAME` | Dart | `Future`, `Object`, `List`, `Duration`, `Uri`, `Stream`, `StreamController`, `Completer`, `Iterable`; the SDK's own `Client`, `ApiError`, `RawResult`, `TelemetryContext`, `TokenStore`; `BinaryDownload`, `DownloadResult`, `JsonResult` and `FutureOr` when binary responses are declared; and one `Resource` per resource, one `StreamEvent` per streaming operation, and one `StreamEvent` per mapped event name in your spec | `Error` | | `GLOTTO_KOTLIN_RESERVED_MODEL_NAME` | Kotlin | `List`, `Map`, `Any`, `Unit`; the SDK's own `Client`, `Response`, `ApiError`, `RawResult`, `TokenStore`, `OAuthTokenResponse`, `TelemetryContext`; `BinaryExchange`, `BinaryResponse` and `DownloadResult` when binary responses are declared; and one `Resource` per resource, one `Request` per inline-object request body, one `StreamEvent` per SSE operation, one `StreamEvent` per mapped event name, one `` per discriminated-union member, and one `Unknown` catch-all per eligible union in your spec; plus, per documented non-2xx status in that spec, one typed-error class — the mapped name where the status has one (a documented 404 reserves `NotFoundError`) and `StatusError` where it does not | `Result`, `Error`, `Slot`, `READ`, `Set`, `Sequence` | | `GLOTTO_CSHARP_RESERVED_MODEL_NAME` | C# | `Task`, `Stream`, `Exception`, `Guid`, `Encoding`; the SDK's own `Client`, `ApiError`, `RequestOptions`, `Response`, `TelemetryContext`; and one `Resource` per resource, one `StreamEvent` per streaming operation, one `StreamEvent` per mapped event name, and one `` per discriminated-union member and one `Unknown` catch-all per eligible union, in your spec; plus, per documented non-2xx status in that spec, one typed-error class — the mapped name where the status has one (a documented 404 reserves `NotFoundError`) and `StatusError` where it does not; when your spec declares binary responses, `BinaryResponse`, `DownloadResult` and the emitted test helper `BinaryTransferPeer` | `String`, `Object`, `Enum` | | `GLOTTO_JAVA_RESERVED_MODEL_NAME` | Java | `String`, `Object`, `Integer`, `Thread`; the SDK's own `Client`, `Response`, `RequestOptions`, `TokenStore`, `ApiError` and the agent-primitive helpers; `BinaryExchange`, `BinaryPayload`, `BinaryResponse` and `DownloadResult` when binary responses are declared; and, from your own spec, one `Resource` per resource, a `Request` per inline request body, a `StreamEvent` per SSE operation and a `StreamEvent` per mapped event name, one `` record per discriminated-union value, and one `Unknown` catch-all per eligible union; plus, per documented non-2xx status in that spec, one typed-error class — the mapped name where the status has one (a documented 404 reserves `NotFoundError`) and `StatusError` where it does not | `List`, `Exception`, `Builder`, `Slot`, `Map`, `Optional`, `Record` | | `GLOTTO_GO_RESERVED_MODEL_NAME` | Go | `Client`, `APIError`, `Result`, `Option`, `WithBaseURL`; `BinaryResponse` and `DownloadResult` when binary downloads are declared; and, in the same package-level namespace, one `Resource` accessor per resource, a `StreamEvent` with its `StreamEventValue` marker and one `StreamEvent` per mapped event name, a `` and `Value` and `Unknown` per discriminated union, a `Option` with one `With` constructor per optional query parameter, and one `OAuth2Scope` per declared OAuth2 scope | `Context`, `Reader`, `Time`, `Error`, `Duration`, `Writer`, `Handler` | | `GLOTTO_PYTHON_RESERVED_MODEL_NAME` | Python | `Exception`, `Generic`, `Iterator`, `AsyncIterator`, `TypeVar`, `TypeAdapter`, `BaseModel`, `ConfigDict`, `Field`; the SDK's own `Client`, `AsyncClient`, `ApiError`, `RawResponse`, `RawResult`, `TelemetryContext`, `TokenStore`; and, per resource in your spec, all four of `Resource`, `AsyncResource`, `ResourceWithRawResponse` and `AsyncResourceWithRawResponse` (the last two only where the resource has a buffered method), plus one `Unknown` catch-all per eligible discriminated union; `BinaryResponse` and `AsyncBinaryResponse` when your spec declares binary responses | `Any`, `Enum`, `Callable`, `Protocol`, `Data`, `Result`, `Response`, `List`, `Dict`, `Optional` | | `GLOTTO_RUBY_RESERVED_MODEL_NAME` | Ruby | `Data`, `Hash`, `Array`, `String`, `Integer`, `Time`, `JSON`, `URI`; the SDK's own `Client`, `Response`, `ApiError`, `RawResult`, `TelemetryContext`, `OAuth2Scopes` and its typed-error, OAuth and agent-primitive classes; `BinaryDownload` when binary responses are declared; and one `Resource` per resource, one `Request` per inline-object request body, and one `` per discriminated-union member and one `Unknown` catch-all per eligible union, in your spec; plus, per documented non-2xx status in that spec, one typed-error class — the mapped name where the status has one (a documented 404 reserves `NotFoundError`) and `StatusError` where it does not | `Struct`, `Symbol`, `Regexp`, `READ`, `WRITE` | | `GLOTTO_PHP_RESERVED_MODEL_NAME` | PHP | `String`, `Int`, `Array`, `Object`, `Float`, `Bool`, `Mixed` (matched case-insensitively); the SDK's own `Client`, `ApiError`, `RequestOptions`, `RawResult`, `Response`, `TokenStore`; and one `Resource` per resource, one `Request` per inline-object request body, and one `` per discriminated-union member and one `Unknown` catch-all per eligible union, in your spec; plus, per documented non-2xx status in that spec, one typed-error class — the mapped name where the status has one (a documented 404 reserves `NotFoundError`) and `StatusError` where it does not; and `BinaryDownload` only when an operation declares a binary response | `Exception`, `Closure`, `Generator`, `Error` | | `GLOTTO_REACT_NATIVE_RESERVED_MODEL_NAME` | React Native | `Response`, `Headers`, `Promise`, `Record`, `URLSearchParams`, `WebSocket`; the SDK's own `Client`, `ApiError`, `TelemetryContext`, `RawResponse`, `Uploadable`, `SecureTokenStore`; and, from your own spec, one `Resource` per resource, a `ResourceWithRawResponse` per resource with buffered operations, one `` per discriminated-union member, one `Unknown` catch-all per eligible union, and a `StreamEvent` per streaming operation with event types; `BinaryResponse` when your spec declares binary responses | `Map`, `Error`, `Date`, `Set`, `Symbol`, `JSON` | | `GLOTTO_ELIXIR_RESERVED_MODEL_NAME` | Elixir | The SDK's own `Client`, `ApiError`, `RawResponse`, `RawResult`, `TokenStore`, `Webhook`, `SdkTest`, the agent-primitive helpers (`Tokens`, `Tokenizer`, `VectorMath`, `RetryBudget`, …) and the optional `OAuth` / `GraphQLClient` / `EventClient` / `WebSocketConnection` modules; plus, from your own spec, a bare `` per resource (**no suffix**, unlike every other engine), one `` per discriminated-union variant, and one `Unknown` catch-all per eligible union, and `BinaryDownload` when binary responses are declared | `Map`, `String`, `Enum`, `Jason`, `Req`, `Kernel`, `Keyword` — every Elixir and dependency module is namespaced away from yours | | `GLOTTO_REACT_NATIVE_COLLIDING_MODEL_NAMES` | React Native | Another schema of **your own** — two `models` keys that case onto one type name (`foo_bar` and `fooBar` both emit `FooBar`, as do `FooBar` and `foo_bar`). Renamed automatically; the code is emitted only when you pinned `models..name.react_native` on the schema that would move | Two keys that stay distinct after casing, however similar they look | | `GLOTTO_TYPESCRIPT_COLLIDING_MODEL_NAMES` | TypeScript | The same, for the TypeScript target — both engines resolve a schema's type name through the same rule, so the same pair collides in both | Two keys that stay distinct after casing, however similar they look | Java and Kotlin reserve binary helper names only when an operation declares a binary response. Both then reserve `BinaryResponse`, `DownloadResult` and `BinaryExchange`; Java also reserves `BinaryPayload`. With no binary responses, a customer model named `BinaryResponse` keeps that name. Runtime references are qualified so customer models named Java `Runnable` or Kotlin `Set`, `Flow` and `Job` remain available. Swift is the strictest case and the reason the behavior exists: it resolves a bare type name to the declaration in the same module before any import, and offers no way to disambiguate. Kotlin's `Unit` collides only in the Multiplatform packaging variant, and is reserved across all three so a variant switch cannot break your build. **Ruby is the quietest case**: it does not refuse a second definition of a name already declared in scope — it rebinds it — so the collision produces no error at any moment a build could report one. It is worth knowing about even though you should never meet it. A schema named after a class the Ruby SDK declares would replace that class outright while `ruby -c` passed, `require 'glotto'` exited 0, and the SDK's own test suite stayed green. The failure would arrive as a `NoMethodError` from inside the SDK the first time you called the affected method. That is why these names are renamed rather than reported: there is no earlier moment at which anything could tell you. Two constants that *look* like they belong on that list are not on it — `READ` and `WRITE` live inside `Glotto::OAuth2Scopes`, which Ruby treats as a different constant from `Glotto::READ`, so a schema of either name is left exactly as you wrote it. **Elixir used to be Ruby's failure one step further, and is now the opposite — it fails loudly.** Like Go's, and unlike every other engine's, its reserved set contains no language builtin at all: a module named `Map` in your SDK's namespace is `Glotto.Map`, which never captures Elixir's own `Map`, so schemas called `Map`, `String` or `Enum` are perfectly safe. What is not safe is a schema named after a module the SDK declares itself. Which way that fails depends on where the two `defmodule`s land, and the idiomatic package layout moved them apart. Two declarations of one module name **in a single file** are accepted silently — the later one simply replaces the earlier, and a schema named `Client` would compile cleanly with your model's struct, `from_map/1` and `to_map/1` quietly gone. That was the old single-file SDK. Each module now has its own file, and **across two files Elixir refuses outright**: ``` error: cannot define module Glotto.Client because it is currently being defined in lib/glotto/client.ex:1 ``` The build stops; nothing is silently lost. So the rename is no longer protecting you from a missing model — it is protecting you from an SDK that does not compile at all, which is a better failure but still not one you should have to meet. It applies to your resource modules too: this engine names a resource module after the resource key with **no suffix**, so a `pets` resource emits `defmodule Glotto.Pets` and a schema named `Pets` collides with it. Like the Swift resource case above, that one is a fact about your spec rather than about the engine, so adding the resource later is what creates the collision — the diagnostic names the resource, and renaming or removing it resolves the collision just as renaming the schema does. ##### Two of your own schemas emitting one type name A separate problem with a separate code, and no name of the SDK's involved. C# writes each model to its own `Glotto/.cs` and derives that name in PascalCase, which is lossy — so two schemas that differ only in casing or separators (`widget` and `Widget`, `foo_bar` and `FooBar`) emit **one** type name and claim one file. Left alone, whichever file is written last destroys the other, the SDK builds cleanly, and one of your schemas is simply not in it. | Code | Language | When you see it | | --- | --- | --- | | `GLOTTO_CSHARP_MODEL_NAME_COLLISION` | C# | Two or more of your schemas emit the same C# type name, **and** a `models: { … : { name: { csharp: … } } }` pin sits on the one generation would otherwise rename | **Generation resolves this for you in every other case.** The schema whose name is already spelled exactly as the type it emits keeps that type name, and each other takes the model suffix — `FooBar` and `fooBar` become `FooBar` and `FooBarModel`. Where no schema is spelled that way, the first key in code-point order keeps it — and generation reports every rename it applied, exactly as it does for a [reserved model name](https://glotto.dev/docs/diagnostics-sdk-generation/#reserved-model-names). Only the C# type name changes; your other SDKs keep both original names. Pinning the schema that already keeps its spelling does not trigger this diagnostic: that schema was never going to move, so the pin asks for nothing generation was not already doing. **A pin is a decision, so it is never renamed over.** Where the schema generation would otherwise RENAME got its name from `models..name.csharp`, the C# target is refused instead: renaming over an explicit pin would give you a type name you did not ask for. Give one of the two a different C# name, or rename one schema everywhere with a [`rename_schema` transform](/docs/transforms#rename_schema). Every other target still generates. ##### Two of your own schemas sharing one name Every code above is about a collision with something *Glotto* declares. This one is not: it fires when two schemas **you** wrote would emit the same type, so neither name is reserved and there is nothing of ours in the way. | Code | Language | Fires when | Resolved automatically? | | --- | --- | --- | --- | | `GLOTTO_PHP_MODEL_NAME_COLLISION` | PHP | Two schema names emit one PHP class — `foo_bar`, `fooBar`, `foo-bar` and `FooBar` all emit `class FooBar`, and PHP class names are case-insensitive, so `widget` and `WIDGET` are one class too | Yes — the later ones become `FooBarModel`, `FooBarModel2`, … and every rename is reported. You only see the diagnostic if the rename cannot be applied | PHP gives every model its own file, so before the rename existed the second schema's file simply replaced the first on disk and the model was gone — with `php -l` reporting the survivor clean, the SDK loading without complaint, and its own test suite passing. That is the failure the automatic rename removes. Two situations still stop the PHP target rather than renaming, and both raise the code above. The first is a **pin you cannot have**: if you set `models: { A: { name: { php: FooBar } } }` while another of your schemas already emits `FooBar`, generation refuses instead of quietly giving your pinned model a different name — a pin is a decision, not a suggestion. Rename either schema, or drop the pin and let the automatic rename resolve it. The second is a collision the rename does not yet cover — a schema named after a **discriminated-union variant class**, such as `PetCat` for a `Pet` union with a `cat` discriminator value. Generation stops and names both files rather than writing an SDK with one of the declarations deleted out of it; renaming the schema resolves it. ##### When two of *your own* schemas collide Everything above is a schema colliding with a name the SDK itself puts in scope. Two of *your own* schemas can collide the same way, and Glotto resolves that by renaming too — the second schema keeps its data, its fields and its file, under a `Model` type name that the generate run reports. Nothing is dropped, and no target is skipped. There is one case it cannot resolve for you: when you **pinned** the name it would have to move. | Code | Language | Raised when | How to resolve it | | --- | --- | --- | --- | | `GLOTTO_RUBY_MODEL_NAME_COLLISION` | Ruby | You set `models..name.ruby` to a Ruby name another of your schemas already emits. Ruby's constant and file-name casing merge spellings your spec keeps apart — `FooBar`, `foo_bar`, `fooBar` and `foo-bar` all emit `Glotto::FooBar` into `lib/glotto/foo_bar.rb`, and `foofoo` / `FOOfoo` share the file while `FOOFoo` / `FOO_foo` share the constant, so two names that look distinct to you can be one name to Ruby | Rename either schema: pick a different Ruby name for the pinned one with `models..name.ruby`, or give the other schema a Ruby name of its own the same way. Removing the pin also resolves it — without one the collision is renamed automatically. Only the Ruby target is affected; every other SDK in the same run still generates | | `GLOTTO_RUBY_UNIT_CONSTANT_COLLISION` | Ruby | Two files of the generated Ruby gem would declare the same constant at the top of the `Glotto` module — for example a schema whose name lands on one of the SDK’s own classes after the automatic rename has already run. Ruby does not refuse a repeated constant: it reopens the first declaration and MERGES the second into it, so the gem would load, the generated tests would pass, and the class would be half your schema and half the SDK’s. Generation stops instead | Rename the schema that collides with `models..name.ruby`, choosing a name no other schema and none of the SDK’s own classes use. The message names both files, so the second path tells you which of your schemas is involved. Only the Ruby target is affected; every other SDK in the same run still generates | A pin is treated as a decision rather than a suggestion, which is why this is reported instead of renamed: quietly emitting your pinned schema under a different name would substitute a type name you never asked for. A pin on the schema that *keeps* its name is never reported — that one generates exactly as you pinned it. #### Two generated Kotlin types that claim one file | Code | Meaning | | --- | --- | | `GLOTTO_KOTLIN_EMITTED_PATH_COLLISION` | Two types the Kotlin SDK generates **from your spec** would be written to the same file, and neither of them is a schema, so there is no model for the automatic rename above to move. The message names both keys that produced it. Two cases you can actually hit. (1) An operation with an inline JSON or multipart request body mints `Request`, and a discriminated union mints `` from your own discriminator values — so a `pets.add` operation beside a union named `PetsAdd` that maps the value `request` both mint `PetsAddRequest`. (2) Two of your keys that differ only in CASE mint two type names that differ only in case — resources keyed `widget` and `WIDGET`, each with an inline request body, mint `WidgetAddRequest` and `WIDGETAddRequest`, and a JVM class file is named after the type rather than the source file, so the two claim one `.class` path however the sources are named. Rename the union, the discriminator value, the resource, or the operation; any one of the four resolves it. Only the Kotlin target drops out — every other target you configured still generates. | Kotlin is the one language where two generated types can claim one file *without* a schema being involved, because it writes one file per top-level type and names each file after the type. The same property is why the Kotlin reserved set above compares the SDK's **own** type names case-insensitively while leaving `List`, `Map` and `Any` alone: `Client` and `CLIENT` are two perfectly good Kotlin types, but they compile to `Client.class` and `CLIENT.class`, which are one file on macOS and Windows. That build is **green** and the class is simply absent, so a schema named `CLIENT` is renamed to `CLIENTModel` rather than left for a compiler that will not complain. A separate problem with a separate code, and no name of the SDK's involved. Java writes each model to its own `com/glotto/.java` and derives that name in PascalCase, which is lossy — so two schemas that differ only in casing or separators (`widget` and `Widget`, `foo_bar` and `FooBar`) emit **one** type name and claim one file. Left alone, whichever file is written last destroys the other, the SDK builds cleanly, and one of your schemas is simply not in it. | Code | Language | When you see it | | --- | --- | --- | | `GLOTTO_JAVA_MODEL_NAME_COLLISION` | Java | Two or more of your schemas emit the same Java type name, **and** you pinned the one Glotto would have renamed with `models: { … : { name: { java: … } } }` | **Generation resolves this for you when you have not pinned a name.** The schema whose name is already spelled exactly as the type it emits keeps that type name, and each other takes the model suffix — `FooBar` and `fooBar` become `FooBar` and `FooBarModel`. Where no schema is spelled that way, the first key in code-point order keeps it — and generation reports every rename it applied, exactly as it does for a [reserved model name](https://glotto.dev/docs/diagnostics-sdk-generation/#reserved-model-names). The order your schemas appear in your document never decides this, so re-ordering two of them never moves a type name. Only the named target's type name changes; your other SDKs keep both original names. **A pin is a decision, so it is never renamed over.** Where the schema the rename would have moved got its name from `models..name.`, that one target is refused instead: renaming over an explicit pin would give you a type name you did not ask for. Give one of the two a different name for that target, or rename one schema everywhere with a [`rename_schema` transform](/docs/transforms#rename_schema). Every other target still generates. ##### The same collision in every other target The rule above is not C#'s, Java's, PHP's or Ruby's — it is one rule, and every target Glotto generates applies it. Six more raise their own code for the pinned case: | Code | Language | What the collision would have done, measured | | --- | --- | --- | | `GLOTTO_DART_MODEL_NAME_COLLISION` | Dart | `dart analyze` reports `The name 'FooBar' is already defined` and the SDK does not analyze | | `GLOTTO_ELIXIR_MODEL_NAME_COLLISION` | Elixir | **Nothing at all.** `elixirc` exits 0 with no diagnostic and the second `defmodule` simply replaces the first, so one of your schemas is gone from a build that reported success | | `GLOTTO_GO_MODEL_NAME_COLLISION` | Go | `go vet` reports `FooBar redeclared in this block` and the SDK does not build | | `GLOTTO_PYTHON_MODEL_NAME_COLLISION` | Python | **Nothing at all.** Importing the SDK succeeds and the second `class` rebinds the name, so one of your schemas is gone with no error from any tool | | `GLOTTO_RUST_MODEL_NAME_COLLISION` | Rust | `cargo check` reports `error[E0428]: the name \`FooBar\` is defined multiple times` and the SDK does not build | | `GLOTTO_SWIFT_MODEL_NAME_COLLISION` | Swift | `swiftc` reports `invalid redeclaration of 'FooBar'` and every use site becomes ambiguous, so the SDK does not build | | `GLOTTO_KOTLIN_MODEL_NAME_COLLISION` | Kotlin | `kotlinc` writes one file per top-level type, so the two claim one path and one is discarded before the compiler runs. Distinct from `GLOTTO_KOTLIN_EMITTED_PATH_COLLISION`, which is the case where neither name is a schema of yours | **Two of those six say nothing, and that is the reason the rename exists.** In Elixir and Python the generated SDK builds, imports, and passes its own emitted tests with one of your schemas missing — there is no error to look for, and no exit code that would tell you. The four that fail loudly are the easy half. Everything in the two paragraphs above applies unchanged to all of them: the collision is renamed automatically and every rename is reported, only the affected target's type name moves, and a pin is refused rather than renamed over. #### The spec-repo target Raised when [`targets.spec_repo`](/docs/spec-repo) asks for a document your input cannot produce. Both are refusals of an **input domain**, not of postponed work — and both stop the run rather than publishing a spec repo with a document missing, because a published spec repo is a surface your consumers read: an absent file there reads as a fact about your API rather than as a Glotto refusal. | Code | Meaning | | --- | --- | | `GLOTTO_SPEC_REPO_INPUT_UNSUPPORTED` | The target is enabled for an input that has no spec document to publish. A GraphQL input is the case: it never becomes a canonical spec document, which is the same reason no spec changelog is produced for it. Remove `spec_repo` from `targets:`, or point the project at an OpenAPI or AsyncAPI spec. | | `GLOTTO_SPEC_REPO_VARIANT_UNAVAILABLE` | One requested variant cannot be produced for this input; the message names which and why. `with_code_samples` needs `openapi.code_samples.formats` set (and is OpenAPI-only); `with_transforms` is OpenAPI-only, because the transform engine never runs on an AsyncAPI document. Drop that member from `targets.spec_repo.variants` — the remaining variants publish normally. Distinct from the code above so you can tell "your spec cannot be published" from "two of the three documents you asked for can". | #### Retiring a file Glotto no longer generates Raised when a Glotto release stops emitting a file your project already has — a build config the stack outgrew, a module a newer layout replaced. Regeneration removes it, so your tree keeps matching the project Glotto builds today rather than accumulating files nothing reads. **Only a file Glotto can prove it wrote is ever removed**, and only while your copy still matches the bytes Glotto produced. Every generated source carries a `@glotto:generated-checksum` comment; that comment is what makes the removal safe. A file with no checksum comment — anything you added yourself, and anything Glotto emits unstamped, such as the machine-readable mirrors under `public/` — is left exactly where it is, whether or not Glotto still generates it. Turning a target off in `glotto.yml` deletes nothing either: the tree it wrote stays until you remove it. That leaves one case Glotto will not decide for you. | Code | Meaning | | --- | --- | | `GLOTTO_RETIRED_FILE_EDITED` | Glotto no longer generates this file, **and you have edited it** since it was generated — so removing it would destroy work, and keeping it would leave your project carrying a file the current stack does not use. The run stops before writing anything, so your tree is untouched and every other file is exactly as you left it. Move whatever you still need into a file you own, then rerun. To retire it as-is, rerun with `--force`: Glotto copies the file to `.glotto/backup/` before removing it and names the copy in its summary, so the bytes stay recoverable either way. | ### Diagnostics reference Source: https://glotto.dev/docs/diagnostics/ The SDK-readiness lint rules and the entry point for every GLOTTO_ diagnostic, with severities and resolution guidance. **Looking up a `GLOTTO_*` code?** Jump to [config diagnostics](/docs/diagnostics-config#config-diagnostics), [migration diagnostics](/docs/diagnostics-migrations#migration-diagnostics-from-stainless), or [fatal errors](/docs/diagnostics-fatal#fatal-errors). Every code Glotto can emit has a row in this reference. Glotto's **SDK-readiness ruleset** describes spec shapes that produce awkward or incomplete SDKs — missing `operationId`s, anonymous schemas, undeclared errors or auth, missing idempotency advertisement. Resolving them is the cheapest way to raise the quality of every generated SDK at once, and the rule ids below are the ones [`diagnostics.rules`](/docs/glotto-yml-project-settings#diagnostics) accepts. **Where you see them:** `glotto generate` prints every finding for your spec, alongside any problems in your `glotto.yml`. The ruleset runs as part of generation, so there is nothing separate to run and nothing to remember — if your spec has a readiness problem, you hear about it the next time you generate. This page enumerates every rule in that ruleset, then links to every coded diagnostic Glotto reports. Each finding carries a Spectral-compatible shape: ```json { "rule": "operation-id-missing", "severity": "error", "path": ["paths", "/pets", "post"], "message": "…" } ``` #### Severities Every rule is `error` or `warning`, and the two mean different things: an **error** blocks correct SDK generation, a **warning** is a quality or developer-experience problem in the SDK that results. A rule's default severity is listed with it below, and [`diagnostics.rules`](/docs/diagnostics-config#tuning-diagnostics) overrides it. ##### Release-gating guidance Treat the two severities as a release ladder: - **Resolve every `error` before your first published release** — errors block correct SDK generation (e.g. a method with no name). - **Resolve every `warning` before `1.0`** — warnings are quality and DX issues that are cheap to fix early and awkward to change after consumers depend on the SDK. To turn the second rung into a hard gate, cap the allowed warning count with [`diagnostics.max_warnings`](/docs/diagnostics-config#tuning-diagnostics), then ratchet it down toward zero on the way to `1.0`. #### Errors Errors block high-quality generation and should be fixed first. ##### `operation-id-missing` **Severity:** error · **Path:** `paths..` An operation has no `operationId`. SDK method names derive from `operationId`, so without one the method name has to be synthesized from the path and verb — producing awkward, unstable names. **Fix:** give every operation a unique, descriptive `operationId` (e.g. `listPets`, `createPet`). #### Warnings Warnings don't block generation but degrade SDK quality, docs, or ergonomics. ##### `operation-undescribed` **Severity:** warning · **Path:** `paths..` An operation has neither a `summary` nor a `description`. The generated SDK reference and the MCP tool descriptions both draw on this prose, so its absence yields thin docs and lower-quality MCP tool definitions. **Fix:** add a `summary` (and ideally a `description`) to each operation. ##### `inline-object-schema` **Severity:** warning · **Path:** `paths...requestBody|responses..content..schema` A request body or response uses an **anonymous inline object schema** (a `type: object` / `properties` block, or an array of one) instead of a `$ref` to a named `components.schemas` entry. Inline objects generate unnamed, un-reusable SDK types. **Fix:** hoist the schema into `components.schemas` and reference it with `$ref`. ##### `no-error-response` **Severity:** warning · **Path:** `paths...responses` An operation declares no `4xx`, `5xx`, or `default` response, so the SDK cannot model the errors the endpoint can return — callers get untyped failures. **Fix:** declare the error responses the operation can produce (a shared `default` error response is a good baseline). ##### `mutation-no-idempotency-key` **Severity:** warning · **Path:** `paths..post` A `POST` operation does not advertise an `Idempotency-Key` header parameter. Glotto can auto-generate and inject idempotency keys so retries of a mutation are safe — but only when the endpoint accepts the header. **Fix:** add an `Idempotency-Key` header parameter to the operation (or to the path item, shared across its operations). See [Idempotency keys](/docs/idempotency-key) for the concept. ##### `required-header-param-not-client-scoped` **Severity:** warning · **Path:** `paths..` An operation declares a **required header parameter** that only a minority of the document's operations declare at all. Every generated SDK takes such a header as a normal call argument, so those are unaffected — but a target whose only home for it is *client-scoped* cannot express a value that varies from one operation to the next. The generated Terraform provider is the case in point: the header becomes a `Required` argument on the `provider "glotto"` block, and that single value is sent on **every** request the provider makes. A header the whole API requires (a tenant id, an API-version date) is not flagged — one provider-block value is exactly right for it. **Fix:** if the value really is constant for the whole client, declare the header on every operation that accepts it — or set it once with `auto_populate` in [`glotto.yml`](/docs/glotto-yml), which stamps it into the request and removes it from the generated signatures. If it genuinely varies per call, keep using it from the SDKs (where it is a per-call argument) and expect the Terraform provider to send one fixed value. ##### `no-security-defined` **Severity:** warning · **Path:** *(document root)* The API declares no security at all — no `components.securitySchemes`, no root `security`, and no per-operation `security`. The generated SDK has nothing to wire authentication to. **Fix:** declare a security scheme under `components.securitySchemes` and apply it via a root or per-operation `security` requirement. See [Authentication](/docs/authentication). #### Coded diagnostics Every stable `GLOTTO_*` code remains documented in this reference, grouped by where it is raised: - [Configuration and validation diagnostics](/docs/diagnostics-config) - [Migration diagnostics](/docs/diagnostics-migrations) - [Fatal configuration, spec, and transform diagnostics](/docs/diagnostics-fatal) - [SDK generation diagnostics](/docs/diagnostics-sdk-generation) - [Runtime diagnostics raised by generated SDKs](/docs/diagnostics-runtime) #### Roadmap Importing your existing Spectral ruleset, custom rules, one-click autofixes, and inline editor (LSP) diagnostics are tracked as follow-ons to the linter — they are not available yet. Per-rule severity overrides and suppression, and a release-gating warning budget, **are** available today via the [`diagnostics` config block](/docs/diagnostics-config#tuning-diagnostics). ### Drift detection Source: https://glotto.dev/docs/drift-detection/ Generated code is committed to Git, and Glotto fails a PR when the committed output no longer matches what the generator produces. Glotto **commits generated code to Git** and guards it with drift detection. The check regenerates your SDKs/docs/MCP server in memory and **fails the PR if the committed output no longer matches** what the generator produces from the current spec + `glotto.yml`. #### Why commit generated code - **No slow-install problem** — consumers pull ready-to-use code; there's no toolchain (JVM, Python, Go) running during their install. - **Reviewable output** — code review reads the generator's *actual* output, not an inference of it. A generator change shows up as a concrete diff. #### How the gate works drift detection regenerates and diffs against the committed tree; on drift it exits non-zero and reports the diff. Glotto emits a per-VCS-provider CI workflow that gates every PR with [verification report](/docs/verification-report) — the check fails on drift **or** a hand-edited managed file, and the [**verification report**](/docs/verification-report) (pinned spec/config hashes, per-target drift and custom-code integrity) is published to the job summary and CI artifacts — so "someone changed the generator but forgot to regenerate" can't slip through, and the proof is visible on the PR itself. See [drift detection](/docs/drift-detection) and [verification report](/docs/verification-report) in the CLI reference for the full flag set — comparing against the directories a [workspace file](/docs/cli#the-workspace-file) declares (no flag), a local tree (`--against`) or a remote branch (`--provider`), and emitting the CI workflow (`--emit-workflow`). #### What the gate actually prints Nothing below is typed by hand. Both panes are what `renderDriftReport` emitted over [the committed fixture this repo dogfoods](/docs/pipeline) — the same tree the drift gate regenerates on every one of our own pull requests. **the committed SDK, regenerated and compared — the state every green PR is in** The state every green PR is in: 66 committed files, regenerated and compared. **regenerated + byte-diffed in CI** `renderDriftReport (@glotto/core-vcs)` `packages/cli/tests/fixtures/drift-gate` `0410e9dad464` files compared 81 out of sync 0 ```markdown ✓ SDK in sync — no drift detected. ``` Change one word of the spec and leave the committed SDK alone, and the same check says so — across every target, not just the SDK: **one word changed in the spec — `` `summary: List pets` `` became `` `summary: List every pet` ``** One summary reworded in the spec. Eleven files out of sync, reported as a diff you can read. **regenerated + byte-diffed in CI** `renderDriftReport (@glotto/core-vcs)` `packages/cli/tests/fixtures/drift-gate` `0560947c783e` files compared 81 out of sync 11 ````markdown ## SDK drift detected 11 files out of sync with the committed SDK. ### changed: `cli/README.md` ```diff - | `pets listPets` | `GET /pets` | List pets | + | `pets listPets` | `GET /pets` | List every pet | ``` ### changed: `cli/cli.js` ```diff - "summary": "List pets", + "summary": "List every pet", ``` ### changed: `graph/graph.json` ```diff - "summary": "List pets" + "summary": "List every pet" ``` ### changed: `mcp/src/server.ts` ```diff - // @glotto:generated-checksum e30acd0f04fffbf5e8122b85c3c76c9dbe50cece7ff42c7e47803ef9227fb641 - // Generated by @glotto/codegen-mcp. Edit glotto.yml, not this file. - import { McpServer } from '@modelcontextprotocol/server'; - import { z } from 'zod'; - import { type ClientCapabilities, jsonCoercer, nameAdapter, parseCapabilities, type SchemaAdapter, schemaAdapter } from './adapt.js'; - import { registerDynamicTools, registerOperationTools } from './dynamic.js'; - import type { ServerDescription } from './debug.js'; - import { parseFilters, shouldRegister } from './filters.js'; - import { parseMode, type ServerMode } from './mode.js'; - import { browseOperations, type OperationEntry } from './operations.js'; - import { forwardedCredentialSource } from './passthrough.js'; - import { clampText, errorResult, projectText } from './results.js'; - import { createSandbox, type SandboxSdkBinding } from './sandbox.js'; - import { SDK_ENTRYPOINT, SDK_FILES } from './sdk-source.js'; - import { buildMergedIndex, resolveResultModels, searchDocsPage, type SearchRecord, type TypeDescriptor } from './search.js'; - - const SERVER_NAME = "petstore-mcp"; - const BASE_URL = process.env['GLOTTO_API_BASE_URL'] ?? "https://api.petstore.example"; - const SANDBOX_PERMISSIONS = { allowNet: ["api.petstore.example"] }; - // glotto.yml#/mcp#/modes: a requested-but-disabled mode resolves to the enabled default. - const ENABLED_MODES: readonly ServerMode[] = ["tools","code","dynamic"]; - - // Named models referenced by the tool schemas; z.lazy so reference cycles resolve. - const Model_Pet: z.ZodTypeAny = z.lazy(() => z.object({ "id": z.number().int(), "name": z.string() })); - - const SEARCH_INDEX_MODELS: Record = { - "Pet": { - "kind": "object", - "fields": { - "id": { - "kind": "integer" - }, - "name": { - "kind": "string" - } - }, - "required": [ - "id", - "name" - ] - } - }; - const SEARCH_INDEX_BASELINE: SearchRecord[] = [ - { - "kind": "reference", - "title": "Create a pet", - "url": "/reference/pets/createPet", - "resource": "pets", - "method": "createPet", - "httpMethod": "POST", - "path": "/pets", - "tool": "pets_createPet", - "sdkCall": "client.pets.createPet", - "parameters": [], - "text": "Create a pet", - "requestBody": { - "required": true, - "contentType": "application/json", - "schema": { - "kind": "model", - "model": "Pet" - } - }, - "response": { - "kind": "model", - "model": "Pet" - }, - "sampleInput": { - "id": 0, - "name": "" - } - }, - { - "kind": "reference", - "title": "List pets", - "url": "/reference/pets/listPets", - "resource": "pets", - "method": "listPets", - "httpMethod": "GET", - "path": "/pets", - "tool": "pets_listPets", - "sdkCall": "client.pets.listPets", - "parameters": [], - "text": "List pets", - "response": { - "kind": "array", - "items": { - "kind": "model", - "model": "Pet" - } - } - } - ]; - const DOCS_INDEX_LOCATION = process.env['GLOTTO_DOCS_INDEX']; - // Loaded once at startup; the search_docs handler awaits this cached merged index. - const docsIndexPromise = buildMergedIndex(SEARCH_INDEX_BASELINE, DOCS_INDEX_LOCATION); - - // Attach the upstream API credential to each outbound Tools Mode request: the caller-forwarded - // credential when credential passthrough is armed (mcp-credential-passthrough #3350), else this - // caller's own upstream OAuth token when that flow is on (#3351), else the operator's - // environment. Code Mode reads this too (#3348): its guest↔SDK binding resolves the - // credential HERE, per request, so the guest's client and the Tools Mode handlers can - // never disagree about which credential this caller reaches the API with. - function applyAuth(headers: Record): void { - const forwarded = forwardedCredentialSource(); - const token = forwarded !== undefined ? forwarded.authorization : process.env["GLOTTO_API_TOKEN"]; - if (token) headers['Authorization'] = "Bearer " + token; - } - - // The shared operation table (mcp-dynamic-tools-mode, ADR-0088), in collectOperations order: - // Tools Mode registers each entry as its own tool; Dynamic Mode serves the same entries - // through the list_tools/describe_tools/invoke_tool meta-tools. - const OPERATIONS: readonly OperationEntry[] = [ - { - name: "pets_createPet", - description: "Create a pet", - resource: "pets", - method: "createPet", - httpMethod: "POST", - path: "/pets", - meta: { resource: "pets", tags: ["pets"] }, - jqInjected: true, - inputShape: (coerce) => ({ body: coerce(Model_Pet), jq_filter: z.string().describe("jq-style filter to shape the text result (subset: .field, .[\"key\"], .[0], .[] iteration, | pipes); applied to the JSON response body").optional() }), - outputSchema: z.object({ "id": z.number().int(), "name": z.string() }), - handler: async (input, jqFilter) => { - const path = "/pets"; - const url = new URL(BASE_URL + path); - const headers: Record = {}; - headers['content-type'] = 'application/json'; - applyAuth(headers); - const response = await fetch(url, { - method: "POST", - headers, - body: JSON.stringify(input['body']), - }); - const text = await response.text(); - if (!response.ok) return errorResult(response.status, text); - let structured: unknown; - try { - structured = JSON.parse(text); - } catch { - structured = undefined; - } - if (typeof structured !== 'object' || structured === null || Array.isArray(structured)) { - return { content: [{ type: 'text', text: 'HTTP ' + response.status + ': expected a JSON object response body but received: ' + (text === '' ? 'an empty body' : text) }], isError: true }; - } - const projected = projectText(text, jqFilter); - if (!projected.ok) return { content: [{ type: 'text', text: projected.text }], isError: true }; - return { content: [{ type: 'text', text: clampText(projected.text) }], structuredContent: structured as Record }; - }, - }, - { - name: "pets_listPets", - description: "List pets", + // @glotto:generated-checksum 53232a1a568b12e4d6b606365ec19a3c1375664757cc1611e9edecef1a193b61 + // Generated by @glotto/codegen-mcp. Edit glotto.yml, not this file. + import { McpServer } from '@modelcontextprotocol/server'; + import { z } from 'zod'; + import { type ClientCapabilities, jsonCoercer, nameAdapter, parseCapabilities, type SchemaAdapter, schemaAdapter } from './adapt.js'; + import { registerDynamicTools, registerOperationTools } from './dynamic.js'; + import type { ServerDescription } from './debug.js'; + import { parseFilters, shouldRegister } from './filters.js'; + import { parseMode, type ServerMode } from './mode.js'; + import { browseOperations, type OperationEntry } from './operations.js'; + import { forwardedCredentialSource } from './passthrough.js'; + import { clampText, errorResult, projectText } from './results.js'; + import { createSandbox, type SandboxSdkBinding } from './sandbox.js'; + import { SDK_ENTRYPOINT, SDK_FILES } from './sdk-source.js'; + import { buildMergedIndex, resolveResultModels, searchDocsPage, type SearchRecord, type TypeDescriptor } from './search.js'; + + const SERVER_NAME = "petstore-mcp"; + const BASE_URL = process.env['GLOTTO_API_BASE_URL'] ?? "https://api.petstore.example"; + const SANDBOX_PERMISSIONS = { allowNet: ["api.petstore.example"] }; + // glotto.yml#/mcp#/modes: a requested-but-disabled mode resolves to the enabled default. + const ENABLED_MODES: readonly ServerMode[] = ["tools","code","dynamic"]; + + // Named models referenced by the tool schemas; z.lazy so reference cycles resolve. + const Model_Pet: z.ZodTypeAny = z.lazy(() => z.object({ "id": z.number().int(), "name": z.string() })); + + const SEARCH_INDEX_MODELS: Record = { + "Pet": { + "kind": "object", + "fields": { + "id": { + "kind": "integer" + }, + "name": { + "kind": "string" + } + }, + "required": [ + "id", + "name" + ] + } + }; + const SEARCH_INDEX_BASELINE: SearchRecord[] = [ + { + "kind": "reference", + "title": "Create a pet", + "url": "/reference/pets/createPet", + "resource": "pets", + "method": "createPet", + "httpMethod": "POST", + "path": "/pets", + "tool": "pets_createPet", + "sdkCall": "client.pets.createPet", + "parameters": [], + "text": "Create a pet", + "requestBody": { + "required": true, + "contentType": "application/json", + "schema": { + "kind": "model", + "model": "Pet" + } + }, + "response": { + "kind": "model", + "model": "Pet" + }, + "sampleInput": { + "id": 0, + "name": "" + } + }, + { + "kind": "reference", + "title": "List every pet", + "url": "/reference/pets/listPets", + "resource": "pets", + "method": "listPets", + "httpMethod": "GET", + "path": "/pets", + "tool": "pets_listPets", + "sdkCall": "client.pets.listPets", + "parameters": [], + "text": "List every pet", + "response": { + "kind": "array", + "items": { + "kind": "model", + "model": "Pet" + } + } + } + ]; + const DOCS_INDEX_LOCATION = process.env['GLOTTO_DOCS_INDEX']; + // Loaded once at startup; the search_docs handler awaits this cached merged index. + const docsIndexPromise = buildMergedIndex(SEARCH_INDEX_BASELINE, DOCS_INDEX_LOCATION); + + // Attach the upstream API credential to each outbound Tools Mode request: the caller-forwarded + // credential when credential passthrough is armed (mcp-credential-passthrough #3350), else this + // caller's own upstream OAuth token when that flow is on (#3351), else the operator's + // environment. Code Mode reads this too (#3348): its guest↔SDK binding resolves the + // credential HERE, per request, so the guest's client and the Tools Mode handlers can + // never disagree about which credential this caller reaches the API with. + function applyAuth(headers: Record): void { + const forwarded = forwardedCredentialSource(); + const token = forwarded !== undefined ? forwarded.authorization : process.env["GLOTTO_API_TOKEN"]; + if (token) headers['Authorization'] = "Bearer " + token; + } + + // The shared operation table (mcp-dynamic-tools-mode, ADR-0088), in collectOperations order: + // Tools Mode registers each entry as its own tool; Dynamic Mode serves the same entries + // through the list_tools/describe_tools/invoke_tool meta-tools. + const OPERATIONS: readonly OperationEntry[] = [ + { + name: "pets_createPet", + description: "Create a pet", + resource: "pets", + method: "createPet", + httpMethod: "POST", + path: "/pets", + meta: { resource: "pets", tags: ["pets"] }, + jqInjected: true, + inputShape: (coerce) => ({ body: coerce(Model_Pet), jq_filter: z.string().describe("jq-style filter to shape the text result (subset: .field, .[\"key\"], .[0], .[] iteration, | pipes); applied to the JSON response body").optional() }), + outputSchema: z.object({ "id": z.number().int(), "name": z.string() }), + handler: async (input, jqFilter) => { + const path = "/pets"; + const url = new URL(BASE_URL + path); + const headers: Record = {}; + headers['content-type'] = 'application/json'; + applyAuth(headers); + const response = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify(input['body']), + }); + const text = await response.text(); + if (!response.ok) return errorResult(response.status, text); + let structured: unknown; + try { + structured = JSON.parse(text); + } catch { + structured = undefined; + } + if (typeof structured !== 'object' || structured === null || Array.isArray(structured)) { + return { content: [{ type: 'text', text: 'HTTP ' + response.status + ': expected a JSON object response body but received: ' + (text === '' ? 'an empty body' : text) }], isError: true }; + } + const projected = projectText(text, jqFilter); + if (!projected.ok) return { content: [{ type: 'text', text: projected.text }], isError: true }; + return { content: [{ type: 'text', text: clampText(projected.text) }], structuredContent: structured as Record }; + }, + }, + { + name: "pets_listPets", + description: "List every pet", ``` ### changed: `openapi.decorated.json` ```diff - "summary": "List pets", + "summary": "List every pet", ``` ### changed: `spec_repo/spec.base.json` ```diff - "summary": "List pets", + "summary": "List every pet", ``` ### changed: `spec_repo/spec.base.yaml` ```diff - summary: List pets + summary: List every pet ``` ### changed: `spec_repo/spec.with-code-samples.json` ```diff - "summary": "List pets", + "summary": "List every pet", ``` ### changed: `spec_repo/spec.with-code-samples.yaml` ```diff - summary: List pets + summary: List every pet ``` ### changed: `spec_repo/spec.with-transforms.json` ```diff - "summary": "List pets", + "summary": "List every pet", ``` ### changed: `spec_repo/spec.with-transforms.yaml` ```diff - summary: List pets + summary: List every pet ``` ```` This only works because the [pipeline](/docs/pipeline) is deterministic — a byte-stable canonical spec means regeneration is reproducible, so a diff means a *real* change, not noise. ### Elixir Source: https://glotto.dev/docs/elixir/ A functional Elixir Hex package built on Req: struct models, lazy Stream pagination and SSE, and ApiError structs whose kind is a documented atom. The Elixir SDK is an idiomatic Hex package emitted from the same `GlottoIR` as every other target. It is functional and module-first: the public package facade builds the client with `Petstore.new/2`, and each resource gets its own module whose functions take that struct as their first argument — `Petstore.Pets.list_pets(client)`. It is built on `Req`. #### Quickstart ```elixir def deps do [ {:petstore, "~> 1.0"} ] end ``` ```elixir client = Petstore.new("https://api.petstore.example", token: System.get_env("PETSTORE_TOKEN") ) # calls return {:ok, _} / {:error, %Petstore.ApiError{}} tuples {:ok, pet} = Petstore.Pets.create_pet(client, %{name: "Rex"}) # paginated functions return a lazy Stream Petstore.Pets.list_pets(client) |> Enum.each(&IO.puts(&1.name)) ``` #### Typed models Each `GlottoIR` model becomes a struct module with `from_map`/`to_map`; functions decode and return the typed response and accept a typed request body. Discriminated unions resolve to the right variant. #### Typed errors Functions return a `%Glotto.ApiError{}` struct carrying the parsed error body, with `Glotto.ApiError.kind/1` mapping the status to a documented atom so callers `case` on `:not_found` rather than matching status codes. See [Errors](/docs/errors). #### Pagination Paginated list functions return a lazy `Stream` that walks every page as you enumerate, advancing the cursor for you. Manual-page companions expose typed items, the full response, and explicit next-page navigation. See [Pagination](/docs/pagination). #### Retries & backoff Transient failures (`5xx`, `429`, transport errors) retry with exponential backoff and jitter, configurable per client and per call. A trailing keyword list accepts request headers, timeout, retry count, idempotency and applicable extra query/body values; explicit values override configured method/resource defaults. Terminating the request-owning Task cancels its request and retry wait. See [Retries & timeouts](/docs/retries). #### SSE streaming Server-sent-event endpoints return a `Stream` of typed events decoded from the `text/event-stream` framing. See [Streaming](/docs/streaming) and [Authentication](/docs/authentication). #### Unknown response fields Your API can add a response field without it being a breaking change — but a generated struct has only the keys it declares, and its `from_map/1` factory reads only those. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized. ```elixir {:ok, pet} = Glotto.Pets.create_pet(client, body) # A field your API started returning after this SDK was generated. species = Glotto.Pet.extra_fields(pet)["species"] # Re-encoding preserves it — a read-modify-write never silently drops it. json = pet |> Glotto.Pet.to_map() |> Jason.encode!() ``` `extra_fields/1` returns a plain map, so nested objects and lists survive intact, and retention is recursive. The struct key defaults to an empty map, so `%Glotto.Pet{id: "p1"}` still builds. The retained fields are read-only by design. To *send* a field your spec doesn't model yet, use the per-call extra-body escape hatch rather than writing to the retained bag. #### File uploads `multipart/form-data` operations build the multipart body from their fields for you; an `application/octet-stream` operation takes a positional `body` (a binary) sent raw with the right `content-type`. #### Binary downloads Declared binary responses return an owned `Glotto.BinaryDownload` with status, headers, media type, content length, filename and request ID. Consume its `chunks/1` stream, call `read_all/2` with a required `max_bytes` bound, or `pipe/2` to a binary IO device or callback. Limits count delivered bytes, including decoded gzip bytes. Close an abandoned response explicitly; terminating its original request Task also closes it. The request timeout covers the first byte or EOF, and no retry occurs after delivery. See [File transfers](/docs/file-transfers). ### Endpoint migration Source: https://glotto.dev/docs/endpoint-migration/ Rename or re-version an endpoint without breaking your SDK — aliases keep the old method working against the new operation, and deprecated marks it. Glotto **detects** breaking changes for you ([how](/docs/breaking-changes)). Endpoint migration is the other half: how to **avoid** one. When you rename or re-version an endpoint, the generated SDK method changes with it — and every one of your users' call sites stops compiling. Two `glotto.yml` blocks let you make that change without shipping a major version. #### `aliases` — keep the old method name working ```yaml aliases: createRecord: upsertRecord ``` An alias keeps a method name in the SDK and routes it to the operation you name. The key is the name your users already call; the value is the `operationId` it should now resolve to. Glotto materializes the alias as a **real method** cloned from its target, so it issues the target's request — the *new* endpoint, not the removed one: ```ts // Both exist. `createRecord` issues PUT /v1/records, exactly like upsertRecord. await client.records.upsertRecord({ id: 'rec_1', name: 'Ada' }); await client.records.createRecord({ id: 'rec_1', name: 'Ada' }); ``` Because the alias re-occupies the method the rename vacated, breaking-change detection reports **no breaking change** for it. The renamed operation still shows up — as a non-breaking addition. Alias names are resolved against your spec, so an entry whose target doesn't exist is ignored rather than failing the build. That is what lets one config describe both sides of a diff: when `breaking-changes` builds your *previous* spec, the new target isn't there yet, no alias materializes, and the old operation is simply still present. #### `deprecated` — tell callers where to go ```yaml deprecated: createRecord: Use upsertRecord instead. ``` A plain string is the message for every language. To vary it — method names differ across languages — use the object form, which takes a required `default` plus per-target overrides: ```yaml deprecated: createRecord: default: Use upsertRecord instead. python: Use upsert_record() instead. go: Use UpsertRecord instead. ``` `default` is required so every language always has message text; several targets have no message-less deprecation form. Each SDK renders it in **that language's own construct**, so your users get the warning from their own compiler or editor rather than from release notes: | Language | Emitted | |---|---| | TypeScript, React Native | `@deprecated` JSDoc | | Python | `Deprecated:` docstring line | | Go | `// Deprecated:` (the godoc convention) | | Java | `@Deprecated` + `@deprecated` javadoc | | Kotlin | `@Deprecated("…")` | | C# | `[Obsolete("…")]` | | PHP | `@deprecated` docblock | | Ruby | `# @deprecated` (YARD) | | Rust | `#[deprecated(note = "…")]` | | Swift | `@available(*, deprecated, message: "…")` | | Dart | `@Deprecated('…')` | | Elixir | `@deprecated "…"` | Deprecation is compile-time and documentation-time only — nothing is added to the request path, so a deprecated call costs your users nothing at runtime. #### Putting them together The two blocks are independent maps keyed the same way, so you can deprecate an alias — which is the usual migration shape: the old name keeps working *and* warns. **A rename that breaks nobody** Nothing below is typed by hand. The two input panes are slices of a demo in this repo; the emitted pane is what one real generate run over those exact inputs produced — including the doc comment recording which method is canonical. Derived from `examples/endpoint-migration/inputs` — every byte below is sliced from that demo or from one real generate run over it. **Your spec — the operation after the rename** (`examples/endpoint-migration/inputs/api.v2.yaml` `paths → /v1/records → put`) ```yaml put: operationId: upsertRecord summary: Create or update a record requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/Record' responses: '201': description: Created content: application/json: schema: $ref: '#/components/schemas/Record' ``` **Your glotto.yml — the alias that reoccupies the vacated method** (`examples/endpoint-migration/inputs/glotto.yml` `aliases`) ```yaml # ── The whole feature, in two blocks ────────────────────────────────────────────────────────── # # `POST /v1/records` (`createRecord`) became `PUT /v1/records` (`upsertRecord`). Left alone, the # generated SDKs lose `createRecord()` and every caller breaks. # # `aliases` keeps the old method name in the SDK, routed to the NEW operation: core-ir materializes # `createRecord` as a real method cloned from `upsertRecord`, so it issues `PUT /v1/records` and # every one of the 13 engines emits it with no alias-specific code. It is also what makes # breaking-change detection report the rename as NON-breaking — the classifier keys operations by # their resource-and-method path, and the alias re-occupies the one the rename vacated. # # The SECOND entry is the re-versioning shape (#3166). `POST /v1/exports` (`createExport`) became # `POST /v2/exports` (`createExportV2`) — a different path prefix, so a different path-derived # resource, and `/v1/exports` is not in the current spec at all. The string form cannot express # that: it materializes the alias beside its target, under `v2`, leaving the `v1.exports.…` key # the rename vacated still vacated — and the rename still breaking. # # The object form supplies the one fact the config alone can: `path` is where the superseded # method was SERVED. Glotto derives the resource from it with the same static-segment rule it # derives every other resource from, so the alias reoccupies exactly the key the rename vacated. # `path` places the method; it never routes it — the alias still issues `POST /v2/exports`. aliases: createRecord: upsertRecord createExport: target: createExportV2 path: /v1/exports ``` **Your glotto.yml — the message callers are told** (`examples/endpoint-migration/inputs/glotto.yml` `deprecated`) ```yaml # `deprecated` marks the old name so callers are told where to go, in each language's own # construct (`@deprecated`, `@Deprecated`, `[Obsolete]`, `#[deprecated]`, `// Deprecated:`, …). # A plain string is the message for every language; the object form takes a required `default` # plus per-target overrides — used here because the Python SDK's method is `upsert_record`. deprecated: createRecord: default: Use upsertRecord instead. python: Use upsert_record() instead. createExport: Use createExportV2 instead. ``` **glotto generate** **What Glotto emits — a real method, cloned from its target** (`typescript/src/resources/v1-records.ts` `V1RecordsResource.createRecord`) ```ts /** * Alias of `upsertRecord`. * @deprecated Use upsertRecord instead. */ createRecord(params: RecordModel, options?: RequestOptions): Promise { return this.core.request('PUT', '/v1/records', { ...options, body: params, contentType: 'application/json' }); } ``` generated-checksum `40eab644eedff65fc7e54714906e8f3d463bbc7540c0ad8021ba3c30f90cdd2a` #### Re-versioning — when the path moves too The alias above is materialized **in its target's resource**, which is right whenever the rename leaves the path alone. But Glotto derives the resource tree from your URLs, so a rename that also moves the path — `/v1/exports` → `/v2/exports` — moves the resource with it. The SDK surface your users call (`exports.createExport(...)` under `v1`) is then somewhere your *current* spec no longer describes, and no amount of reading that spec can find it. Say where it used to live, and the alias works exactly as before: ```yaml aliases: createExport: target: createExportV2 path: /v1/exports ``` `path` is the path the **superseded** method was served at. Glotto derives its resource from that path with the same rule it uses for every other path in your spec, so the alias reoccupies exactly the surface the move vacated — and `breaking-changes` reports no breaking change, same as the in-place rename. `path` **places** the method; it never **routes** it. The alias still issues the target's request: ```ts // Still under the v1 surface your users already call — and it POSTs /v2/exports. await client.v1.exports.createExport({ format: 'csv' }); ``` Both fields are required in this form. An alias with only a `target` is the plain string form written the long way, so Glotto rejects it rather than quietly treating it as one. #### Limitations - Deprecating a **model, field, or individual enum member** is not supported yet; `deprecated` is keyed by operation. #### See also - [`glotto.yml` reference](/docs/glotto-yml) — the full config surface. - [Drift detection](/docs/drift-detection) — the gate that keeps committed output honest. - [The verification report](/docs/verification-report) — what every regeneration proves. ### Errors Source: https://glotto.dev/docs/errors/ A failed request throws a typed ApiError carrying the status, method, path, parsed body, headers, and request id, plus per-status-family subclasses. A failed request throws a typed `ApiError` carrying the full context — status, method, path, response body, headers, and the request id: ```ts import { Client, ApiError } from '@your-org/petstore'; try { await client.pets.createPet({ name: '' }); } catch (err) { if (err instanceof ApiError) { console.error(err.status); // e.g. 422 console.error(err.requestId); // for support console.error(err.body); // parsed response body } } ``` The error message is `" -> : "`, so it's readable in logs without unwrapping. Subclasses per status family are generated when the spec models them. #### Every SDK, same error model The same catch-and-narrow shape is emitted in every language — these are the bytes `glotto generate` produced, each stamped with the checksum of the artifact it came from: **TypeScript** (regenerated + byte-diffed in CI `a0f8af4b462c`) ```ts import { ApiError } from 'glotto-sdk'; try { // any client call } catch (err) { if (err instanceof ApiError) { console.error(err.status, err.toString()); } } ``` **React Native** (regenerated + byte-diffed in CI `a1fefe7d3458`) ```ts import { ApiError } from 'glotto-sdk'; try { // any client call } catch (err) { if (err instanceof ApiError) { console.error(err.status, err.toString()); } } ``` **Python** (regenerated + byte-diffed in CI `2242160f8958`) ```python from glotto_sdk import ApiError try: ... # any client call except ApiError as err: print(err.status, err) ``` **Go** (regenerated + byte-diffed in CI `f0c565cc46f8`) ```go // err from any client call var apiErr *sdk.APIError if errors.As(err, &apiErr) { fmt.Println(apiErr.Status, apiErr.Error()) } ``` **Java** (regenerated + byte-diffed in CI `12c05e098737`) ```java import com.glotto.Client; import com.glotto.errors.ApiError; public class Snippet { public static void main(String[] args) throws Exception { try { // any client call } catch (ApiError e) { System.err.println(e.status() + ": " + e.getMessage()); } } } ``` **Kotlin** (regenerated + byte-diffed in CI `0f5e53ae2522`) ```kotlin import com.glotto.errors.ApiError try { // any client call } catch (e: ApiError) { println("${e.status}: ${e.message}") } ``` **C#** (regenerated + byte-diffed in CI `c5013ce0e88b`) ```csharp using Glotto; using ApiError = Glotto.Errors.ApiError; try { // any client call } catch (ApiError err) { Console.WriteLine($"{err.Status}: {err.Message}"); } ``` **PHP** (regenerated + byte-diffed in CI `1acd16081b46`) ```php status . ': ' . $e->getMessage() . PHP_EOL); } ``` **Ruby** (regenerated + byte-diffed in CI `a2b854f6593f`) ```ruby require 'glotto' begin # any client call rescue Glotto::ApiError => e warn "#{e.status}: #{e.message}" end ``` **Rust** (regenerated + byte-diffed in CI `716e9ec181d1`) ```rust // `result` is the Result<_, ApiError> returned by any client call if let Err(err) = result { eprintln!("{err}"); } ``` **Swift** (regenerated + byte-diffed in CI `0cc357352baf`) ```swift import GlottoSdk do { // any client call } catch let error as APIError { print(error.status, error.kind) } ``` **Dart** (regenerated + byte-diffed in CI `67567d6df435`) ```dart try { // any client call } on ApiError catch (e) { print('API error ${e.status}'); } ``` **Elixir** (regenerated + byte-diffed in CI `b97af402f1d7`) ```elixir # any generated operation, e.g. Glotto..(client, ...) case client_call do {:ok, result} -> result {:error, %Glotto.ApiError{status: status} = error} -> IO.warn("#{status}: #{Exception.message(error)}") end ``` ### File uploads & downloads Source: https://glotto.dev/docs/file-transfers/ Send files and consume binary responses without losing bytes or metadata. An operation declared with a binary response returns an owned byte response. You can inspect its HTTP status, media type, content length, headers, and `Content-Disposition` filename before consuming it. The generated README shows the response type and usage for your SDK's language. Use the bounded read helper for small files, supplying the maximum number of bytes your application will accept. For larger files, stream chunks to a sink instead. Where the language supports backpressure, the SDK waits for the sink before reading more data. Close the response when you finish or stop early; scoped ownership and explicit cancellation release the underlying transfer. The read limit counts the bytes delivered to your application, including any transparent decompression by the HTTP client. `Content-Length` remains response metadata and may describe compressed bytes on the wire. #### Mixed responses and errors An operation can return JSON for one success status and a file for another. Its generated return type exposes both outcomes; an explicitly documented no-content success also remains distinct. Error statuses follow the SDK's ordinary error handling, including JSON error bodies. A JSON string with a base64 schema stays a value inside a JSON document. It does not become a file response merely because the schema's format is `byte` or `binary`. Binary response handling follows the operation's media type. #### Uploading Raw octet-stream bodies accept the language's byte representation. Multipart operations expose their declared file and scalar fields, and the SDK builds the multipart boundary. Use the generated operation example for the correct argument names and file representation; do not set the outer multipart `Content-Type` yourself without its matching boundary. These uploads take in-memory values. They do not provide a streaming upload or a resumable transfer protocol. Per-part filename and media-type controls depend on the language's existing upload surface. Request defaults, overrides, and establishment deadlines follow [Retries & timeouts](/docs/retries). A download's opening timeout does not limit the lifetime of an established transfer. ### Forward compatibility Source: https://glotto.dev/docs/forward-compatibility/ A server-added enum value or response field reaches an SDK generated months earlier — without crashing, and without losing data. APIs grow. A new enum member, a new response field — neither is a breaking change on the server, and neither should break an SDK a consumer pinned last quarter. Generated SDKs are built so they don't. Three guarantees, all on by default, none requiring a regeneration to benefit from: - **Open enums** — a value outside the set the SDK was generated from decodes, round-trips, and is detectable. - **Unknown response fields** — a field the SDK was never generated from is retained on decode and written back out. - **Per-call escape hatches** — you can send parameters your spec doesn't model yet. #### Open enums When your server starts returning `status: "archived"` and the SDK was generated when the set was `active | inactive`, a conventional generated enum throws on decode. Glotto's don't. An unrecognized value satisfies three properties: 1. **It decodes without crashing.** No exception, no dropped response. 2. **It round-trips at its original value.** Read-modify-write sends `archived` back, not a fallback or a null. 3. **It is detectable.** You can tell a server-added value from a known member and branch on it. This holds for **string** enums and, since Glotto took the deferred integer decision, for **numeric** enums too — the same three-part contract over a different primitive. In TypeScript that surfaces as an open union, so the known members still autocomplete while any other value is accepted: **A value the SDK predates, decoded intact** Two enums out of one real spec, and one real generate run over it: a string enum and a numeric one, both emitted open. Neither type below is written on this page — a value outside either set decodes, round-trips, and compares unequal to every member the SDK knows. Derived from `examples/realworld-assistant-api/inputs` — every byte below is sliced from that demo or from one real generate run over it. **Your spec — a string enum with two members today** (`examples/realworld-assistant-api/inputs/assistant.openapi.yaml` `components → schemas → Message`) ```yaml Message: type: object required: - role - content properties: role: type: string enum: - user - assistant content: type: array items: $ref: '#/components/schemas/ContentBlock' ``` **Your spec — the same contract over a numeric enum** (`examples/realworld-assistant-api/inputs/assistant.openapi.yaml` `components → schemas → ErrorResponse`) ```yaml ErrorResponse: type: object description: The body every operation returns when a request fails. required: - status - message properties: status: type: integer description: The HTTP status the request was answered with. # A NUMERIC enum, open on the same three-part contract a string one is: # a status this SDK predates decodes, round-trips, and is detectable. enum: - 400 - 401 - 429 - 500 message: type: string ``` **glotto generate** **What Glotto emits — an OPEN union, so `` `archived` `` still decodes** (`typescript/src/models/message-role.ts` `MessageRole`) ```ts export type MessageRole = 'user' | 'assistant' | (string & {}); ``` generated-checksum `4442693d5c56f9bfdd21804afa01eb02c8fe16c0f3f9d2813848d267301b81e1` **What Glotto emits — the same shape over a number** (`typescript/src/models/error-response-status.ts` `ErrorResponseStatus`) ```ts export type ErrorResponseStatus = 400 | 401 | 429 | 500 | (number & {}); ``` generated-checksum `a6ee2afd7e6f9fe6638e48da045c76eae987bf808bc72132899fdf8f7eff6936` Checking for a value this SDK predates is the comparison you would write anyway: it equals none of the members above. Log it, route it, or ignore it — it survives re-encoding either way. > The `& {}` is load-bearing, not noise. A plain `'active' | 'inactive' | string` collapses to > `string` and discards the known members — which are the documentation of what the API actually > accepts. ##### How it surfaces in your language Every engine keeps the same guarantee and spells it the way its language already handles extensible values: | Language | Surface | Detect an unrecognized value | | --- | --- | --- | | TypeScript, React Native | Open union (spelled out below) | It equals none of the generated members | | Python | `StrEnum` / `IntEnum` with a `_missing_` hook returning a value-preserving pseudo-member | `.is_known` is `False` | | Swift | A `RawRepresentable` struct wrapping the raw value, known members as static constants | `isKnown` is `false` | | C#, Dart, Java, Kotlin | The same value-preserving wrapper shape, known members as constants | It equals no generated constant | | Rust | A `#[non_exhaustive]` enum carrying the unrecognized value | Match its unknown variant | | PHP | A union of the generated enum and its primitive (spelled out below) | The value is the raw primitive, not an enum case | | Go, Ruby, Elixir | Raw-value passthrough — the declared type *is* the primitive | Compare against the generated constants | Open unions are `'a' | 'b' | (string & {})` and `200 | 404 | (number & {})`. For PHP, the exact surface is `Status|string`: the generated enum united with its primitive. The passthrough row is not a weaker guarantee, just a different one: in those languages the declared type never constrained the value in the first place, so an unrecognized member was always going to arrive intact. The comparison you write is the same. **Mixed and boolean enums stay closed** by design. A mixed enum has no single primitive to be open over, and a boolean enum is already total — there is no value outside `true | false` to be forward-compatible with. #### Unknown response fields The second leg covers fields rather than values: a response field your SDK was never generated from is retained on decode, readable through an accessor, and written back out on re-encode — so a read-modify-write never silently deletes data the SDK didn't recognize. Each language page documents the accessor its engine emits, in that language's idiom: [TypeScript](/docs/typescript#unknown-response-fields) · [Python](/docs/python#unknown-response-fields) · [Go](/docs/go#unknown-response-fields) · [Ruby](/docs/ruby#unknown-response-fields) · [Elixir](/docs/elixir#unknown-response-fields) — and the same section on every other [Languages](/docs/typescript) page. #### Sending what your spec doesn't model yet The retained fields above are read-only by design: they record what the SDK *received*. To **send** a parameter your spec doesn't model yet, use the per-call extra-query and extra-body escape hatches rather than writing into the retained bag. That keeps the two directions honest — retention is a record of the wire, and anything you add is something you chose to send. #### Why this matters more than it sounds Forward compatibility is what makes a pinned SDK safe to leave pinned. Without it, every additive server change is a silent deadline: consumers on older SDKs start throwing on values you consider non-breaking, and you find out from their bug reports. With it, the additive change is exactly what you intended it to be. ### The generated CLI Source: https://glotto.dev/docs/generated-cli/ A ready-to-run command-line client for your API: one command per operation, with spec-derived flags, auth, environments, uploads, and retries. From the same [Glotto IR](/docs/glotto-ir) as your SDKs, docs site, and MCP server, Glotto generates a **command-line client for your API**: a zero-dependency, executable Node.js script (Node ≥ 20, `node:` builtins and global `fetch` only), plus a `package.json` with a `bin` entry and shell-completion scripts. What you ship isn't a one-time scaffold — every regeneration re-derives the command tree, flags, and auth wiring from your spec, and the [drift gate](/docs/drift-detection) proves the committed artifact never lags it. Enable it in `glotto.yml`: ```yaml targets: cli: binary_name: acme # optional; defaults from your API name ``` `glotto generate` then writes `/cli/` — `cli.js`, `package.json`, `README.md`, and `completions/` — installable with `npm install -g .` or runnable directly with `node cli.js `. #### One command per operation Every operation becomes a command named by its resource path plus method — subresources nest (`acme events batches list`), and client-level methods surface at the root. Parameters map to flags by wire name: path parameters interpolate into the URL, query parameters land on the query string (array parameters repeat, honoring `query_settings.array_format`), header parameters become request headers, and cookie parameters fold into a `Cookie` header. `--help` (globally or per command) prints the derived command list and flags. #### Auth and environments Auth resolves like the SDKs, from the same spec: bearer/OAuth2 tokens, API keys (header or query, honoring the declared name), and HTTP basic — including per-endpoint security overrides on multi-scheme APIs. Credentials come from per-scheme environment variables (or `--api-key`). For OAuth2 schemes that declare a `clientCredentials` flow, the CLI performs the token handshake itself: set `_CLIENT_ID` and `_CLIENT_SECRET` (or pass `--client-id`/`--client-secret`) and it exchanges them at your token endpoint before the request. Environments come from your spec too — select one with `--environment ` or override with `--base-url `. #### Output, streaming, and exit codes Successful JSON responses pretty-print to stdout; streaming operations (SSE/NDJSON) pass the raw response body through as it arrives, so output pipes cleanly into `jq` or a file. Usage errors exit `2`, request failures exit `1`, success exits `0` — script-friendly by construction. #### Uploads Request bodies follow the operation's declared content type. JSON bodies take `--data ` or `--data @file`; raw bodies (`application/octet-stream` and friends) take `--data @file` for file bytes or inline `--data` for text; `multipart/form-data` operations take repeated `--field =` flags, where a value starting with `@` attaches that file as a file part. #### Pagination Commands for paginated operations accept `--all`, which walks every page using the same pagination locators the SDK iterators use — cursor, cursor-id, page/offset, and `Link`-header strategies — and prints the concatenated items as a single JSON array. #### Retries The CLI honors the same retry policy as your SDKs (`client_settings.retry`): exponential backoff with jitter, `Retry-After` honored, applied to network failures and 408/409/429/5xx responses on GET commands and operations your spec marks idempotent. #### Shell completions Bash, zsh, and fish completion scripts for the full command tree and per-command flags are emitted beside the CLI under `completions/` — the emitted README documents where each installs. #### Kept true, not just generated Like every Glotto artifact, the CLI is a deterministic projection of your spec: byte-stable output, locked by golden tests, regenerated in lockstep with your SDKs and docs, and verified by the same [drift detection](/docs/drift-detection) and compile-verification gates. Add an operation, rename a parameter, tighten auth — the CLI is provably current on the next regeneration, forever. ### The generated docs site Source: https://glotto.dev/docs/generated-docs-site/ Glotto emits an Astro docs site your customers own — reference pages, multi-language snippets, search, and theming — from the same IR as the SDKs. Alongside the SDKs, Glotto emits a complete **Astro + React + Tailwind docs project your customer owns** — generated from the same [Glotto IR](/docs/glotto-ir) and the per-language SDK snippets, so the docs never drift from the SDKs. > This is the *generated customer* docs site (the `@glotto/codegen-docs` output). It is a > different surface from this site — Glotto's own docs, which you're reading now. #### What it includes - **Reference pages** — one per operation, with params and request/response schema. - **Multi-language snippet tabs** — populated from the emitted SDK snippets. - **MDX / Markdown / Markdoc authoring** for narrative pages — guides live in a typed, schema-validated collection (`src/guides/`), so a new file is routed, listed, and indexed automatically. - **Search** (Pagefind), opened with Cmd-K / Ctrl+K and navigable with the arrow keys, with facets by HTTP method, resource, SDK language, and parameter name. Facet labels and order are configurable through `docs.search.facets`, alongside a **CSS-variable theming** system. - **First-party, cookieless analytics** (opt-in, off by default) against your own PostHog or Plausible project — page views plus search-query, feedback-vote, and playground events, with Do-Not-Track honored and no cookies or consent banner. - **Branch-preview deploys** and **custom-domain + SSL** wiring. - **Third-party decorators** — inject snippets into the OpenAPI doc as `x-codeSamples` for customers who host docs on Mintlify / ReadMe / Redocly / Bump / GitBook. - **An MCP connect page** — when your `glotto.yml` has an `mcp` block, a nav-linked `/connect-mcp/` page carries the install blocks (`claude mcp add`, `.mcp.json`, a Cursor install link) for your [generated MCP server](/docs/mcp-server). #### Hosting your docs elsewhere: the decorated spec URL If you host your reference docs on **Mintlify, ReadMe, Redocly, Bump or GitBook** rather than on the generated site, Glotto publishes the decorated OpenAPI document — your spec with the per-language SDK snippets injected as `x-codeSamples` (or `x-code-samples` for ReadMe) — at a stable URL your provider can point at: ``` https://api.glotto.dev/v1/spec/decorated//openapi.decorated.yml https://api.glotto.dev/v1/spec/decorated//openapi.decorated.json ``` Paste it into `mint.json` (or your provider's spec-URL field) **once**. The address never changes and is not tied to a spec revision: it always serves the most recent decorated document, so every regeneration reaches your docs provider with no CI step of your own. Pick the format by **extension** — the URL determines whether you get YAML or JSON, so the same link always means the same thing. Responses carry a strong `ETag`, so a provider that polls gets a cheap `304 Not Modified` until the document actually changes. This is **opt-in per project and off by default**: until you enable it, the URL returns `404`, and a project that has not opted in is indistinguishable from one that does not exist. Turn it on in your project config: ```yaml hosted_spec: enabled: true ``` The document is served without authentication — that is what lets a docs provider fetch it — so enable it only for a spec you are comfortable publishing. The source-of-truth template lives in `templates/docs-site-template/`; the customer can override any Astro/React component. ### Getting started Source: https://glotto.dev/docs/getting-started/ Turn one API spec into SDKs, docs, and an MCP server with Glotto. Glotto turns a single API spec into idiomatic SDKs across 13 languages (mobile-first), a docs site you own, and a multi-mode MCP server — provably correct and in lockstep with your spec. You describe your API and outputs in a `glotto.yml`, point it at an OpenAPI / AsyncAPI / GraphQL spec, and run the `glotto` CLI. #### A minimal project A Glotto project is a `glotto.yml` plus a spec file: ```yaml organization: name: Petstore contact: api@petstore.example openapi: source: ./petstore.openapi.yaml environments: production: https://api.petstore.example targets: typescript: {} resources: pets: methods: listPets: get /pets ``` #### The workflow > **Glotto is in private beta.** `@glotto/cli` is not on the public npm registry yet, and sign-in is > by invitation, so the install below will not resolve for you today. The workflow is real and is > what shipping looks like once you have access — it is written out here so you can see what you are > signing up for, not so you can run it this afternoon. Ask for an invitation at > [hello@glotto.dev](mailto:hello@glotto.dev). The published `@glotto/cli` is a **thin client** — code generation runs server-side in the Glotto control plane, so you authenticate once, then generate. A fresh install to first SDKs: ```sh npm install -g @glotto/cli # the thin client (engines run server-side) glotto login # authenticate to the control plane (device flow) glotto init # scaffold glotto.yml + spec/openapi.yaml # …edit glotto.yml and spec/openapi.yaml to describe your API… glotto generate # validate the config, then generate SDKs into ./sdks ``` - **`glotto login`** authenticates to the control plane and stores a token under `~/.glotto/`. `glotto generate` needs it because generation runs server-side. - **`glotto init`** scaffolds a starter `glotto.yml` and `spec/openapi.yaml`. - **`glotto generate`** first validates your `glotto.yml`, then writes SDKs for every configured target into `./sdks` (override with `--out`, or limit with `--target typescript`). Because it validates first, there is no separate check step in the happy path — a config error stops generation with the same diagnostics. Prefer to keep your editor honest? `glotto schema` prints the `glotto.yml` JSON Schema you can wire into a `# yaml-language-server: $schema=…` header. These four commands — `login`, `init`, `schema`, `generate` (plus `logout` and `mcp`) — are what the published CLI ships. See the [CLI reference](/docs/cli) for every command, its flags, and which distribution provides it, and the [glotto.yml reference](/docs/glotto-yml) for the full configuration schema. ### The Glotto IR Source: https://glotto.dev/docs/glotto-ir/ GlottoIR is the intermediate representation every codegen target consumes — the single contract between spec ingest and the language engines. **GlottoIR** is Glotto's intermediate representation: the normalized, language-neutral model of your API that every codegen engine consumes. Spec ingest produces it once, and all 21 `@glotto/codegen-*` engines read the *same* `GlottoIR` — which is why the TypeScript, Python, Go, Swift, … SDKs stay structurally consistent. #### Where it comes from `buildGlottoIR` runs over the **canonical spec** (the byte-stable, normalized OpenAPI / AsyncAPI / GraphQL document produced by spec ingest) and applies the `transforms` from your `glotto.yml`. The output is a `GlottoIR` value; codegen never reads raw OpenAPI. #### What it contains - **Resources** — a tree (with `subresources`), derived from path segments, that becomes nested clients like `client.pets.photos`. - **Methods** — per operation: `http_method`, `path`, `parameters`, `request_body`, `responses`, the detection flags `paginated` / `streaming` / `polling`, `sample_inputs`, and the captured prose (`summary` / `description` / `x-glotto-mcp-prompt`). - **Models** — named types, including any `discriminator` for `oneOf` polymorphism. - **Auth**, **environments**, and **snippet seeds** at the top level. #### Transforms `glotto.yml#/transforms` reshape an imperfect spec without editing the source — `rename_schema`, `flatten_composition`, `dedupe_inline_objects`, `extract_ref`, `fix_invalid_example`. Each is **fail-fast**: a transform whose target no longer exists raises a diagnostic instead of silently doing nothing. ### glotto.yml API surface Source: https://glotto.dev/docs/glotto-yml-api-surface/ Control which operations Glotto emits, aliases, streaming behavior, and query serialization. #### API surface ##### `skip` & `only` ```yaml skip: # exclude an operation from specific targets (by operationId) createWidget: [go, java] only: # emit an operation ONLY for the listed targets internalPing: [typescript] ``` Per-operation target exclusion, keyed by `operationId`. An operationId may not appear in both. `skip` and `only` are **per target language**. To withhold an endpoint from *everything* Glotto generates — including your docs site and your MCP server — use [`exclude`](https://glotto.dev/docs/glotto-yml-api-surface/#exclude) instead. ##### `exclude` ```yaml exclude: # withhold an operation from EVERY generated artifact - createInternalAudit # by operationId... - "post /internal/import" # ...or by position (Stainless's spelling) ``` An entry addresses its operation either by `operationId` or by its **position** — `" "`, the spelling Stainless's `unspecified_endpoints` uses. The two are interchangeable: excluding an operation by position produces byte-identical output to excluding it by name. The verb is case-insensitive; the path is compared exactly, written as the template your spec declares (`/pets/{petId}`). Position is the only spelling available for an operation whose `operationId` your spec omits, since the name Glotto synthesizes for it appears nowhere you can read. The target-agnostic deny-list. Where `skip` removes an operation from the SDKs you name, `exclude` removes it from every artifact in the run: all 13 SDKs, the Terraform provider, the CLI and graph targets, the docs site (its reference pages, sitemap, `llms.txt`, search index **and** the co-served `openapi.json`, across every documented version), and the generated MCP server — where the operation is registered as no tool and gets no handler. Reach for `exclude` rather than listing every target in `skip`: an `exclude` entry stays correct when you add a target later, whereas an enumerated `skip` list silently re-exposes the endpoint the day a new target joins. Notes: - An operation named here is treated as if your spec never declared it, so an [`aliases`](https://glotto.dev/docs/glotto-yml-api-surface/#aliases--deprecated) entry pointing at it does **not** materialize — an alias can't resurrect an excluded endpoint. To withhold an alias, remove its `aliases` entry. - An operation may not be named by both `exclude` and `only` (contradictory) — and that holds however each side spells it, so `exclude: ["post /pets"]` alongside `only: { createPet: … }` is reported just as naming `createPet` on both sides is. Appearing in both `exclude` and `skip` is fine — `exclude` simply subsumes the narrower entry. - An entry naming an `operationId` your spec doesn't carry is ignored, so a config can outlive a spec change. - `exclude` is a deny-list, not an allow-list: operations you don't name keep shipping. A new endpoint added to your spec is published until you exclude it. Migrating from Stainless? This is the counterpart to `unspecified_endpoints`. ##### `client_methods` ```yaml client_methods: - getStatus # client.getStatus() instead of client.system.getStatus() - ping ``` A list of `operationId`s to hoist onto the **client root** so they hang directly off the client (e.g. `client.getStatus()`), rather than under a resource. Handy for health/status endpoints that don't belong to a resource. Absent → no client-level methods. ##### `aliases` & `deprecated` ```yaml aliases: createRecord: upsertRecord # keep the old method, routed to the new operation createExport: # …and when the PATH moved too, say where it used to be target: createExportV2 path: /v1/exports deprecated: createRecord: # a plain string works too: `createRecord: Use upsertRecord.` default: Use upsertRecord instead. python: Use upsert_record() instead. ``` `aliases` maps a method name you want to keep exposing → the `operationId` it should resolve to, so renaming or re-versioning an endpoint doesn't break your users' call sites — and Breaking-change detection stops reporting the rename as breaking. A plain string is enough when the rename leaves the path alone. When the path moves — and with it the resource Glotto derives from it — use the `{ target, path }` form, where `path` is where the **superseded** method was served; it places the method on the surface your users already call, and never changes what it requests. Both of its fields are required. `deprecated` maps an `operationId` → the migration message, rendered in each language's own construct (`@deprecated`, `@Deprecated`, `[Obsolete]`, `#[deprecated]`, `// Deprecated:`, …). The object form's `default` is required; the other keys are per-target overrides. Both absent → unchanged output. See [Endpoint migration](/docs/endpoint-migration) for the full workflow. ##### `streaming` ```yaml streaming: on_event: - { data: "[DONE]", action: done } # terminate the stream on this sentinel - { event_type: error, action: fatal_error } - { fallthrough: true, action: skip } # drop anything else unrecognized, keep going ``` Termination / error / tolerance handlers for SSE & NDJSON stream iterators. Each rule has exactly one matcher — `data` (the event payload equals this sentinel), `event_type` (the SSE `event:` name; `null` matches untyped events), or `fallthrough: true` (catch-all) — and an `action`: `break` (stop), `done` (clean end), `fatal_error` (raise), or `skip` (drop the matched event and continue). `skip` is the opt-in counterpart to the strict-by-default decode: because rules match the **raw** payload before any JSON parse, a `skip`-matched frame is dropped *before* it would be decoded, so a `fallthrough: skip` (or a targeted `data: skip`) lets you tolerate benign non-JSON noise — keepalive / control frames, vendor sentinels — without the stream surfacing a parse error. With no `skip` rule, an unrecognized non-JSON frame still surfaces the parse error (the strict default is unchanged). ```yaml streaming: dual_mode: createChatCompletion: # operationId param_discriminator: stream # required — the request field selecting the mode stream_event_model: ChatCompletionChunk # optional — the per-event model params_type_name: ChatCompletionParams # optional — names the shared params model method_suffix: streaming # optional — default "streaming" ``` `dual_mode` handles the **one endpoint, two modes** shape every major AI API uses: a request field selects between a streamed sequence and a single buffered body, and the two return different types. Glotto emits **two methods** from the one operation — `createChatCompletion` (buffered) and `createChatCompletionStreaming` — removes the discriminator from both signatures, and pins the right value on the wire for each. Optional/additive: with no `dual_mode` block, output is unchanged. The per-event type is read from the spec when the endpoint documents a streaming media type alongside `application/json`; otherwise name it with `stream_event_model`. An entry that can't be applied is reported as `GLOTTO_CONFIG_DUAL_MODE_REFUSED` rather than silently ignored. See [Streaming → Dual-mode endpoints](/docs/streaming#dual-mode-endpoints-stream-true). ##### `query_settings` ```yaml query_settings: array_format: comma # or repeat (default) ``` How an **array-typed query parameter** is serialized onto the request URL: `repeat` (`?tags=a&tags=b`, the OpenAPI `form`/`explode` default) or `comma` (`?tags=a,b`). A single, client-wide policy; defaults to `repeat`. ### glotto.yml client behavior Source: https://glotto.dev/docs/glotto-yml-client-behavior/ Configure generated client runtime behavior, transforms, naming, casing, and parameters. #### Optional keys ##### `client_settings` ```yaml client_settings: default_timeout: 60s # per-request overall timeout (default: 30s) retry: max_attempts: 3 initial_delay: 200ms max_delay: 8s jitter: true max_elapsed: 90s # optional overall wall-clock retry deadline auth: scheme: bearer env_var: ACME_API_KEY header_prefix: Token # Authorization: Token (default: Bearer) schemes: # per-scheme overrides, keyed by OpenAPI scheme name AdminKey: env_var: ACME_ADMIN_KEY DpopAuth: header_prefix: DPoP # Authorization: DPoP for this scheme only idempotency: true # or { enabled: true, header: X-Idempotency-Key } telemetry_headers: true # send X-Glotto-Retry-Count + X-Glotto-Timeout ``` SDK runtime defaults. `default_timeout` bounds each request attempt in every generated SDK — a duration string (`60s`, `500ms`), defaulting to 30 seconds when unset; see [Retries & timeouts](/docs/retries) for how per-call and per-client overrides compose with it. See [Retries & timeouts](/docs/retries) for the `retry` block, [Authentication](/docs/authentication) for `auth`, and [Idempotency keys](/docs/idempotency-key) for `idempotency` (a boolean, or an object that customizes the injected header name). `telemetry_headers` (off by default) makes every request carry `X-Glotto-Retry-Count` and `X-Glotto-Timeout`, so your server can see the client's retry state and timeout budget. `auth.scheme` / `auth.env_var` / `auth.header_prefix` describe the API's *single* security scheme. When your spec declares several — a default token plus a per-endpoint admin key, say — `auth.schemes` sets the same knobs per scheme, keyed by the scheme name as written in the OpenAPI `securitySchemes`. `env_var` names the environment variable that scheme's credential falls back to, and `header_prefix` sets its `Authorization` keyword; both are optional, and a scheme you don't list keeps the defaults. `header_prefix` applies to `bearer` and `oauth2` schemes — the ones with an `Authorization` keyword to vary — and is ignored on `apikey`, `basic`, and `custom`. The two levels combine, and the two keys reach different schemes on purpose: - **`auth.header_prefix`** applies to **every** `bearer`/`oauth2` scheme, so an API that frames all its bearer credentials as `Token` needs one line rather than an entry per scheme. - **`auth.env_var`** applies only to the schemes in your spec's **global** `security` requirement. A per-endpoint scheme is a different credential, so it is never given the default scheme's environment variable — name its own under `auth.schemes` instead. A per-scheme value always wins over the top-level one, so `header_prefix: Token` alongside `schemes.DpopAuth.header_prefix: DPoP` gives `DpopAuth` the `DPoP` keyword and every other bearer-style scheme `Token`. ##### `default_request_options` Use resource defaults for related calls and method defaults for an exception such as a slow export: ```yaml resources: exports: default_request_options: headers: X-Export-Route: archive timeout: 30s methods: slowExport: endpoint: get /exports/{exportId} default_request_options: timeout: 120s max_retries: 0 ``` Resources, nested subresources and object-form methods accept `headers`, `timeout` and `max_retries`. A timeout must be a positive duration. The retry count must be a nonnegative integer; zero disables retries. Header names are matched without regard to case, and duplicate names in one declaration are rejected. Each option resolves from the per-call override, then the method, nearest resource, client and generated default. Headers merge across those levels; a more specific value replaces the same header without discarding unrelated headers. A nested resource inherits values it does not set. For SSE, NDJSON and binary downloads, the timeout bounds opening through the first byte or EOF; it does not terminate an established stream. Cancellation and explicit close remain available. These settings affect generated SDK requests. They do not alter the API specification or its breaking-change classification. Each declaration must address an existing endpoint; a resource with defaults must contain method declarations, directly or beneath a subresource. ##### `transforms` ```yaml transforms: - rename_schema: { from: InvoiceDTO, to: Invoice } - dedupe_inline_objects: { threshold: 2 } ``` An ordered list of in-config OpenAPI rewrites. See the [Transforms reference](/docs/transforms) for every transform and its arguments. ##### `naming` & `parameter_naming` ```yaml naming: # per-language member renames: model → wire property → target → identifier Widget: public: { java: isPublic } User.address: # dot-path: rename a NESTED inline-object property (User.address.zipCode) zipCode: { python: zip_code } parameter_naming: # per-language parameter renames: operation → wire param → target → identifier listWidgets: class: { python: cls } ``` Rename a model property (`naming`) or a method parameter (`parameter_naming`) for a specific target language when the wire name collides with that language's keywords — the wire name is preserved for serialization. A `naming` key may be a **dot-path** (`Model.field[.field…]`) to address a property on a *nested* inline object: the first segment is a named model and each later segment is an object field (or an array-of-object field) that resolves to a deeper object. So `User.address` with property `zipCode` renames `User.address.zipCode` without naming the generator's synthesized model. A path that doesn't resolve against the current spec is ignored (like an unknown plain model), so a config can outlive a schema change. ##### `custom_casings` ```yaml custom_casings: # declared initialisms: lowercase word → how to render it api: API # getApiKey → getAPIKey id: ID # widgetId → widgetID url: URL ``` Declare the initialisms your API uses, so generated identifiers render them the way your team writes them rather than the way a naive word-splitter would. Identifier words are lowercased before matching, so the key is always the **lowercase** form — `api`, never `API`. The rendering must be a pure **re-casing** of the key: only letter case may differ. Changing the word itself is a rename, and belongs in [`naming`](https://glotto.dev/docs/glotto-yml-client-behavior/#naming--parameter_naming) (model members) or [`parameter_naming`](https://glotto.dev/docs/glotto-yml-client-behavior/#naming--parameter_naming) (method parameters). A key that is not a lowercase alphanumeric word, or a rendering that is not a re-casing of its key, is reported as [`GLOTTO_CONFIG_CUSTOM_CASINGS`](/docs/diagnostics). **It does not reach every identifier yet.** Casings are applied where the IR carries a per-language identifier — model members, method parameters, and enum constants — and **not** to method names, class and type names, or resource accessors. Declaring `{ api: API }` and still reading `getApiKey` is that gap, not a mistake in your config; Glotto reports it as [`GLOTTO_CONFIG_CASING_NOT_APPLIED`](/docs/diagnostics) rather than letting you discover it in the emitted SDK. ##### `positional_params` ```yaml positional_params: # per-language argument order: operation → target → wire param names getRepo: typescript: [repoId, orgId] # getRepo(repoId, orgId, options?) instead of (orgId, repoId, …) go: [repoId] # PARTIAL: name the first, the rest keep their derived order ``` Override the order a method's positional **path** arguments are emitted in, for one target language. Useful when your API reads more naturally in another order, or when you are migrating from a hand-written SDK whose signature your users already call. Name the parameters by their **wire** name (the `{placeholder}` in the path), not the emitted identifier — so one entry means the same thing in every language, and it keeps working if you also rename the parameter with `parameter_naming`. The list is a **prefix**: any path parameter you do not name keeps its derived position behind the ones you do, so naming just the first argument is a complete declaration. It is **per target language** on purpose, because the languages genuinely differ — Ruby's arguments are keyword arguments, Go leads with `ctx` and Elixir with `client`, and Swift labels every argument at the call site. Reordering never changes the request: the URL, query string, and headers are built from the wire names and are byte-identical whichever order you declare. An entry naming something that is not one of that operation's path parameters — or naming one twice — is reported and the whole entry is ignored, so the method keeps its derived order rather than a half-applied one you never reviewed. Reordering the request **body** relative to the path arguments is not supported yet. ##### `enum_naming` ```yaml enum_naming: # per-enum-value renames: enum model → wire value → identifier StatusCode: "200": Ok # one LOGICAL name, cased per language (Rust `Ok`, Java `OK`, Swift `ok`) "404": NotFound Kind: "in-progress": { java: RUNNING } # or pin the identifier per target ``` Rename the constant an enum **value** is emitted as. The wire value is always preserved for serialization — only the language identifier changes. A value key is the value's string form, so a numeric `200` is written `"200"`. Most specs need no entry here: values whose derived identifier would be illegal in a target language — a leading digit (`2xx`, `200`), punctuation (`in-progress`, `n/a`), or a language keyword — are **repaired automatically**, so generation always produces compiling code. Use `enum_naming` when you want a *meaningful* name (`Ok`) rather than the derived one. The bare-string form is one logical name that each language cases idiomatically; the object form pins an exact identifier for specific targets. `elixir`, `typescript`, and `react_native` emit no enum constant (they use string lists and literal unions), so entries for them have no effect. ### glotto.yml model shaping Source: https://glotto.dev/docs/glotto-yml-model-shaping/ Shape generated models, enum types, required inputs, and auto-populated values. #### Model shaping ##### `models` ```yaml models: WidgetsCreateWidgetRequestAddress: name: Address # rename the model, in every target and every artifact Coordinates: name: { go: Coords } # or pin the name per target TinyWrapper: inline: true # don't emit a standalone type — inline it at each reference DebugInfo: inline: { go: true } # or un-promote it for specific targets only ``` Shape the **models** Glotto emits. Two directives, each taking a single value (applying everywhere) or an object keyed by target: - **`name`** renames a model. This is the escape hatch for the names Glotto *synthesizes*: an inline object schema — say an `address` property on a request body — is promoted to a named model so every language can type it, and its name is derived from where it sits in the spec (`WidgetsCreateWidgetRequestAddress`). Rename it to whatever reads well. Only the emitted type name changes; the JSON payload is untouched. - **`inline`** does the opposite: it un-promotes a model, substituting its body wherever it's referenced and emitting no standalone type. Useful for a small one-off wrapper that isn't worth a named type of its own. A name set as a single value applies to **every** artifact — the SDKs, the docs site, the MCP server, the mock server and the Terraform provider all show it — so one model has one name everywhere. The per-target object form is applied per SDK instead, leaving the shared name in place elsewhere. An entry naming a model your spec doesn't produce is ignored, so a config can outlive a schema change. A few directives are declined so generation stays correct: renaming onto a name another model already holds (it would merge two types), and inlining a model that refers back to itself or that is a member of a discriminated union. In each case the model simply stays as it was — and both `glotto generate` tells you so, naming the directive's path and why it was declined ([`GLOTTO_CONFIG_MODEL_SHAPING_REFUSED`](/docs/diagnostics-config#config-diagnostics)). Migrating from Stainless? Glotto translates `x-stainless-model` and `x-stainless-model-skip` into this block for you. ###### Renaming a model for one language `name`'s object form is also where you settle a **name collision with the SDK's own code**. Some type names are unusable in a given language because the generated SDK already refers to them: a model called `Data` in a Swift SDK would land in the same module as the client's own `Data` references and take them over, and Swift offers no way to disambiguate a same-module declaration. Glotto handles this for you. A colliding model is emitted under a suffixed name — `Data` → `DataModel`, and `DataModel2` if that is taken too — in **that target only**, with every reference updated to match, and `glotto generate` prints a [`GLOTTO_CONFIG_MODEL_RENAMED`](/docs/diagnostics-config#config-diagnostics) warning saying which name it chose. Your other SDKs keep the original name. To choose the name yourself instead, pin it per target — an explicit name always wins, and the warning stops: ```yaml models: Data: name: { swift: Payload } # Swift emits `Payload`; every other target still emits `Data` ``` The warning opens with the exact key to set, so you can read it straight out of the output — and it stays the key **you** wrote even if another `models` entry has already renamed that model. There is no separate `model_names` block: this is that setting. Use `transforms` with `rename_schema` if you want the model renamed in *every* target and artifact rather than just one. ##### `enums` ```yaml enums: Status: nominal: false # emit a plain alias over the primitive, not a named type Kind: nominal: { go: false } # or just for specific targets ``` Choose an enum's **typing shape**: its own named type, or a plain alias over the primitive it's carried by. By default Glotto gives an enum its own type in every language that can express one — a `#[non_exhaustive]` enum in Rust, a `RawRepresentable` struct in Swift, an `extension type` in Dart, a `StrEnum` subclass in Python (an `IntEnum`, or a float-backed enum, when the enum's values are numbers), a defined `type Status string` in Go. Set `nominal: false` and it becomes an alias instead (`pub type Status = String`, `typealias Status = String`, `type Status = string`, …), over whichever primitive the enum is actually carried by — so a numeric enum aliases to `i64` / `Int` / `int` / Python's `int` (or `float` for decimal values), not to a string type. Reach for it when a named type gets in the way rather than helping: your callers would rather pass a plain string than import and construct a type; you're moving from a hand-written SDK where the field *was* a string and promoting it would break your users; or the enum's members change often, so the named type churns your public API on every regeneration while an alias doesn't. The trade-off is worth stating plainly: **an alias has no members**, so you give up the generated constants (`Status.active`, `Status::Active`) and callers write the raw string. Nothing changes on the wire — the value is the same primitive either way — so switching shape is not an API re-versioning event. This applies to the six targets whose language has a type-alias construct: **Go, Rust, Swift, Dart, Kotlin and Python**. Java, PHP, C# and Ruby have no alias construct to emit, and TypeScript, React Native and Elixir have no nominal form to choose against (TypeScript is structurally typed, so its emitted `type Status = 'active' | 'archived'` is already an alias). `glotto generate` warns if you address one of them per target. `nominal: true` states the default explicitly. It's accepted and carried, but it doesn't change emission: where a named type is available it's already what you get, and where one isn't — a **mixed** enum (say `['a', 1, true]`, which has no single primitive to be typed over) or a **boolean** one in Rust, Swift or Dart — Glotto declines rather than inventing a shape it can't stand behind. Migrating from Stainless? Glotto translates `x-stainless-nominal` into this block. Note the defaults are opposite — Stainless aliases by default, Glotto names by default — and only enums you actually annotated are translated, so review the ones you didn't. ##### `soft_required` ```yaml soft_required: listInvoices: parameters: [account_id] # the SDK method demands this query/header parameter createInvoice: body: true # …and this operation's request body body_fields: [currency] # …and this field of the request-body model ``` Mark inputs your API *accepts* without, but that every real caller should send. The generated SDK method **demands** them — a Python keyword with no default rather than `= None`, a Go `string` value rather than a `*string`, a TypeScript member without its `?` — so the ergonomics guide the caller toward the call you actually want. What makes this different from editing `required` in your spec is everything it *doesn't* touch. Your docs reference still lists the parameter as optional, the generated MCP server still marks it optional in its tool schema, the generated mock server still accepts a request that omits it, and Breaking-change detection reports **nothing** — because none of those describe the SDK signature, they describe your protocol, and your protocol hasn't changed. Adding an entry here is never a breaking change. Entries are keyed by `operationId`, and one naming an operation, parameter, or field your spec doesn't carry is ignored, so a config can outlive a schema change. One case is handled on a copy so the generated SDK stays correct: a `body_fields` entry on a model your API also **returns**. Requiredness there is the same flag the SDK's decoder reads, so demanding the field on the shared type would make responses that omit it fail to parse — a preference turned into a runtime error on data you don't control. Instead the SDK gains a request variant of that model, named `Request`, and only the operation you named uses it: ```ts export interface Invoice { id: string; memo?: string } // what you receive export interface InvoiceRequest { id: string; memo: string } // what createInvoice demands createInvoice(params: InvoiceRequest): Promise ``` Every other reference — responses, nested fields, and other operations' bodies — keeps using the original model, so nothing that parses your API's output changes. If the name is already taken, the variant becomes `Request_2`; renaming the model itself with the [`models`](https://glotto.dev/docs/glotto-yml-model-shaping/#models) block carries through (`Invoice` → `Bill` yields `BillRequest`). Because that adds a type you didn't name, `glotto generate` tells you it happened, as a [`GLOTTO_CONFIG_SOFT_REQUIRED_SPLIT`](/docs/diagnostics-config#config-diagnostics) warning naming the operation, the field, the shared model and the variant — so a new SDK type never appears unexplained. (An entry your spec doesn't carry stays silently ignored, as above: that's the stale-config case, not a promotion that moved.) Migrating from Stainless? Glotto translates `x-stainless-soft-required` into this block for you. ##### `auto_populate` ```yaml auto_populate: createCustomer: parameters: [api_version] # the SDK sends this query/header parameter itself body_fields: [object] # …and this field of the request body ``` The inverse of [`soft_required`](https://glotto.dev/docs/glotto-yml-model-shaping/#soft_required). Where that block **adds** an input your protocol doesn't demand, this one **removes** an input your protocol *does* demand — because there is only one value it could ever hold, and the SDK can supply it. The shape this is for is the Stripe-style discriminant: `object: { type: string, enum: ["customer"] }` on a request body, or a pinned `api_version` query parameter. The schema permits exactly one value, and every caller types it at every call site anyway. Name the position here and the generated method drops it from its signature, while the generated request still sends it: ```ts // before createCustomer({ object: 'customer', email: 'a@b.com' }) // after createCustomer({ email: 'a@b.com' }) ``` Entries are keyed by `operationId`. `parameters` names `query` and `header` parameters by their wire name; `body_fields` names properties of the request-body object. All 13 languages honour it, and in each one the value is injected at the same place the per-call `extraQuery` / `extraHeaders` / `extraBody` escape hatch merges — so a caller who passes an explicit value for the same key **still wins**. The constant is the base, never an override. Like `soft_required`, this changes only the SDK signature. Your docs reference still documents the field, the generated MCP server still exposes it, the generated mock still accepts it, and Breaking-change detection reports **nothing** — none of those describe the SDK signature, they describe your protocol, and your protocol hasn't changed. A position is only eligible if its schema permits exactly one value with a sendable wire form. Naming one that doesn't — a multi-value enum, a `null`-only schema, a field of a non-object body — is reported by `glotto generate` as a [`GLOTTO_CONFIG_AUTO_POPULATE_REFUSED`](/docs/diagnostics-config#config-diagnostics) warning naming the position and the rule it failed, rather than being silently skipped. (An entry naming an operation or field your spec doesn't carry stays silently ignored, as elsewhere: that's the stale-config case, not a directive that was declined.) One case is handled on a copy. If the request body resolves to a model your API also **returns**, or that another operation sends without asking for the same removal, removing the field in place would be lossy — the response type would lose a field the server still sends, or the other operation's signature would lose an input with nothing to re-inject it. Instead the SDK derives a `Request` variant carrying the removal, and only the operations you configured point at it. Unlike the `soft_required` split there is nothing to announce: the field is *gone* from the signature either way, so the variant never appears in a call site. Migrating from Stainless? Glotto translates `x-stainless-const` into this block for you. ### glotto.yml project settings Source: https://glotto.dev/docs/glotto-yml-project-settings/ Configure diagnostics, code owners, MCP, generated docs, licensing, releases, and hosted binding. #### Project settings ##### `diagnostics` ```yaml diagnostics: rules: no-error-response: error # remap a lint rule's severity (off | warn | error) mutation-no-idempotency-key: off # suppress a rule max_warnings: 10 # release-gate: linting fails above this budget ``` Tunes spec linting without touching the rules engine: `rules` overrides per-rule severity (`off` suppresses), and `max_warnings` is a release-gating threshold (lint exits non-zero once warnings exceed it). Optional and additive. See the [Diagnostics reference](/docs/diagnostics-config#tuning-diagnostics) for the full rule list and behavior. ##### `settings` ```yaml settings: detect_breaking_changes: true ``` Stainless-parity toggles. `detect_breaking_changes` makes [breaking-change detection](/docs/breaking-changes) call out every breaking change at the top of the preview comment on your pull request, rather than leaving it in a table cell. Optional and additive. ##### `code_owners` ```yaml code_owners: '*': - '@acme/sdk-team' 'src/**': - '@acme/core-team' - '@octocat' ``` Declares who reviews changes to the repos Glotto manages. Optional and additive — set nothing and your output is byte-for-byte what it was. Rules are emitted **in the order you declare them**, because a CODEOWNERS file is resolved *last-match-wins*: the rule furthest down that matches a path is the one that owns it. So the block above gives the SDK team everything and hands `src/**` to the core team and `@octocat`. **Owner syntax is per-provider**, resolved from each target's [`repo_provider`](/docs/glotto-yml#targets) and defaulting to `github`: | Provider | Owners it accepts | |---|---| | `github` | `@login` (letters, digits and single interior hyphens, ≤39 characters), `@org/team-slug` at **exactly** two segments, or an email address. | | `gitlab` | The same, plus `_` and `.` inside a segment, and `@group/subgroup/…` nested to any depth. | | `bitbucket` | A workspace member's handle, account id, uuid, or display name — resolved against your workspace when you apply it. | | `azure-repos` | An identity's display name, unique name, or email — resolved against your organization when you apply it. | A GitHub team handle is not a GitLab one, and an owner that a forge cannot resolve makes it ignore the **whole line** — the path is left unowned and nothing says so. That is why Glotto rejects an owner the target's provider could not resolve rather than emitting it ([`GLOTTO_CONFIG_CODE_OWNERS`](/docs/diagnostics-config#config-diagnostics)). **Two of the four providers read no CODEOWNERS file at all.** GitHub and GitLab receive `.github/CODEOWNERS` and `.gitlab/CODEOWNERS` from `glotto generate`; Bitbucket configures a repo-wide default-reviewer set and Azure Repos a required-reviewers branch policy, both through their APIs — so for those two, Glotto applies the block by calling the provider rather than by writing a file into your repository. It authenticates with the credentials you connected the repository with; see [Connect your VCS](/docs/vcs-connect). Bitbucket's default reviewers have **no path scoping**, so the result names which of your patterns had to be flattened, and any owner whose directory matched nothing, rather than reporting a plain success. A target with a [`repo_path`](/docs/glotto-yml#targets) — an SDK living in a subtree of a shared repo — receives no file either: GitHub and GitLab read CODEOWNERS only from the repository **root**, so one written into the subtree would never be opened. Every one of these cases is reported by name, by `glotto generate`, as [`GLOTTO_CONFIG_CODE_OWNERS_NOT_APPLIED`](/docs/diagnostics-config#config-diagnostics). ##### `mcp` ```yaml mcp: package_name: "@acme/api-mcp" # emitted package identity (default: -mcp; a valid npm name) registry_name: io.github.acme/api # opt-in: emit a server.json for the official MCP registry modes: [code, tools, dynamic] # MCP server modes to enable (omit for all three) filters_enabled: true search_docs: true operations: # per-tool overrides (canonical _ keys) pets_get: { description: "Fetch a single pet by id.", name: fetch_pet } permissions: # method allow/block sets (convenience layer, NOT a security boundary) allow_http_gets: true # allow every operation mapped to HTTP GET allowed_methods: [pets\..*] # regexes over the qualified . name blocked_methods: [pets\.delete] # applied last — beats both allow keys experimental: async_tasks: false # EXPERIMENTAL: task-capable tools for long-running operations (default off) ``` Configuration for the generated multi-mode MCP server. See the [MCP server guide](/docs/mcp-server). `permissions` narrows which operations the emitted server can reach: patterns match the fully-qualified `.` name (`pets.photos.add`) and are fully anchored, the allow set is constrained only when `allowed_methods` or `allow_http_gets` is set, and `blocked_methods` is subtracted last. Denied operations are never registered in Tools/Dynamic Mode, and Code Mode's `execute` refuses submitted code that references one before running it. It is a **convenience layer that keeps an agent in its lane, not a security boundary** — the Code Mode half is static analysis, which obfuscation defeats; use a scoped API token or the MCP Cloud gateway for the real boundary. See [Method permissions](/docs/mcp-server#method-permissions). `experimental.async_tasks` opts the operations your spec marks long-running into the MCP **async tasks** capability — see [Experimental: async tasks](/docs/mcp-server#experimental-async-tasks). It is experimental and off by default — and currently **withheld**: the final 2026-07-28 MCP spec moved tasks to the `io.modelcontextprotocol/tasks` extension, and until the emission is rebuilt for that shape, enabling the flag over a long-running-capable API fails generation with an actionable error. ##### `docs` ```yaml docs: theme: { primary: "#5b21b6" } authoring: { format: mdx, base_path: ./docs } deploy: { target: cloudflare, custom_domain: docs.acme.com } ask_ai: { endpoint: https://ask.acme.com } # docs "Ask AI" widget analytics: { posthog_key: phc_yourkey } # cookieless analytics (opt-in; PostHog or Plausible) search: # rename and order the generated search facets facets: - { filter: method, label: HTTP method } - { filter: resource, label: API resource } i18n: { default_locale: en, locales: [en, de] } # site locales (routing + lang + chrome strings) structured_data: {} # schema.org JSON-LD on every page (opt-in) versions: [ { slug: v2, default: true, openapi: { source: ./v2.yaml } } ] ``` The generated docs site: theme, authoring, deploy target (`cloudflare` / `vercel` / `netlify` / `static` / `self-host`), the Ask-AI widget, opt-in analytics, locales, structured data, and multi-version docs. `search.facets` controls the names and display order of the generated search filter panel. Each entry's `filter` is one of `method`, `resource`, `language`, or `parameter`; `label` is optional, and the list order is the display order. A filter you omit keeps its default name and appears after the configured filters. Omitting `search` preserves the default generated site unchanged. `structured_data` is off unless you set it (`structured_data: { enabled: false }` turns it back off explicitly). With it on, every generated page carries one schema.org JSON-LD block derived from your spec and your configured site identity — an `APIReference` per operation with the same title, description, and canonical URL its `` already advertises, a `SoftwareSourceCode` per code sample you show, breadcrumbs matching the page's place in the navigation, and `WebSite` + `Organization` on the home page. It is re-derived on every regeneration, so it cannot drift from your spec the way hand-maintained markup does. For the things your spec cannot know — your organization's `sameAs` profiles, a `HowTo` on a guide, a locale-prefixed route — add your own nodes: ```yaml docs: structured_data: extra_nodes: # appended to every page - "@type": Organization "@id": https://acme.com/#org sameAs: ["https://github.com/acme"] extra_nodes_by_route: # appended to one route only /guides/quickstart: - "@type": HowTo name: Quickstart ``` A node is free-form: whatever keys you write are emitted as authored, inside the same single JSON-LD document, escaped the same way the derived nodes are. What Glotto does check is the node's **shape** — that it is a non-empty object, that it carries a `@type` a consumer can dispatch on, and that `@id`/`@context` hold the forms JSON-LD defines for them. A node that fails one of those, or that claims an `@id` belonging to a node Glotto derived from your spec, is reported by `glotto generate` ([`GLOTTO_CONFIG_DOCS_STRUCTURED_DATA`](/docs/diagnostics)) and left out; your other nodes still emit. This is a structural check, not a vocabulary one — a misspelled `@type` is emitted as you wrote it. The merge is **additive** — your nodes follow the derived ones and cannot replace, reorder, or remove them, so the graph Glotto guarantees stays the graph Glotto guarantees. Route keys are matched leniently (`guides/quickstart`, `/guides/quickstart/` and `/guides/quickstart` are the same route), and a route that isn't a generated page still gets its nodes, which is how you reach locale-prefixed routes. Set neither key and your site is byte-identical to the derived-only output; set `enabled: false` and the whole feature is off, injected nodes included. `deploy.custom_domain` is the bare hostname — `docs.acme.com`, not a URL, and no path, port, or wildcard — because it becomes both your canonical URLs' authority and the domain handed to your deploy provider ([`GLOTTO_CONFIG_DOCS_DEPLOY_DOMAIN`](/docs/diagnostics)). Each `versions[].slug` becomes a `//` route, so it may only use characters a URL path keeps verbatim ([`GLOTTO_CONFIG_DOCS_VERSIONS`](/docs/diagnostics)). Colours under `og_images.template.colors` are painted by the card renderer rather than a browser, so hex, comma-separated `rgb()`/`rgba()`, and CSS named colours work but `hsl()` and gradients do not ([`GLOTTO_CONFIG_DOCS_OG_TEMPLATE`](/docs/diagnostics)). `glotto generate` reports each of these. Each `versions` entry declares its snapshot with **exactly one** of `openapi`, `asyncapi`, or `graphql` — the same three input keys the top level accepts, in the same shapes, so a GraphQL snapshot carries its own `operations` document: ```yaml docs: versions: - slug: v1 graphql: { source: ./v1.graphql, operations: ./v1-operations.graphql } - slug: v2 default: true openapi: { source: ./v2.yaml } ``` Versions may mix input kinds — the case a migration produces — and each version's reference pages are built from its own snapshot. Only an `openapi` version co-serves an `openapi.json` (and so contributes a `service-desc` link to the site's API catalog); the others simply serve none. Versioning is docs-only: your SDKs always come from the top-level source. `analytics` activates the site's built-in cookieless analytics against your own project — PostHog by default (`posthog_key` is the publishable client key; optional `posthog_host` selects EU cloud or self-hosted ingestion) or Plausible via `provider: plausible` + `plausible_domain` (optional `plausible_host` for self-hosted). It counts page views plus three product events — search queries, feedback votes, and playground calls — with no cookies, Do-Not-Track honored, and nothing sent beyond each event's named fields (never feedback comment text, parameter values, or credentials). `i18n` declares the site's locales: `default_locale` sets the `` and stays at the unprefixed routes, additional `locales` get `//` route prefixes for your translated pages plus a generated chrome string catalog to translate (`src/lib/ui-strings.mjs`). ##### `license` ```yaml license: MIT # or Apache-2.0 ``` The permissive license stamped onto generated SDKs and emitted templates. Defaults to `MIT`. ##### `release` ```yaml release: mode: auto # or manual ``` Controls the release flow for generated SDKs. `mode` is the **only** key this block accepts. Any other is a configuration error reported against your `glotto.yml` before anything is generated, so a file naming branches here fails at the door rather than being silently ignored. Absent, releases behave as `auto`: Glotto opens the release pull request for you, and merging it publishes. `manual` stops Glotto opening that pull request — you open it yourself from the branch Glotto has already pushed. Publishing on merge is an `auto`-only behaviour: the release webhook does not act on a repository whose project is in `manual` mode. **The branch names are not a `glotto.yml` key.** To move one of them, use [`targets..release`](/docs/glotto-yml#targets), which takes `branch`, `baseline_branch` and `base_branch` per target: ```yaml targets: typescript: repo: acme/acme-typescript release: branch: sdk-next # the release PR's head — the merged result baseline_branch: sdk-generated # the pristine generator output (the merge base) base_branch: trunk # the release PR's base — released code ``` Unset, each defaults to Glotto's [`generated` → `next` → `main`](/docs/multi-vcs-release) vocabulary. A **project-level** set of the same three names exists, but it lives in the project's control-plane configuration (`PUT /v1/projects/:id/config`) rather than in your repository's `glotto.yml`. Setting them once for every target from `glotto.yml` is not supported yet. One of those three carries a second meaning worth knowing before you set it: the project-level `base_branch` is both **the branch Glotto watches on your spec repository** and **the base of the pull request opened on a split-layout target**. Moving it changes which pushes trigger a release *and* where the release lands — set it when a repository's trunk is not called `main`, not to retarget one of the two. The staging repositories Glotto hosts are not affected by any of these names. Their three branches are created with the repository and are always `generated`, `next` and `main`; every build pushes its pristine output there whatever your targets are called. ##### `readme` ```yaml readme: example_requests: headline: listPets # operationId fronting the README quickstart pagination: listInvoices # operationId fronting the Pagination section streaming: streamEvents # operationId fronting the Streaming section # or, with example-value overrides (wire parameter names): # headline: # operation: updatePet # params: { petId: p_42, notify: true } ``` Configures the `README.md` shipped inside each generated SDK — every SDK language target ships one. `example_requests.headline` selects the operation fronting the README's usage example by `operationId` — as a plain string, or as `{ operation, params }` where `params` overrides the example values rendered in the quickstart, keyed by wire parameter name (path and query parameters and top-level request-body fields). `example_requests.pagination` and `example_requests.streaming` select, in the same two forms, the operations fronting the README's **Pagination** and **Streaming** sections (the named operation must actually paginate / stream; a slot's `params` apply only to its own section's example). Absent slots default to the first matching operation, with example values sampled from the spec's `example`/`default` fields. `glotto generate` warns when an operation or a params key doesn't match the spec, or when a `pagination`/`streaming` operation isn't of that kind. The README's code blocks are the same generated snippets the docs site renders, so they always match the current client surface. ##### `hosted` ```yaml hosted: project: prj_9f2c81 ``` Binds the repository to its Glotto control-plane project. Hosted mode resolves the project as `--project`, then `GLOTTO_PROJECT_ID`, then this committed value — so a checkout with only the API token in the environment can publish check-results to the hosted run. The project id is not a secret; credentials (`GLOTTO_API_TOKEN`) and the API URL stay environment-only and have no config key. Optional and additive, and ignored by generation entirely. ### glotto.yml reference Source: https://glotto.dev/docs/glotto-yml/ The complete glotto.yml configuration reference — every key, its shape, and accepted values. `glotto.yml` is the single config for a project: it describes your organization, the API spec to read, the SDK targets to emit, your API surface, and how the generated clients behave. This reference documents every key across this overview and the linked optional-reference pages. Where a key has its own page, it links there rather than repeating detail. #### Editor validation Generate the JSON Schema for `glotto.yml` and reference it from the file for inline completion and validation in VS Code, Cursor, Neovim, and other editors with the YAML language server: ```bash glotto schema --out glotto.schema.json ``` ```yaml # yaml-language-server: $schema=./glotto.schema.json organization: name: Acme # … ``` `glotto schema` (no flags) prints the schema to stdout instead. It's the same byte-stable schema core-config exports as `glottoConfigJsonSchema()`. --- #### Required keys ##### `organization` ```yaml organization: name: Acme contact: support@acme.com homepage: https://acme.com # optional # optional: security metadata → a SECURITY.md in every generated repo security_contact: security@acme.com security_policy_url: https://acme.com/security security_policy_terms: Reports are acknowledged within two business days. ``` `name` and `contact` are required; `homepage` is optional. ###### Security metadata → `SECURITY.md` Setting **any** of `security_contact`, `security_policy_url`, or `security_policy_terms` makes `glotto generate` write a `SECURITY.md` into every generated repo — the file GitHub's "Report a vulnerability" affordance keys off, so a researcher who finds an issue in your SDK has a documented route to you. Set none of them and nothing is emitted; your output is byte-for-byte what it was. Each key contributes one section, and an unset key contributes none: | Key | Section it adds | |---|---| | `security_contact` | **Reporting a Vulnerability** — the address, plus a note directing researchers away from public issue trackers. Must be an email address. | | `security_policy_url` | **Disclosure Policy** — a link to your published policy, named with `organization.name`. Must be an absolute `http`/`https` URL. | | `security_policy_terms` | **Additional Terms** — your prose, verbatim. Free text, no grammar. | The document is deterministic: it embeds no date or other ambient value, so regenerating an unchanged config reproduces it byte-for-byte. ##### Input source — exactly one of `openapi` / `asyncapi` / `graphql` ```yaml openapi: source: ./spec/openapi.yaml # a local path or an http(s) URL code_samples: formats: [x-codeSamples, readme] # optional: code-sample formats to emit ``` `source` accepts a **string** — a local path (resolved against the directory holding your `glotto.yml`, so it means the same thing wherever you run Glotto from) or an `http(s)://` URL — or one of three structured objects: `command` (run your own exporter), `introspect` (read a framework's source directly), or `git` (below). `asyncapi` takes a `source`; `graphql` takes a `source` plus optional `operations` and `autogenerate`. Exactly one input source must be present. **The same source forms apply to all three inputs**, and to `graphql`'s second document: a git-hosted AsyncAPI channel document, or a GraphQL schema produced by your own exporter, is written exactly the way the OpenAPI equivalent is. ```yaml asyncapi: source: git: { repo: https://github.com/acme/api-definitions.git, ref: main, path: events/chat.yaml } # or graphql: source: { command: npm, args: [run, print-schema], output: { kind: stdout } } operations: ./operations.graphql ``` The one exception is `introspect`, which is accepted on **`openapi.source` only**: introspection reads a framework's source and *synthesizes an OpenAPI document*, so it cannot be what an `asyncapi` or `graphql` key means. Writing it anywhere else is a `glotto generate` error ([`GLOTTO_CONFIG_INTROSPECT_SOURCE_OPENAPI_ONLY`](/docs/diagnostics)). ###### Specs in another repository When your OpenAPI document lives in a **different git repository** — the usual shape when a separate API-definition repo produces the spec — point `source` at it directly instead of vendoring a copy you then have to keep in step by hand: ```yaml openapi: source: git: repo: https://github.com/acme/api-definitions.git ref: main # a branch, tag, or commit sha path: openapi/petstore.yaml # relative to the repository root ``` All three keys are required. `path` is resolved inside the cloned repository, never against your `glotto.yml` — a path escaping the repository is rejected. Glotto shallow-clones `repo` at `ref` on each generation, reads that one file, and removes the clone; authentication uses your ambient git configuration. The same `{ git: … }` object is accepted wherever a spec or overlay is referenced — `asyncapi.source`, `graphql.source`, `graphql.operations`, every `docs.versions[]` snapshot, `docs.changelog.previous_spec.source`, and an [`apply_overlay`](/docs/transforms) `source`. Because `ref` can be a moving branch, the [verification report](/docs/verification-report) records the exact bytes it read, under the machine-independent origin `git:#:`. ###### Multi-file specs Glotto ingests a **single** OpenAPI document. If your spec is split across multiple files (`$ref`s into sibling files — common for large APIs and a frequent migration case), bundle it into one document first. Glotto does not merge multiple sources itself, so the bundling stays in your control — there are two ways to wire it: **Pre-bundle** — run a bundler as a build step and point `source` at the result: ```sh redocly bundle ./openapi/main.yaml -o ./openapi/bundled.yaml ``` ```yaml openapi: source: ./openapi/bundled.yaml ``` **Bundle inline** — let Glotto run the bundler at generate time via the `command` form of `source`; its stdout (or a file it writes) is ingested directly, with no committed intermediate: ```yaml openapi: source: command: redocly args: [bundle, ./openapi/main.yaml] output: { kind: stdout } # or { kind: file, path: ./.glotto/bundled.yaml } ``` ##### `environments` ```yaml environments: production: https://api.acme.com staging: https://staging-api.acme.com ``` A map of environment name → base URL, with at least one entry. ##### `default_environment` ```yaml environments: dev: https://dev.acme.com production: https://api.acme.com default_environment: production ``` Which environment everything Glotto emits points at when it needs **one** base URL — your SDK clients and their README quickstarts, the code samples on your docs site, your docs playground, and your MCP server's upstream. Optional; it must name one of the keys under [`environments`](https://glotto.dev/docs/glotto-yml/#environments). Leave it out and Glotto resolves the default for you, in this order: 1. an environment named `production`, if you declared one — the name `glotto init` writes; 2. otherwise the first environment **by sorted name**. That second rule exists so your generated code depends only on what your config *says*, never on the order you happened to type it in. It is a determinism rule, not a guess at what you meant — so when it is what decides the answer (two or more environments, none of them named `production`, none declared), `glotto generate` tells you, with [`GLOTTO_CONFIG_DEFAULT_ENVIRONMENT_INFERRED`](/docs/diagnostics). Naming an environment that isn't declared is an error, [`GLOTTO_CONFIG_DEFAULT_ENVIRONMENT`](/docs/diagnostics). ##### `targets` ```yaml targets: typescript: package_name: "@acme/sdk" react_native: package_name: "@acme/sdk-react-native" secure_storage: keychain # or 'mmkv', 'expo' expo_plugin: true # optional explicit opt-in for the public ./plugin subpath csharp: package_name: Acme.Commerce # NuGet PackageId (publish identity) namespace: Acme.Commerce # C# namespace (code identity) go: namespace: github.com/acme/commerce-go # go.mod module path java: namespace: com.acme.commerce # Java package + Gradle group dart: secureStorage: true # emit the flutter_secure_storage token-store adapter python: {} ``` A map of language slug → per-target config. Every target accepts: - `package_name` — the **published/registry** identity stamped on the manifest (npm name, PyPI name, NuGet ``, …). - `namespace` — the SDK's **code** identity, interpreted idiomatically per engine: the C# `namespace`/``, Java/Kotlin `package` (+ source directory + Gradle `group`), Go `go.mod` module path, PHP PSR-4 namespace, Ruby/Elixir top-level module, Python's import package (dots nest into directories, so `acme.commerce` gives `from acme.commerce import Client`), and Rust's Cargo `[lib] name` (a lib name is a flat identifier, so dots collapse — `acme.commerce` gives `use acme_commerce`). Optional — absent means each engine's default (`Glotto` / `com.glotto` / `glotto_sdk`). Engines whose code identity *is* the published package — `typescript`/`react_native` (npm name), `dart`, `swift` — have no separate code identity to set: `package_name` is the only knob, and setting `namespace` on one of them warns, [`GLOTTO_CONFIG_NAMESPACE_NOT_APPLIED`](/docs/diagnostics), rather than being silently ignored. - `repo` — the target's **release repo** (`owner/name`; Azure Repos `org/project/repo`), with an optional `repo_provider` (`github`, `gitlab`, `bitbucket`, or `azure-repos`) naming the VCS host. Used by the hosted platform's targets sync, which derives each project's SDK-target records from this file — where release PRs and tags for that language's SDK go. Optional — omit both for registry-only targets; `repo_provider` requires `repo`. - `repo_path` — for a **monorepo** release repo: the repo-relative directory this language's SDK lives under (e.g. `sdks/typescript`). Several targets may share one `repo` when each names a distinct `repo_path` — each gets its own subtree, release branch, and release PR. Optional (requires `repo`) — absent means the SDK owns the repo root, the one-repo-per-SDK layout. - `release` — this target's branch names, overriding the project-level [`release`](/docs/glotto-yml-project-settings#release) block one name at a time. Three optional keys, all defaulting to Glotto's [`generated` → `next` → `main`](/docs/multi-vcs-release) vocabulary: ```yaml targets: typescript: repo: acme/acme-typescript release: branch: sdk-next # the release PR's head — the merged result baseline_branch: sdk-generated # the pristine generator output (the merge base) base_branch: trunk # the release PR's base — released code ``` `branch` and `baseline_branch` must differ — one branch cannot be both the merged result and the base it merges against. Two targets releasing to the same `repo` may not name the same `branch`. Use `base_branch` when a repository's released branch is not called `main`: Glotto refuses to open the PR rather than guessing at a branch you never named. - `variants` — **[`spec_repo`](/docs/spec-repo) only**: which documents the published spec repo carries. A non-empty list of `base` (your spec as supplied, `exclude`-pruned), `with_transforms` (after every [`transforms`](/docs/transforms) correction — the document your SDKs were generated from), and `with_code_samples` (with per-operation SDK samples embedded as `x-codeSamples`). Optional — absent means all three. An unknown member, and a member listed twice, are both `glotto generate` errors rather than being ignored or deduplicated: a variant you asked for and did not get is never silently dropped. Asking for one your input cannot produce fails loudly — see [which variants each input can publish](/docs/spec-repo#which-variants-your-input-can-publish). - `formats` — **`spec_repo` only**: the serializations each variant is published in. A non-empty list of `yaml` and `json`, same duplicate/unknown-member rules. Optional — absent means `yaml` alone. Emitted file names are a function of the selection's content, never of the order you listed it in. React Native additionally accepts `secure_storage` and `expo_plugin`. The Expo plugin is emitted by default only when the API has an intrinsic plugin capability. An API without OAuth or bearer-style/secure-storage auth capability—for example, one using only custom or API-key auth—has no `./plugin` export by default. Set `expo_plugin: true` to opt that target in explicitly, then ##### `resources` ```yaml resources: invoices: models: invoice: '#/components/schemas/Invoice' methods: createInvoice: post /v1/invoices getInvoice: get /v1/invoices/{id} listInvoices: { endpoint: get /v1/invoices, paginated: cursor } subresources: line_items: methods: addInvoiceLine: post /v1/invoices/{invoice_id}/lines ``` Your API surface, grouped into resources. A method is either a `"VERB /path"` string or an object `{ endpoint, paginated?, streaming?, polling?, default_request_options? }`. `resources` recurse through `subresources` arbitrarily deep. Set [`default_request_options`](/docs/glotto-yml-client-behavior#default_request_options) on a resource or method to configure routing headers, attempt timeouts and retry limits for its calls. > **The method key declares the emitted method name.** Writing `list: get /v1/invoices` emits > `client.invoices.list()` in every language, whatever the operation's `operationId` says. Declare > nothing and the emitted name is the `operationId` exactly as before — naming is opt-in per > operation, so a config with no `methods` keys produces a byte-identical SDK. > > The **resource tree itself is still derived from your spec's paths**: the key names the *method*, > not the resource, and never moves a method between resources. > > Two things are refused rather than guessed, and both stop `glotto generate` > ([`GLOTTO_CONFIG_RESOURCE_METHOD_NAME`](/docs/diagnostics)) — a name you asked for and did not get > is never silently dropped: > > - an endpoint matching no operation in your spec (the verb and path must match **exactly**, > placeholder names included); > - two operations on one resource ending up with the same method name. Glotto names both and stops, > rather than picking a winner or appending a suffix — you wrote both names, so you hold the fix. > > Renaming a method your customers already call is an **API-compatible** change if you say so: keep > the old name working with [`aliases`](/docs/glotto-yml-api-surface#aliases--deprecated) and mark > it with [`deprecated`](/docs/glotto-yml-api-surface#aliases--deprecated), > and breaking-change detection reports the rename as non-breaking. --- #### Optional-key reference pages Optional settings are grouped by the part of the generated surface they shape: - [Client behavior](/docs/glotto-yml-client-behavior) — client runtime defaults, transforms, naming, custom casings, positional parameters, and enum naming. - [Model shaping](/docs/glotto-yml-model-shaping) — model names and inlining, enum types, soft-required inputs, and auto-populated values. - [API surface](/docs/glotto-yml-api-surface) — target filtering, global exclusion, client methods, aliases and deprecations, streaming, and query serialization. - [Project settings](/docs/glotto-yml-project-settings) — diagnostics, project-wide settings, code owners, MCP, generated docs, licensing, releases, README examples, and hosted binding. --- See the [CLI reference](/docs/cli) for the commands that read this config, the [pipeline](/docs/pipeline) concept for how it's consumed, or [Getting started](/docs/getting-started) for a walkthrough. ### Go Source: https://glotto.dev/docs/go/ An idiomatic Go module: context.Context-first methods, pull pagination, typed errors for errors.As, and unknown response fields kept on decode. The Go SDK is an idiomatic, context-aware client emitted from the same `GlottoIR` as every other target. It ships as a buildable Go module with a `go.mod`, so `go get` then construct a client and call methods. #### Quickstart ```bash go get github.com/your-org/petstore ``` `github.com/your-org/petstore` is a placeholder: replace it with the generated module path for your SDK. ```go ctx := context.Background() client := petstore.NewClient(petstore.WithToken(os.Getenv("PETSTORE_TOKEN"))) // a single call pet, err := client.Pets.CreatePet(ctx, &petstore.NewPet{Name: "Rex"}) if err != nil { return err } // Pull iteration stops fetching immediately when the loop ends. for pet, err := range client.Pets.ListPetsIter(ctx) { if err != nil { return err } fmt.Println(pet.Name) } ``` #### Context-aware methods Every method takes a `context.Context` first argument — `client.Pets.CreatePet(ctx, body)` — so cancellation, deadlines, and request-scoped values flow through the call the idiomatic Go way. #### Pagination Use the `Iter` companion to range over typed values and errors. It fetches on demand; breaking the loop releases its work before returning. The original `<-chan Result[T]` methods remain available, but callers must cancel their context when abandoning a channel. See [Pagination](/docs/pagination). Manual `Page` companions fetch one typed page at a time. `Response()` decodes the full wrapper without another request, and `NextPage()` retains request options. See [manual pagination](/docs/pagination). #### Retries & backoff Per-call `RequestOption` values override headers, timeouts, retry counts and idempotency. Methods without an existing option slot expose compatible `WithOptions` companions. See [request options and defaults](/docs/retries). Transient failures (`5xx`, `429`, transport errors) retry with exponential backoff and jitter; the policy is configurable per client and honors the request context's deadline. See [Retries & timeouts](/docs/retries). #### SSE streaming Server-sent-event endpoints return a typed event stream decoded from the `text/event-stream` framing. Their `Iter` companions close the response on a range-loop break; NDJSON endpoints use the same pull API. Establishment shares buffered retry, idempotency and telemetry policy. `WithRequestTimeout` bounds each opening attempt through the first body byte, then stops. The context or an explicit `WithStreamTimeout` can bound the established stream. WebSocket connectors also expose a `WithContext` companion; HTTP 101 ends their opening timeout. See [Streaming](/docs/streaming). #### Webhooks Use the standalone `VerifyWebhook` helper in your HTTP handler to verify an inbound delivery's raw body before trusting it. See [Webhooks](/docs/webhooks). #### Polling Pollable operations receive a `Poll` companion, and `WaitUntil` covers custom predicates; both respect context cancellation and deadlines. See [Polling](/docs/polling). #### Telemetry Pass `WithHooks(&TelemetryHooks{…})` when constructing the client to observe requests, responses, errors, and retries for your logs, metrics, or traces. See [Telemetry hooks](/docs/telemetry). #### Typed errors Non-2xx responses return typed error values carrying the parsed error body; use `errors.As` to narrow to the concrete type rather than matching on status codes. See [Errors](/docs/errors). #### Unknown response fields Your API can add a response field without it being a breaking change — but a closed Go struct would drop it, and `encoding/json` discards what it doesn't recognize. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through `ExtraFields()`, and written back out when the model is re-serialized. ```go pet, _ := client.Pets.CreatePet(ctx, body) // A field your API started returning after this SDK was generated. if raw, ok := pet.ExtraFields()["species"]; ok { var species string _ = json.Unmarshal(raw, &species) } // Re-encoding preserves it — a read-modify-write never silently drops it. body, _ := json.Marshal(pet) ``` `ExtraFields()` returns `map[string]json.RawMessage`, so nested objects and arrays survive intact, and retention is recursive: a nested model keeps its own unknown fields. A payload whose fields are all known encodes exactly as before, declared-field order included. The accessor returns a snapshot: changing its map or `json.RawMessage` bytes does not change the model or a later request. To send a field your spec does not model yet, use the per-call extra-body escape hatch. ##### Updating retained timestamps A timestamp the native `time.Time` carrier cannot represent remains available through `RetainedTemporal()` and survives re-encoding. Assigning a valid `*time.Time` to the typed field sends your replacement. Use the generated `Set(value *time.Time)` method to update both the field and retained state; passing `nil` explicitly sends JSON null, including for an optional field. For example, a timestamp field named `At` has `SetAt(nil)` unless that method name needs a collision suffix. The method's generated documentation gives its exact spelling. A normal struct copy preserves untouched retained values. Calling a timestamp setter on that copy leaves the original's retained state unchanged. Assigning nil directly leaves the existing retained wire value available, so use the setter when you intend to clear it. #### Binary downloads Binary endpoints return an owned `BinaryResponse` with response metadata. `ReadAll(limit)` bounds delivered bytes; `WriteTo` copies to an `io.Writer`. Both close the response. Close it explicitly after abandoning incremental reads. Mixed JSON/binary operations expose an explicit result branch. See [file transfers](/docs/file-transfers). #### File uploads `multipart/form-data` operations assemble the body with a `mime/multipart.Writer` for you, and an `application/octet-stream` body is sent raw — so binary uploads work without hand-building the multipart payload. ### The API-surface graph Source: https://glotto.dev/docs/graph/ A machine-readable graph of your API surface: operations, models, auth schemes, events and the typed edges between them, in one deterministic graph.json. Your spec already describes an entity graph — resources contain operations, operations accept and return models, models reference other models, auth schemes gate calls, endpoints stream events. But nothing in an OpenAPI document lets a tool *traverse* it. Answering "which operations return a `Charge`?", "what events does this endpoint stream?", or "which scopes gate this resource?" means reading generated code or prose. The `graph` target projects the [Glotto IR](/docs/glotto-ir) into a single machine-readable document — `graph.json` — that answers those questions directly. It is a **derivation, not a description**: an edge exists if and only if your spec says so, which is what makes it safe to hand to an agent. There is nothing in it to hallucinate. Enable it in `glotto.yml`: ```yaml targets: graph: {} ``` `glotto generate` then writes `/graph/graph.json`. The target takes no options — the graph is selected or it isn't. #### The envelope Every artifact carries a format discriminator and a vocabulary version, so a consumer can refuse a document it doesn't understand rather than mis-reading one: ```json { "format": "glotto-surface-graph", "version": 1, "nodes": [], "edges": [] } ``` `version` is bumped when the node/edge vocabulary changes in a breaking way; additive changes keep it stable. #### Nodes Every node carries a stable `:` id, so a reference to it survives regeneration as long as the underlying entity keeps its name: | Kind | One per | Notable attributes | |---|---|---| | `resource` | resource, recursively — subresources get dotted names | — | | `operation` | resource method **and** hoisted client method | `http_method`, `path`, `summary`, and the `pagination` / `streaming` / `polling` / `idempotency` strategies when the operation declares them | | `model` | named model | `type` | | `security_scheme` | declared scheme — plus a synthesized `default` when your API has a single scheme rather than a registry | `scheme` | | `environment` | declared environment | `url` | | `tag` | operation tag | — | | `channel` | AsyncAPI channel | — | #### Edges Edges are typed, and each carries the attribute that makes it specific — which status returned the model, which content type accepted it, which scopes the call requires. **Structure.** `has_subresource` (resource → resource), `has_operation` (resource → operation), `has_model` (resource → model, with the `accessor` name), and `tagged` (operation → tag). **Data flow.** `accepts` (operation → request-body model, with `content_type`), `returns` (operation → model, one per response status, with `status`, resolved transitively through inline schemas), and `references` (parameter schema → model, with the parameter name). **Auth.** `secured_by` (operation → security scheme) resolves exactly the way the generated SDKs do — a method's own security if it declares one, otherwise the client default — and carries the required scopes. An operation your spec marks public (`security: []`) has no edge, which is itself the answer to "what can be called unauthenticated?". **Events and pagination.** `streams` (operation → event model, one per declared event type, with the wire `event` value) and `paginates_over` (operation → the item model your pages are made of). **Models.** `references` (model → model, for refs reachable through fields, items, values and union members) and `has_variant` (union → member model, with the discriminator's `wire_value` when your spec maps one). AsyncAPI channels contribute `sends` and `receives` edges to their message models. #### A worked example A two-operation petstore, with a bearer-authenticated `list` and `create` over one `Pet` model, emits: ```json { "format": "glotto-surface-graph", "version": 1, "nodes": [ { "id": "environment:production", "kind": "environment", "name": "production", "attributes": { "url": "https://api.example.com" } }, { "id": "model:Pet", "kind": "model", "name": "Pet", "attributes": { "type": "object" } }, { "id": "operation:pets.createPet", "kind": "operation", "name": "pets.createPet", "attributes": { "http_method": "post", "path": "/pets", "summary": "Create a pet" } }, { "id": "operation:pets.listPets", "kind": "operation", "name": "pets.listPets", "attributes": { "http_method": "get", "path": "/pets", "summary": "List pets" } }, { "id": "resource:pets", "kind": "resource", "name": "pets" }, { "id": "security_scheme:default", "kind": "security_scheme", "name": "default", "attributes": { "scheme": "bearer" } }, { "id": "tag:pets", "kind": "tag", "name": "pets" } ], "edges": [ { "kind": "accepts", "from": "operation:pets.createPet", "to": "model:Pet", "attributes": { "content_type": "application/json" } }, { "kind": "has_model", "from": "resource:pets", "to": "model:Pet", "attributes": { "accessor": "Pet" } }, { "kind": "has_operation", "from": "resource:pets", "to": "operation:pets.createPet" }, { "kind": "has_operation", "from": "resource:pets", "to": "operation:pets.listPets" }, { "kind": "returns", "from": "operation:pets.createPet", "to": "model:Pet", "attributes": { "status": "201" } }, { "kind": "returns", "from": "operation:pets.listPets", "to": "model:Pet", "attributes": { "status": "200" } }, { "kind": "secured_by", "from": "operation:pets.createPet", "to": "security_scheme:default" }, { "kind": "secured_by", "from": "operation:pets.listPets", "to": "security_scheme:default" } ] } ``` "Which operations return a `Pet`?" is every `returns` edge whose `to` is `model:Pet`. "What does creating a pet require?" is the `accepts` edge plus the `secured_by` edge. Multi-hop questions — "which resources expose an operation that streams an event carrying this model?" — are a walk, not a search. #### What it's for The artifact is deliberately a plain document with no query engine, so anything that reads JSON can consume it: - **Agents and MCP servers** — ground a tool-using model in what your API actually exposes, with no chance of it inventing a field, a scope, or an endpoint that doesn't exist. - **Internal tooling** — impact analysis ("what breaks if this model changes?"), coverage reports, API-surface review at a scale nobody eyeballs. - **Retrieval** — answer support and docs questions by traversing relations rather than matching text. #### Kept true, not just generated Like every Glotto artifact, the graph is a deterministic projection of your spec: nodes sorted by id, edges by a total order, duplicate edges collapsed, byte-stable across repeated emission and locked by golden tests. It is regenerated in lockstep with your SDKs, docs site and MCP server — from the same IR, so it cannot describe a surface they don't implement — and the [drift gate](/docs/drift-detection) proves the committed copy never lags your spec. That is the point. Drawing a graph of an API once is easy; the hard part, and the part that decays, is keeping it true forever. Add an operation, tighten a scope, add an event type — the graph is provably current on the next regeneration. ### Idempotency keys Source: https://glotto.dev/docs/idempotency-key/ Glotto treats the Idempotency-Key as a first-class config concept — auto-injected on mutations whose spec advertises it, so retries are safe. Glotto treats the **`Idempotency-Key`** header as a first-class concept, so retrying a mutation can't accidentally create two of something. When an operation's spec advertises the header, the generated SDK **auto-injects a fresh UUID** on each `POST`, and the server dedupes on it. #### How it interacts with retries Glotto ships built-in retries — but deliberately *not* blind ones: - **Safe methods** (`GET` / `HEAD`) plus `5xx` and retryable transport errors are retried with exponential backoff + jitter, out of the box. - **Mutations** are retried only when their OpenAPI spec advertises `Idempotency-Key` (so the server can dedupe a replayed request). Mutations without the advertised header are not retried by default — configurable via `client_settings`. This pairs Stainless-style "retries out of the box" with the safer "don't retry blindly" discipline: the SDK auto-injects the key, callers (and the SDK) can retry, and the server dedupes. ### Idempotency Source: https://glotto.dev/docs/idempotency/ Glotto auto-injects a fresh Idempotency-Key UUID on writes, so a retried call can't create two resources — per operation, or for every mutation. When the API advertises an `Idempotency-Key` header, the client **auto-injects a fresh UUID** on each `POST`. That makes a write safe to retry: the server treats a repeat with the same key as the same operation, so a network blip can't create two resources. ```ts const client = new Client({ idempotency: true }); await client.pets.createPet({ name: 'Rex' }); // sends a generated Idempotency-Key ``` This is why the [retry policy](/docs/retries) will retry idempotent mutations but leaves non-idempotent ones alone — the key is what makes the retry safe. Each call gets a fresh key, and that same key is reused across the client's own retries of that call — so the write applies at most once. There are two ways an operation becomes idempotent-aware: - **Spec-advertised (automatic).** When an operation documents an `Idempotency-Key` header parameter (or sets `x-idempotent: true` in the spec), the generator flags that method — it injects the key and is retried even with no client configuration at all. Setting `x-idempotent: false` opts an operation out. Shipped today in the TypeScript and React Native SDKs; rolling out to the other languages. - **Client-level (`idempotency: true`).** The constructor flag above turns on injection and mutation retries for **every** mutation, in every SDK — useful when the spec doesn't document the header but the API supports it. ### Java Source: https://glotto.dev/docs/java/ A buildable Java library with Gson models, resource sub-clients, CompletableFuture async twins, and unchecked ApiError exceptions — nothing to wrap. The Java SDK is an idiomatic, buildable Java library emitted from the same `GlottoIR` as every other target. It ships with Gson models and an SDK-managed, Jetty-backed HTTP transport. Existing `java.net.http.HttpClient` injection remains supported. Add the artifact, construct a `Client`, and call methods — no checked exceptions to wrap. #### Quickstart ```gradle implementation 'com.your-org:petstore:1.0.0' ``` ```java import com.your_org.petstore.Client; Client client = new Client(System.getenv("PETSTORE_TOKEN")); // resources are reached through an accessor call var pet = client.pets().createPet(newPet); // paging is a Stream — bind it, then drain var pets = client.pets().listPets(); pets.forEach(System.out::println); ``` #### Resource sub-clients Operations hang off resource accessors rather than a flat method list: `client.pets().get(id)`, `client.pets().photos().add(...)` — the same service-accessor shape the other engines emit — and each method carries Javadoc generated from the operation prose in the spec. #### Unchecked errors Non-2xx responses throw `ApiError`, an unchecked `RuntimeException` carrying the parsed, typed error body, so callers narrow with `instanceof` rather than being forced into `try/catch` for a checked `IOException`. See [Errors](/docs/errors). #### Async twins Every buffered operation has a `CompletableFuture` async twin alongside its blocking form, so the SDK fits both a synchronous caller and a non-blocking pipeline without a second client. #### Pagination List methods return a lazy walker over the API's pagination strategy (cursor, page, offset, or link-header), fetching each page as you iterate. See [Pagination](/docs/pagination). #### Retries & backoff Transient failures (`5xx`, `429`, transport errors) retry with exponential backoff and jitter, configurable per client. Discriminated-union response bodies resolve to the right concrete type. See [Retries & timeouts](/docs/retries), [Streaming](/docs/streaming), and [Authentication](/docs/authentication). #### Unknown response fields Your API can add a response field without it being a breaking change — but a generated record has no component to put it in, and Gson discards what it does not recognize. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized. ```java Pet pet = client.pets().createPet(body); // A field your API started returning after this SDK was generated. JsonElement species = pet.extraFields().get("species"); // Re-encoding preserves it — a read-modify-write never silently drops it. String json = client.gson().toJson(pet); ``` `extraFields()` is the record's own accessor and returns an immutable `Map`, so nested objects and arrays survive intact, and retention is recursive. Your existing construction still compiles: the models keep a constructor over their original component list, so `new Pet(id, name, tag, status, photoUrls)` is unchanged. The retained fields are read-only by design. To *send* a field your spec doesn't model yet, use the per-call extra-body escape hatch rather than writing to the retained bag. #### Pages, request controls, and files Paginated methods also expose [manual pages](/docs/pagination#fetch-one-page), so you can inspect one response and request its successor explicitly. Request overrides apply to the initial call and to each requested next page; see [Retries & timeouts](/docs/retries) for precedence and cancellation. [Binary downloads](/docs/file-transfers) return an owned byte response with metadata, a bounded read helper, and incremental consumption. Close the response when you stop early. Your generated README demonstrates these operations with your API's names and the language's native calling conventions. ### Kotlin Source: https://glotto.dev/docs/kotlin/ A coroutine-first Kotlin client: suspend functions, Flow streaming and pagination, sealed-type errors, plus Android-native and Multiplatform targets. The Kotlin SDK is a modern, coroutine-first client emitted from the same `GlottoIR` as every other target. It leans on Kotlin's defaults — primary-constructor config with default args, nullable `String?` params — and emits KDoc from the operation prose in the spec. #### Quickstart ```kotlin implementation("com.your-org:petstore:1.0.0") ``` ```kotlin import com.your_org.petstore.Client import kotlinx.coroutines.runBlocking fun main() = runBlocking { val client = Client(token = System.getenv("PETSTORE_TOKEN")) // methods are suspend functions val pet = client.pets.createPet(NewPet(name = "Rex")) // paginated methods are Flows client.pets.listPets().collect { println(it.name) } } ``` #### Resource sub-clients Operations hang off resource accessors — `client.pets.get(id)`, `client.pets.photos.add(...)` — rather than a flat method list, the same service-accessor shape the other engines emit. #### Coroutines Methods are `suspend` functions (`client.pets.get(id)` from a coroutine), with streaming and pagination surfaced as a `Flow`, so the SDK composes with structured concurrency rather than blocking a thread. Cancelling the calling coroutine aborts an active buffered HTTP request, including response-body reads, on JVM and Android. Cancellation propagates as `CancellationException` and is not retried. The Multiplatform client uses Ktor's coroutine cancellation support. #### Typed errors Non-2xx responses throw `ApiError`, a `RuntimeException` carrying the parsed, typed error body — Kotlin has no checked exceptions, so there is nothing to wrap. Discriminated-union bodies resolve to the right sealed-type variant. See [Errors](/docs/errors). #### Pagination Paginated list methods expose a `Flow` that walks every page as you collect it, advancing the cursor for you. See [Pagination](/docs/pagination). #### Retries & backoff Transient failures (`5xx`, `429`, transport errors) retry with exponential backoff and jitter, configurable per client. See [Retries & timeouts](/docs/retries), [Streaming](/docs/streaming), and [Authentication](/docs/authentication). #### Android-native > **Opt-in.** The Android target is compile-verified against a real Android SDK by the scheduled > `mobile-compile` CI lane — the emitted > library (including its OkHttp transport) builds with the real Android Gradle Plugin. The plain-JVM > Kotlin SDK above is the default and is unaffected. Opt in with `targets.kotlin.android: true` in your `glotto.yml` to emit an Android library (`com.android.library`) instead of the plain-JVM package — the same native-mobile integration the React Native SDK ships, adapted to Kotlin: ```yaml targets: kotlin: android: true ``` - **Secure token storage.** The client takes an optional `tokenStore: TokenStore?`; the bearer token is read from it per request (falling back to the static token). A Keystore-backed implementation is generated for you — `androidKeystoreTokenStore(context)` encrypts values with an AES-256-GCM key held in the Android Keystore (no third-party crypto dependency). - **Network-state-aware retries.** Pass a `ConnectivityMonitor` and the retry loop awaits the backoff and re-checks while the device is offline rather than burning the request; the generated `AndroidConnectivityMonitor` is backed by the system `ConnectivityManager`. - **Lifecycle-aware backoff.** Pass an `AppLifecycle` and the backoff pauses (no retry attempt consumed) while the app is backgrounded; the generated `AndroidAppLifecycle` is backed by `ProcessLifecycleOwner`. - **OkHttp transport.** The plain-JVM SDK's HTTP transport is the JDK's `java.net.http.HttpClient`, which is absent from `android.jar` — so the Android variant emits its entire transport (requests, retries, streaming, file upload, OAuth token exchange) on **OkHttp** instead, preserving the same retry/`Retry-After`/telemetry/idempotency behavior. OkHttp is added as a dependency automatically. The emitted `build.gradle.kts` carries the `android {}` block (with the Java/Kotlin JVM target pinned to 17) and the `androidx.lifecycle` / `okhttp` dependencies, and the `AndroidManifest.xml` declares the `INTERNET` and `ACCESS_NETWORK_STATE` permissions. The interfaces (`TokenStore`, `ConnectivityMonitor`, `AppLifecycle`) are plain Kotlin, so you can supply your own implementations or the bundled `androidx`-backed ones. ##### Biometric-gated storage > **Opt-in, experimental.** `targets.kotlin.androidBiometric: true` requires > `targets.kotlin.android: true` and is compile-verified on the scheduled `mobile-compile` lane. > The plain Keystore store above stays the > default and byte-identical. For credentials that must be released only after the user authenticates, add `targets.kotlin.androidBiometric: true`: ```yaml targets: kotlin: android: true androidBiometric: true ``` This additionally emits a `BiometricKeystoreTokenStore` (via `androidBiometricKeystoreTokenStore(activity, promptInfo)`) whose AES-256-GCM key is created with `setUserAuthenticationRequired(true)`, so every read and write is authorized through a `BiometricPrompt` (class-3 / STRONG biometrics) bound to the crypto operation — the plain `androidKeystoreTokenStore` above stays available. The emitted `build.gradle.kts` adds the `androidx.biometric` dependency and the `AndroidManifest.xml` declares the `USE_BIOMETRIC` permission. #### Unknown response fields Your API can add a response field without it being a breaking change — but a generated data class has no property to put it in, and Gson discards what it does not recognize. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized. ```kotlin val pet = client.pets.createPet(body) // A field your API started returning after this SDK was generated. val species = pet.extraFields()["species"] // Re-encoding preserves it — a read-modify-write never silently drops it. val json = client.gson.toJson(pet) ``` `extraFields()` returns a read-only `Map`, so nested objects and arrays survive intact, and retention is recursive. The Multiplatform target carries the same guarantee through kotlinx.serialization rather than Gson, so a `commonMain` consumer reads the same accessor. Your existing construction still compiles — the property is a trailing parameter with a default. The retained fields are read-only by design. To *send* a field your spec doesn't model yet, use the per-call extra-body escape hatch rather than writing to the retained bag. #### Kotlin Multiplatform (preview) > **Opt-in, phased.** The **entire shared client** now lives in `commonMain` on a **Ktor transport** — > the model layer (`kotlinx.serialization`, including discriminated unions via a polymorphic serializer), > the `Client` + retry loop, synchronous webhook verification (pure-Kotlin crypto), and the OAuth/PKCE > helpers are all platform-agnostic and gson-free, compile-verified on a real toolchain. Every API shape the > standard Kotlin SDK supports now also emits under the multiplatform target. Landed: the **iOS target** > (`iosX64`/`iosArm64`/`iosSimulatorArm64` on the Ktor Darwin/NSURLSession engine, compile-verified on a > macOS/Xcode CI runner) **including an iOS Keychain-backed secure token store**, and the **Android target** > — `multiplatform` now composes with `android` (a declared `androidTarget()` on the OkHttp engine + an > AndroidKeyStore secure token store), plus the opt-in **JS / Wasm / watchOS / tvOS** targets via > `targets.kotlin.multiplatformTargets`. Opt in with `targets.kotlin.multiplatform: true` to emit a **Kotlin Multiplatform** project instead of the single-module JVM library: ```yaml targets: kotlin: multiplatform: true ``` The engine emits a `kotlin("multiplatform")` `build.gradle.kts` with `jvm()` and the **iOS targets** (`iosX64` / `iosArm64` / `iosSimulatorArm64`), plus a shared `commonMain` source set holding the **whole** portable client: the `kotlinx.serialization` models (discriminated unions become a sealed interface with a polymorphic `KSerializer` that dispatches on the discriminator), the `Client` + retry loop on a **Ktor `HttpClient`** (the JVM target binds the **OkHttp** engine, iOS binds **Darwin/NSURLSession**; `java.net.http` is JVM-only and absent from Kotlin/Native), pure-Kotlin SHA-256/HMAC so webhook verification stays synchronous on every platform, and the OAuth/PKCE helpers (a `suspend` Ktor token exchange + a secure-random `expect`/`actual` for the PKCE verifier). The remaining `java.*` (time, UUID, env, URL-encoding) is handled by `kotlin.time` / `kotlin.uuid` / a common encoder plus a small `expect`/`actual` shim (JVM-backed by `System`/`SecureRandom`, iOS by `posix`/`Foundation`) — so the emitted client is gson-free and each platform source set carries only its HTTP engine + the platform actuals. The default (non-`multiplatform`) JVM SDK is unchanged. The iOS Kotlin/Native build is compile-verified on a scheduled macOS/Xcode CI runner. For secure credentials, the client takes an optional `tokenStore: TokenStore?` read per request (falling back to the static token); iOS ships a `KeychainTokenStore` backed by the system Keychain, and you can supply your own `TokenStore` on any platform. Setting `targets.kotlin.android: true` **alongside** `multiplatform` adds a declared `androidTarget()` (the OkHttp Ktor engine on `androidMain` + an AndroidKeyStore-backed `KeystoreTokenStore`) to the same KMP build — the android target is configured only on a machine with the Android SDK, so the `jvm()`/iOS build stays buildable without it. Because the `commonMain` client is fully platform-agnostic, you can opt into extra targets beyond the default JVM/iOS(+Android) set with `targets.kotlin.multiplatformTargets` — any of `js`, `wasmJs`, `watchos`, `tvos`: ```yaml targets: kotlin: multiplatform: true multiplatformTargets: [js, wasmJs, watchos, tvos] ``` `js` adds a `js(IR)` target and `wasmJs` a Kotlin/Wasm target, both on the Ktor **JS** engine (browser fetch / Node) with a `Platform.js.kt` / `Platform.wasmJs.kt` actual over the standard JS APIs (`process.env`, the JS `Date`, Web-Crypto `getRandomValues`); webhook verification stays synchronous there too (the crypto is pure Kotlin). `watchos` / `tvos` add the watchOS/tvOS targets, which reuse the iOS Darwin actuals (and Keychain) via a shared `appleMain` source set. Omit `multiplatformTargets` (or leave it empty) and the output is exactly the JVM/iOS(+Android) project above, byte-for-byte. JS + Wasm are compile-verified on Linux CI; the Apple targets on the macOS lane. Publishing works the same as the single-module Kotlin SDK: the generated multiplatform `build.gradle.kts` applies the `com.vanniktech.maven.publish` plugin, so publishing the Kotlin target (a single `publishAndReleaseToMavenCentral` Gradle task) ships the **root module + every per-target variant** (`-jvm`, `-android`, `-iosarm64`, `-js`, …) **and the Gradle Module Metadata** that lets a multiplatform consumer resolve the right artifact per target. #### Pages, request controls, and files Paginated methods also expose [manual pages](/docs/pagination#fetch-one-page), so you can inspect one response and request its successor explicitly. Request overrides apply to the initial call and to each requested next page; see [Retries & timeouts](/docs/retries) for precedence and cancellation. [Binary downloads](/docs/file-transfers) return an owned byte response with metadata, a bounded read helper, and incremental consumption. Close the response when you stop early. Your generated README demonstrates these operations with your API's names and the language's native calling conventions. ### The MCP server Source: https://glotto.dev/docs/mcp-server/ Glotto generates a multi-mode Model Context Protocol server — Tools Mode, Code Mode, and Dynamic Mode — that your API's consumers point their AI clients at. From the same [Glotto IR](/docs/glotto-ir), Glotto generates a **multi-mode MCP server** your customers ship, so AI clients (Claude Desktop, Claude Code, Cursor, …) can call your API. It's a TypeScript server built on the official MCP TypeScript SDK v2 (`@modelcontextprotocol/server`), speaking the **2026-07-28 MCP spec** — the stateless core (no protocol-level sessions, serverless/edge-friendly) with `server/discover`, per-request version negotiation, and the required standard request headers handled by the SDK. #### Three modes, one artifact Selected at server start (`--mode tools|code|dynamic`, `GLOTTO_MCP_MODE`) or per request on the HTTP transport (`?mode=`) — no regeneration: - **Tools Mode** (the default) — one MCP tool per API operation (`_`). Best for small, focused APIs. - **Code Mode** — exactly two tools, `execute` (runs TypeScript in a sandboxed isolate against your generated SDK, pre-authenticated — see *Transports & auth* below) + `search_docs`. Drastically reduces context use and supports chained calls — the competitive wedge for large APIs on code-capable agents. - **Dynamic Mode** — three meta-tools over the same operations: `list_tools` (a compact, schema-free menu, optionally narrowed by resource), `describe_tools` (full JSON-Schema input schemas on demand — the same fidelity Tools Mode registers), and `invoke_tool` (validates the arguments, then executes the identical request and result rendering as the per-operation tool, including its `jq_filter` projection). The honest fallback for **large APIs on agents that cannot execute code** — schemas arrive on demand instead of up front, so context cost stays near-constant however many operations your API has. `glotto.yml#/mcp/modes` restricts which modes the server enables (omit it to enable all three); a requested-but-disabled mode falls back to the enabled default. `search_docs` is **always available** in every mode, even when other tools are filtered out. ##### What a `search_docs` hit tells you A reference hit describes the operation, not just its name. Alongside the tool name and the `client..` SDK call, each hit carries: - **`requestBody`** — the body's type, its content type, and whether it is required. - **`response`** — the type of the first success response. - **`parameters[]`** — each parameter's location, whether it is required, its description from your spec, and a full `schema` describing it. A `schema` keeps what a caller needs in order to construct a value: enum members (and any deprecation notice on a member), string and integer `format`, array item types, map value types, object fields and which of them are required, union members and any discriminator, and nullability. A named model appears as `{ "kind": "model", "model": "Pet" }` rather than being expanded inline. Those names resolve through the payload's `models` map, which carries the models the returned hits reference — including ones reached only through other models: ```json { "results": [ { "kind": "reference", "tool": "pets_create", "sdkCall": "client.pets.create", "requestBody": { "required": true, "contentType": "application/json", "schema": { "kind": "model", "model": "Pet" } }, "response": { "kind": "model", "model": "Pet" } } ], "models": { "Pet": { "kind": "object", "required": ["name"], "fields": { "name": { "kind": "string" }, "status": { "kind": "enum", "values": ["available", "sold"] } } } } } ``` The payload is this `{ results, models }` object rather than a bare array of results. If you consume `search_docs` output directly, read `.results` from it; each entry keeps every field it had before. ##### Browsing the surface without a query Searching only helps once you can guess an API's vocabulary, and Code Mode has no `tools/list` to fall back on. So **calling `search_docs` with no `query` browses the operations instead of searching the docs**: `results` becomes a compact listing — tool name, one-line title, resource, method, HTTP method, path — alongside a `resources` table of contents counting the operations in each. Narrow it with `resource`, page it by echoing back the `nextCursor` a response returns, and search a listed tool name to get that operation's full record. Pages are bounded in size, so browsing a large API never dumps its whole surface into one response. The listing shows what the server can actually invoke: in Tools and Dynamic Mode that is exactly what `list_tools` reports, filters included; in Code Mode — which registers no per-operation tools for a filter to hide — it is everything `glotto.yml#/mcp/permissions` allows. `resource` and `cursor` shape a browse, so passing either alongside a `query` is an error rather than a silently ignored argument. A **search** returns the ten best matches plus `total`, the number of records that actually matched. Without that count a query matching your whole API and one matching exactly ten results look identical, so `total` is what tells you an answer was truncated and the query is worth narrowing. ##### `execute` type-checks before it runs An agent's first attempt at an unfamiliar API is often wrong, and the cheapest place to find that out is *not* your production upstream. So before `execute` runs anything, it type-checks the submitted code against the SDK it is about to bind. Code that calls a method the SDK does not have, or passes a wrongly-shaped request body, is **not run**: nothing is sent to your API, and the diagnostics come back as the tool result — at line and column numbers in the code the agent submitted, so it can correct the call and resubmit. That matters most for the mistakes that would otherwise *succeed at reaching you*. A hallucinated method is only a wasted round-trip, but a wrong request body is a real, credentialed request against your API — one that can mutate state on a `POST` and answers with your validation error rather than "that field does not exist". This is a correctness aid, not an access control. A `// @ts-ignore` suppresses it, so what a run is *permitted* to reach is still governed by the sandbox permissions and by [method permissions](https://glotto.dev/docs/mcp-server/#method-permissions). Set `GLOTTO_MCP_CODE_MODE_TYPECHECK=off` to skip the check; it is on by default. ##### Your custom code is not bound in Code Mode Every generated TypeScript SDK re-exports your never-overwritten `lib/` directory as `lib`, so `sdk.lib.myHelper()` is part of your SDK's public surface. A Code Mode run does **not** carry it: the binding hands the isolate the generated SDK, and your own `lib/` sources are not part of it. Rather than let that surface as `undefined`, an agent that reaches for one of your helpers is stopped twice — the type-check above refuses the call before the run starts, and if the check is off or suppressed, the access throws an error that names the boundary instead of `undefined is not a function`. Only *reaching for a member* fails: code that logs, serializes, or passes `lib` around keeps working. Call the generated client from `execute` and keep helper logic on the agent's side of the sandbox. ##### Code Mode's one prerequisite: Deno `execute` runs the agent's code in a [Deno](https://deno.com) sandbox, so a **self-hosted** Code Mode server needs the Deno CLI on its host (`GLOTTO_MCP_DENO_PATH` selects the binary when it isn't on `PATH`). Two paths need no install at all: the emitted **Docker image already includes Deno**, and the hosted Glotto MCP Cloud gateway runs the sandbox on our infrastructure. Nothing else depends on it: Tools Mode, Dynamic Mode, and `search_docs` work without Deno. A server started in Code Mode without it warns at start-up rather than waiting for the first `execute` to fail. #### Generating the server The MCP server is **not** a `targets:` entry. It is configured by a top-level `mcp:` block, the same way the docs site and the mock server are — add one to your `glotto.yml` beside your spec: ```yaml openapi: source: ./openapi.yaml targets: typescript: {} # your SDKs, as usual mcp: # <- the MCP server, a top-level block modes: [tools, code, dynamic] search_docs: true ``` Glotto publishes the generated server to npm for you, so `mcp` *is* a surface for **publishing** — but the `targets:` map is the codegen surface, and putting `mcp` there fails with [`GLOTTO_CONFIG_UNKNOWN_TARGET`](/docs/diagnostics). With the block in place there are three ways to get a server, and they differ in what you end up holding: ```bash glotto generate # your SDKs AND the MCP server, into sdks/mcp/ glotto mcp generate --out ./server # just the server glotto mcp serve # run it now, over stdio — nothing written to disk ``` - **`glotto generate`** is the one to use when the server ships beside your SDKs in the same repo — it is the artifact your regeneration and drift checks then cover. - **`glotto mcp generate`** writes the server on its own, for a separate repo or image. - **`glotto mcp serve`** starts a server straight from `glotto.yml` with no build step. This is the fastest way to point Claude Desktop or Cursor at your API and see the tools appear. On the published `@glotto/cli`, `generate` and `mcp generate` run **server-side** and need `glotto login` first; `mcp serve` runs the emitted artifact locally. The emitted project is an ordinary npm package — `npm install && npm run build` produces `dist/main.js`, which is what you point an MCP client at. #### Transports & auth - **stdio** — for local clients (an `npx` entrypoint). The default. - **Streamable HTTP** — for remote/hosted deployments, with **OAuth 2.1 + PKCE**. Selected with `--transport=http` (or the shorter `--http`, or `GLOTTO_MCP_TRANSPORT=http`); the listen port comes from `--port=N`, else `PORT`, else 3000. A flag always outranks its environment variable, so a container image that baked one in can still be overridden on the command line. The HTTP transport binds **loopback (`127.0.0.1`) by default**, so a server you start on your own machine is not reachable from the network until you say otherwise. Pass `--host=0.0.0.0` (or set `GLOTTO_MCP_HOST`) to serve every interface. The emitted Docker image sets `0.0.0.0` for you, and that is not an exception to the rule: inside a container that address is the container's own network namespace, and what makes the server reachable is the port you publish with `-p 3000:3000`. Binding loopback inside a container would break publishing rather than secure it. On the HTTP transport the server also checks the browser `Origin` header. A request carrying an `Origin` you have not allowlisted is refused with `403` **before** its credentials are checked, which is what stops a page in someone's browser from resolving a hostname it controls to `127.0.0.1` and reaching a server bound there. Requests with **no** `Origin` — every non-browser client, so effectively all normal agent traffic — are unaffected, and the allowlist defaults to your own `GLOTTO_MCP_RESOURCE` origin, so most deployments need no configuration. Name others with `--allowed-origin a,b` (env `GLOTTO_MCP_ALLOWED_ORIGINS`), or pass `*` to switch the check off where a gateway already terminates the browser connection. How the server validates an inbound bearer depends on what your authorization server mints. For **JWTs**, set `GLOTTO_MCP_OAUTH_JWKS_URI` and the server verifies signatures locally against your published JWK set — no round trip per request. For **opaque** tokens, which carry nothing to verify locally, set `GLOTTO_MCP_OAUTH_INTROSPECTION_URL` plus the client id and secret your AS issued you and the server validates each token via RFC 7662 introspection. Set `GLOTTO_MCP_OAUTH_ISSUER` and `GLOTTO_MCP_OAUTH_AUDIENCE` alongside either: RFC 7662 does not require an authorization server to scope its answer to *your* server, so those checks are what prove a token was minted for you rather than for another client of the same AS. Introspection is uncached by default, so revoking a token takes effect on the very next request; `GLOTTO_MCP_OAUTH_INTROSPECTION_CACHE_TTL` opts into caching positive results, and a cached result never outlives the token itself. Configure neither and the server rejects every token — secure by default; configure both and it refuses to start rather than silently picking one. The two directions of auth are distinct: OAuth 2.1 + PKCE above governs how a **client authenticates to the MCP server**. To authenticate the server's **outbound calls to your upstream API**, Tools Mode handlers attach the API's configured credential — matching your `glotto.yml` auth scheme (bearer / OAuth2 token, HTTP basic, or API key) — read from the environment at runtime (the API's configured env var, else a `GLOTTO_API_*` default). Code Mode reaches your API through **the same generated SDK**. When your `glotto.yml` builds the TypeScript SDK alongside the MCP server, `execute`'s sandbox is handed that SDK as the module `./sdk`, so guest code opens with the import it would use anywhere else: ```ts import { Client } from './sdk'; const client = new Client(); // already pointed at your API, already authenticated const pets = await client.pets.listPets(); ``` `new Client()` is pre-addressed and pre-authenticated: Glotto resolves the credential **host-side**, through the same `applyAuth` the Tools Mode handlers use — so caller-forwarded passthrough, per-user outbound OAuth, and the operator's environment variable all reach guest code in that same precedence order, per request. No credential belongs in the submitted code. Binding the SDK **grants the sandbox nothing**. The credential arrives as a pre-bound request header rather than as an environment grant, and the isolate keeps its deny-all baseline exactly as before: no environment access, no filesystem access, and network only to your configured API host. A guest that tries to read `Deno.env` still gets a permission denial with the SDK sitting right there. The one refinement: guest code sees an **empty** `process.env` rather than a denial, because the generated SDK probes it for the telemetry opt-out and for env-var credential fallbacks, and a denial there would fault `new Client()` before it could run. Empty is the honest answer — nothing from your server's environment is present to read, and the credential arrives as a header regardless. When your API authenticates **per operation** (a multi-scheme `securitySchemes` registry), the credential is scoped to the *request* rather than to the client. A client constructed before an operation is chosen cannot know which scheme's secret to carry, so the server resolves **each** operation's own requirement up front — through that same `applyAuth` — and the bound client attaches only the entry matching the request it is actually making. An endpoint that asks for scheme B receives B's credential and nothing else; a scheme's secret is never sent to an endpoint that did not ask for it. An API key configured to travel in the query string is applied there rather than as a header, and an operation you have blocked under `mcp.permissions` never has its credential placed in the sandbox at all. Two cases stay narrower, and the emitted `execute` description says which one applies rather than promising more than the server has. Without the `typescript` target there is no SDK to bind, so guest code calls the API with `fetch`. And when there is no credential to attach at all — an unauthenticated API, or a registry whose every scheme is `custom` — the SDK is bound but nothing authenticates it, so an authenticated call passes a credential in through `execute`'s `input`. ##### Acting on behalf of each user (outbound OAuth) The environment credential above is **one credential for the whole deployment** — right when you host a server for your own team, wrong when you host one server for all of your customers. If your API's OpenAPI spec declares an OAuth2 `authorizationCode` flow, the HTTP transport can instead run that flow as a *client* to your API, so each caller reaches it as themselves: ```yaml mcp: upstream_oauth: enabled: true ``` The authorization and token endpoints come from your **spec's** `flows.authorizationCode` — there is nothing to re-declare here, and nothing to keep in sync by hand. Register your server with your provider and give it `GLOTTO_MCP_UPSTREAM_CLIENT_ID` / `GLOTTO_MCP_UPSTREAM_CLIENT_SECRET`; secrets never belong in `glotto.yml`. On a multi-scheme API, name the scheme the flow satisfies (`upstream_oauth.scheme`) — the server will not guess which credential a user's token stands in for. Every authorization and token request also names your API as its RFC 8707 `resource`, so a provider that supports resource indicators can issue each user a token restricted to your API rather than one valid everywhere that provider is trusted. It is derived from the environment URL already in your `glotto.yml` — nothing to configure — and providers that do not implement the parameter are required to ignore it, so sending it is safe either way. Consent is **three steps**, and the third one is the point. The agent calls `/oauth/upstream/authorize` with its own access token and gets a redirect to your provider's consent screen. Your provider redirects back to `/oauth/upstream/callback`, which exchanges the code and — storing nothing yet — shows the user a single-use **claim code**. The agent then posts that code to `/oauth/upstream/confirm`, again with its own access token, and only that call writes a token to the vault: ```sh curl -X POST https://your-server.example.com/oauth/upstream/confirm \ -H "Authorization: Bearer $AGENT_ACCESS_TOKEN" \ -H 'content-type: application/json' \ -d '{"claim_code":""}' ``` The extra step exists because starting an authorization is not the same as granting one. Without it, any authenticated caller could hold a live `state`, get one of your users to complete the real consent screen, and receive that user's credential. The claim code only ever reaches the browser that saw the consent screen, so holding it is the proof. If the confirming account is not the one that started the flow, the grant is discarded with a `403` rather than re-homed — finishing an authorization on behalf of someone who never started it is the outcome this prevents. Each user's tokens are keyed on the **verified** subject of their inbound access token — never on a value the caller supplies — and access tokens refresh automatically, including against providers that rotate the refresh token on every exchange. Tokens live behind a `TokenVault` interface. The shipped default holds them **in memory**, so every user re-consents after a restart; implement the interface to back it with Redis or a database. This is opt-in and HTTP-only: with the flag off, or on stdio, the environment credential above is unchanged. If your API has **no** OAuth at all, the per-caller passthrough below is the other way to lift the same single-tenant assumption. They are alternatives, not layers: arm both and the server says so at start-up, passthrough wins, and the per-user tokens it vaults are never sent upstream. ##### Per-caller credentials (remote deployments) Sourcing the outbound credential from the environment welds one deployment to one credential — right for a team hosting a server for itself, wrong for an API company hosting **one** server for all of its customers. Setting `GLOTTO_MCP_CREDENTIAL_PASSTHROUGH=1` on an HTTP deployment forwards **the caller's** credential upstream instead, so a single deployment serves many users with no OAuth machinery: ```json { "mcpServers": { "my-org-mcp": { "url": "https://mcp.example.com/mcp", "headers": { "Authorization": "the-caller's-credential" } } } } ``` It is off by default because it changes the trust model, and three properties are worth knowing before you arm it: - **It replaces the inbound OAuth bearer gate.** The inbound credential is forwarded upstream rather than verified as an MCP access token — your API becomes the authority. The server is not thereby unauthenticated: armed, it holds no credential of its own to spend. - **It fails closed.** A request that forwards no credential is refused with `401`. The environment credential is never used to serve a caller who supplied none, so an anonymous caller can never spend yours. - **Multi-scheme APIs name the scheme.** With one scheme the credential rides `Authorization`, used verbatim. With several, each scheme has its own `x-glotto-upstream-` header — `Authorization` cannot say which scheme it satisfies, so it is not consulted. The emitted README lists the exact headers your API accepts. HTTP only, on the standalone transport and the embedded mounts alike; stdio is a local single-user process where the environment credential is already correct. ###### Capping which schemes may be caller-sourced The env var is an **operator's** switch, and on a multi-scheme API it arms every scheme at once — including one whose credential is the deployment's rather than the caller's, like a partner key or a signing secret. `mcp.credential_passthrough` is the **author's** half: a ceiling on which schemes may *ever* be caller-sourced, which the operator's switch can only fill, never widen. ```yaml mcp: credential_passthrough: schemes: [userAuth] # only this scheme may be sourced from the caller ``` Config declares, env enables. Three properties follow, and the third is the one to read closely: - **It only ever narrows.** The env var stays the sole arming switch; the ceiling never turns passthrough on, and with no `credential_passthrough` block nothing changes. - **It cannot be widened at runtime.** The cap is baked in at generation time — the emitted server contains no code path to a forwarded credential for a scheme you did not name, so there is nothing an environment variable or flag could switch back on. - **A capped scheme keeps spending your credential.** This is the deliberate exception to the fail-closed rule above: while passthrough is armed, a scheme outside the ceiling still sources from its environment variable, and a forwarded header for it is ignored. That is the point — you declared that credential yours to spend — but it does mean any caller the 401 gate admits reaches your API as *your deployment* for those schemes. Cap a scheme because it should be yours to spend, not as a way to keep callers away from an operation. The block is an **allow-list**, so it resolves fail-closed: writing `credential_passthrough` at all declares a ceiling, and a name that matches no scheme in your spec simply admits nothing rather than falling back to admitting everything. Run the server with `--debug` to see what it resolved: ```text passthrough userAuth (mcp.credential_passthrough ceiling — 1 of 3) ``` If your API **does** declare an OAuth `authorizationCode` flow, prefer the outbound flow above: it binds each user's credential to an identity your server cryptographically verified, where passthrough trusts whatever the caller sends. Reach for passthrough when there is no OAuth to run. Tool filtering at start (`--resource`, `--operation read|write`, `--tag`) lets a client subset a large API; a filtered-out operation is invisible in Tools Mode and to all three Dynamic Mode meta-tools alike. `--operation read|write` splits on **HTTP method safety** (RFC 9110), the same partition the [tool annotations](https://glotto.dev/docs/mcp-server/#tool-annotations) below publish — so `GET`, `HEAD`, `OPTIONS` and `TRACE` are all reads, and only the verbs that can actually change something are writes. Stand up a read-only deployment with `--operation read` and a `HEAD` operation comes with it. #### Debugging what the server actually resolved Four `glotto.yml` keys can quietly beat a runtime flag — `mcp.modes` clamps `--mode` to an enabled mode, `mcp.filters`' `locked` set overrides `--resource`/`--operation`/`--tag`, `mcp.permissions` removes operations before the filters ever see them, and `mcp.credential_passthrough` caps what `GLOTTO_MCP_CREDENTIAL_PASSTHROUGH` can reach — and from outside the process none of it is visible. `--debug` (or `GLOTTO_MCP_DEBUG=1`) prints what the server resolved, naming the key that won, then starts normally: ```text glotto mcp debug — petstore-mcp transport stdio (default) mode tools (requested "code" is not enabled by mcp.modes) modes enabled tools, dynamic filters operation=read (mcp.filters resolved over the runtime flags "operation=write") tools 2 of 3 registered (mcp.permissions denies 1 of 3) search_docs registered sandbox binary deno (default) sandbox allow-net api.petstore.example ``` The report goes to **stderr**, never stdout — on the stdio transport stdout carries the MCP protocol itself, so it stays clean whether or not you pass the flag. #### Tool annotations Every operation tool is annotated with the standard MCP hints, derived from the operation's HTTP method. You configure nothing — they come from the same spec the tools do: | HTTP method | `readOnlyHint` | `idempotentHint` | `destructiveHint` | |---|---|---|---| | `GET`, `HEAD`, `OPTIONS`, `TRACE` | `true` | `true` | `false` | | `POST` | `false` | `false` | `false` | | `PUT` | `false` | `true` | `true` | | `PATCH` | `false` | `false` | `true` | | `DELETE` | `false` | `true` | `true` | `POST` is the one write verb marked non-destructive: it is the verb that *creates*, where `PUT` replaces, `PATCH` modifies, and `DELETE` removes state that already exists. MCP clients increasingly use these hints to decide what an agent may run unattended and what needs a human to confirm — auto-running a list call while pausing on a delete. Tools Mode publishes them on `tools/list`; in Dynamic Mode, `describe_tools` reports the same values. Two deliberate choices worth knowing: - **`openWorldHint` is never set.** Whether an operation reaches the wider internet is a property of your API's *implementation*, and an OpenAPI document does not describe it. Rather than guess, the server leaves the hint unset and your client's own default applies. - **An unrecognized HTTP method is annotated at maximum restriction** — not read-only, not idempotent, destructive — rather than left bare, so an unusual verb is never mistaken for a safe one. > Annotations are advice to a client, not enforcement. A client is free to ignore them, and > nothing about them prevents a call. To constrain what the server can reach at all, use > **method permissions** below; to enforce access per request, use a scoped API token or the > Glotto MCP Cloud gateway. #### Method permissions Filters are an *operator* convenience an operator can also widen back. When you want the server to be **born** unable to reach certain operations — a self-hosted deployment that should only read, say — declare a permission set in `glotto.yml`: ```yaml mcp: permissions: allow_http_gets: true # every operation mapped to HTTP GET allowed_methods: # regexes over the qualified method name - pets\.photos\..* blocked_methods: # applied last — beats both allow keys - pets\.delete ``` The name a pattern matches is the operation's **fully-qualified method name** — `.`, dotted through subresources: `pets.list`, `pets.photos.add`. Patterns are fully anchored, so `pets\.get` matches `pets.get` and not `pets.getAll`. An invalid regex fails `glotto generate` rather than the running server. Resolution mirrors the shape you may know from Stainless: 1. The allow set is **constrained** only if `allowed_methods` has a pattern or `allow_http_gets` is `true`; a method is in it if it matches a pattern **or** is a GET under `allow_http_gets`. 2. `blocked_methods` is subtracted **after** — a method both allowed and blocked is denied. 3. With **neither allow key set**, everything not blocked is permitted. Enforcement differs by mode, and the difference matters: - **Tools Mode and Dynamic Mode** — a denied operation is simply never registered. `list_tools` omits it, `describe_tools` calls it unknown, `invoke_tool` refuses it. Nothing at runtime widens the set: no `--resource` flag, env var, or `mcp.filters` value can add back a method your `glotto.yml` denies. - **Code Mode** — `execute` statically scans the submitted TypeScript **before running it** and refuses, without reaching the sandbox, when the code references a denied operation by qualified name or by request path. > **Method permissions are a convenience layer, not a security boundary.** > In Tools and Dynamic Mode the gate is structural — an unregistered tool cannot be called. > In Code Mode it is *static analysis of guest code*, and static analysis can be circumvented: > dynamically constructed URLs, indirection, and deliberate obfuscation all defeat it. Use > permissions to keep a well-behaved agent inside its lane. To actually protect sensitive API > operations, use API-layer authentication with a **scoped API token or restricted API key**, > or put the server behind the **Glotto MCP Cloud gateway**, where access control is enforced > per request. #### Experimental: async tasks The `glotto.yml#/mcp/experimental/async_tasks` flag opts the operations your spec marks long-running (a `202`-accepted response or an explicit `x-polling` extension) into **async task-capable tools**. The flag is **experimental, default off, and revision-tracked** — and that revision tracking just fired exactly as designed: the final **2026-07-28 MCP spec** redesigned async tasks as the official `io.modelcontextprotocol/tasks` extension and retired the experimental 2025-11-25 (SEP-1686) shape the flag previously emitted. The flagged emission is being rebuilt for the extension shape. Until that lands, enabling the flag for an API with long-running operations **fails generation with an actionable error** rather than emitting a server on the retired shape; with the flag absent or `false` (the default — and the only state to ship to consumers) the emitted server carries no task surface and is unaffected. #### Install & distribution The emitted server is a **publishable npm package** (no `private` flag, a runnable `bin`, a dist-only `files` list) and ships with every mainstream install affordance: - **README install blocks** — a `claude mcp add` command, a generic `.mcp.json` snippet, a Cursor install deep link, and an `npx` quickstart naming the exact env vars the server reads. - **npm publish** — the release flow publishes it to npm exactly as it does your SDKs (`--all` includes it automatically whenever your `glotto.yml` has an `mcp` block). - **Docker** — an emitted `Dockerfile` runs the server over stdio (`docker run -i`) or Streamable HTTP (`GLOTTO_MCP_TRANSPORT=http`). - **Claude Desktop one-click install** — an emitted [MCPB](https://github.com/anthropics/mcpb) `manifest.json`; `npx -y @anthropic-ai/mcpb pack` produces the `.mcpb` bundle, and the installer prompts for the API credential instead of baking it in. - **Official MCP registry** — set `mcp.registry_name` (your reverse-DNS registry namespace) and Glotto emits a `server.json` for [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io). Your [generated docs site](/docs/generated-docs-site) closes the loop with a `/connect-mcp/` page — the same install blocks, linked from the site nav, so API consumers can connect an AI client without leaving your docs. ### Migrate from Fern Source: https://glotto.dev/docs/migrate-from-fern/ Convert a Fern project — generators.yml and docs.yml — to glotto.yml with one command, pin your SDK's member names, and keep every inbound docs link working. Fern was absorbed by Postman in January 2026. You keep the SDKs you already generated — but the pipeline that regenerated them on every spec change is no longer the product you bought. Glotto picks it up, and adds the part that is hard to rebuild in-house: every regeneration is **verified, not just emitted** — compile and contract statuses, per-target drift, and custom-code preservation, recorded in a [verification report](/docs/verification-report) delivered to CI, the release PR, and the console. The conversion is mechanical, complete, and reported line by line. **You don't need anything from your old vendor.** Fern is Apache-2.0, so `fern/generators.yml`, `.fernignore` and your OpenAPI document are already in your own git repo. That is the practical difference from [migrating off Stainless](/docs/migrate-from-stainless), where the config has to be exported from a dashboard first. #### What Glotto needs from you Your `fern/` directory — that is it. The OpenAPI document your `api.specs[]` block points at is read from there, resolved relative to `generators.yml` exactly as `fern generate` does. #### How you get it Unlike the [Stainless conversion](/docs/migrate-from-stainless#run-it), this one does **not** run in the published CLI. It reads your API surface through the spec pipeline — the part of Glotto that stays on our infrastructure — and there is no self-serve route for it yet, so `glotto migrate fern` in the installed CLI exits `2` and says exactly that rather than half-running. Email [hello@glotto.dev](mailto:hello@glotto.dev) with your `fern/` directory and we run the conversion and hand back the `glotto.yml` and the report described below. Everything on this page is what that conversion does; nothing here is a preview of unbuilt work. #### What you get back A `glotto.yml`, plus a **migration report** saying what happened to every key. Three choices are yours to make, and only one is ever forced: - **Which generator group.** Required when your project declares several and has no `default-group` — see [Several generator groups](https://glotto.dev/docs/migrate-from-fern/#several-generator-groups) below. - **Which API document.** By default the one your `api.specs[]` block names; supply another to override it. - **Whether to pin member names** to what your Fern generator produced, rather than take Glotto's idiomatic casing. See [Keep your SDK's member names](https://glotto.dev/docs/migrate-from-fern/#keep-your-sdks-member-names). The conversion copies every recognized key, normalizes what differs, and is **honest about the rest**: nothing is silently invented or silently lost. Nothing already on disk is overwritten. ##### Several generator groups A Fern project often has more than one group — say `sdks` and `server`. A `default-group` selects one and the migration just works. Without one, the migrator **stops and names them** rather than guessing: ``` groups error GLOTTO_MIGRATE_FERN_GROUP_AMBIGUOUS this project declares 2 generator groups ("sdks", "server") and no `default-group`; name the group to migrate. ``` No file is written. Each group migrates into its own config instead — `sdks` into `glotto.yml`, `server` into `server.glotto.yml`. #### Read the migration report This is the part worth actually reading, and it is where Glotto's Fern migration differs most from a hand-translation. Every key in your `generators.yml` is classified into exactly one of **four** verdicts — "no verdict" is not a state the report can express: | Verdict | What it means | |---|---| | **Mapped** | Translated into `glotto.yml`. The report names the key it became. | | **Already guaranteed** | **You are not losing this.** Glotto provides it unconditionally, so there is no config to carry across. | | **Not honored** | A Fern *emission-shape* knob Glotto answers differently on purpose. Reported with the reason. | | **Unmapped** | Genuinely no Glotto home. This is the list that needs your attention. | The **already-guaranteed** column is why the report has four verdicts rather than two. A two-way report would list these beside your genuine losses, telling you that you are giving up forward-compatible enums at the exact moment you are gaining a stronger, un-configurable version of them: | Your Fern setting | Why you no longer need it | |---|---| | `enable-forward-compatible-enums`, `respect-forward-compatible-enums`, `pydantic_config.enum_type: forward_compatible_python_enums` | Glotto emits open enums unconditionally — an unrecognized wire value round-trips instead of throwing. See [Forward compatibility](/docs/forward-compatibility). | | `allowExtraFields`, `generate-unknown-as-json-node`, `pydantic_config.extra_fields: allow` | Glotto preserves unknown response fields unconditionally and round-trips them on re-send. | | `enable_wire_tests`, `enableWireTests`, `generate-mock-server-tests` | Glotto emits a test suite with every SDK, and its mock serves a spec-validated local server. | The **not-honored** column is the largest, and it is a deliberate non-goal rather than a gap to close later. `noSerdeLayer`, `useBrandedStringAliases`, `neverThrowErrors`, `union: v0|v1`, `package-layout`, the `pydantic_config` block — these configure *Fern's emitter*. Glotto owns its own emission shape; honoring them would mean re-implementing Fern. #### Timeouts are converted, not copied Worth calling out on its own, because getting it wrong is invisible. Fern's timeout key changes units per language: | Generator | Key | Unit | |---|---|---| | TypeScript | `defaultTimeout` | milliseconds | | C# | `default-timeout-in-milliseconds` | milliseconds | | Python | `timeout` | **seconds** | So a project declaring `defaultTimeout: 30000` and `timeout: 30` is declaring **one** timeout, and the migrator emits one: `client_settings.default_timeout: 30s`. Copying the numbers across instead would be a silent 1000× error in your production timeout — it validates clean, and only surfaces under load. `'infinity'` (legal in TypeScript and Python) has no `default_timeout` spelling, so it is reported rather than coerced to some arbitrary large number you never chose. #### Keep your SDK's member names A migration that renames your SDK's public members breaks every consumer downstream. Two facts, and they are good news: **Your method names already survive.** Glotto derives an emitted method name from the OpenAPI `operationId` and re-cases it per language — which is exactly Fern's rule. `listPets()` stays `listPets()` in TypeScript and `list_pets()` in Python, with no configuration at all. **Your property names need a choice.** Glotto keeps the *wire* spelling for an SDK member where Fern re-cases it — a wire property `adoptedAt` emits as `adoptedAt` in the Python SDK, where Fern emitted `adopted_at`. Ask for the Fern spelling to be pinned and the conversion emits the [`naming`](/docs/glotto-yml) / `parameter_naming` entries that hold them there, and **reports every name it pinned**: ``` Pinned names: 3 - property adopted_at → naming.Pet.adoptedAt.python - parameter starting_after → parameter_naming.listPets.startingAfter.python - parameter pet_id → parameter_naming.getPet.petId.python ``` It also refuses the pins that would break your build, and says so: - A property whose wire name is a **reserved word** in the target language. `class` is a Python keyword; Glotto escapes it to `class_`, and pinning the raw name back would emit a module that does not compile. The refusal is per *language* — Go has no `class` keyword, so it is pinned there. - A property containing an **acronym**, where casing conventions genuinely disagree (`HTTPStatus` → `http_status` or `httpstatus`?). Rather than guess and introduce the divergence it is supposed to remove, the migrator names both candidates and leaves the choice to you. Anything Glotto cannot pin is reported naming both spellings — including the client class name, which has no `glotto.yml` key (every Glotto SDK exports `Client`). Nothing is left silently divergent. #### `.fernignore` becomes something better Fern's `.fernignore` **freezes** a file: it stops being generated, and you stop receiving generator updates to it. That is a real cost — the file is yours now, including its bugs. Glotto's [custom-code preservation](/docs/custom-code) three-way-merges instead, so you keep your edits *and* keep the updates. Because the two are semantically different, the migrator **reports** each `.fernignore` entry with the construct that replaces it rather than translating it, and writes no files: - Hand-written files belong in `lib/`, which Glotto writes if absent and never overwrites. - Generated files you have edited stay where they are — the checksum guard preserves your changes across regeneration. #### Mapping reference | Fern | Glotto | Notes | |---|---|---| | `api.specs[].openapi` | `openapi.source` | Re-based onto the directory your `glotto.yml` is written to. | | `api.specs[].origin` | `openapi.source` | Used when no local path is declared. | | `api.specs[].overlays` | `transforms[].apply_overlay.source` | One [`apply_overlay`](/docs/transforms) entry per overlay document, in the order you listed them. Relative paths are re-based onto your `glotto.yml`'s directory. | | `api.specs[].overrides` | `transforms[].merge_document.source` | One [`merge_document`](/docs/transforms#merge_document) entry per overrides file, re-based onto your `glotto.yml`'s directory. This is deliberately **not** `apply_overlay`: an overrides file is an OpenAPI-shaped *merge document*, not an [Overlay](https://spec.openapis.org/overlay/v1.0.0.html) with `actions[]`. Your file carries across unchanged. | | `api.specs[].namespace` | — | Reported. Glotto takes one input source per `glotto.yml`. | | `api.specs[].git` | — | Reported. Glotto reads a path or URL; vendor the spec or point at its raw URL. | | `api.specs[].settings.filter.endpoints` | `exclude` | Direct — Glotto's `exclude` accepts the `"POST /users"` spelling. | | `api.specs[].settings.*` | — | Fern's OpenAPI *ingestion* knobs; Glotto's ingestion answers each with one behavior. | | `default-group` / `groups.` | `targets` | The selected group's generators become your target map. | | `groups..generators[].name` | `targets.` | By generator name. A language Glotto doesn't generate is omitted and reported, never emitted as an invalid target. | | `generators[].output.package-name` | `targets..package_name` | The published registry id. | | `generators[].output.location` | — | Reported as the registry that target is published to. | | `generators[].github.repository` | `targets..repo` (+ `repo_provider`) | Feeds the [multi-VCS release flow](/docs/multi-vcs-release). | | `generators[].version` | — | Glotto versions the generator, not your config. | | `config.maxRetries` / `default_max_retries` | `client_settings.retry.max_attempts` | See [Retries & timeouts](/docs/retries). | | `config.defaultTimeout` / `timeout` / `default-timeout-in-milliseconds` | `client_settings.default_timeout` | **Unit-converted** — see above. | | `config.auto-generate-idempotency-key` | `client_settings.idempotency` | See [Idempotency](/docs/idempotency). | | `config.namespace` / `namespaceExport` / `clientModuleName` | `targets..namespace` | The emitted *code* module — distinct from the registry id above. | | `config.client_class_name` / `clientName` | — | Reported. Every Glotto SDK exports `Client`; re-export it under your own name if consumers depend on it. | | `config.offsetSemantics` | — | Reported. Glotto models pagination [per method](/docs/pagination), not per client. | | `.fernignore` | `lib/` + checksum-guarded managed files | Reported, not translated — see above. | #### Your docs migrate too If your project has a `fern/docs.yml`, it migrates too — the branding into `glotto.yml#/docs`, the `navigation` tree into the generated site's guides collection, and the page content into MDX the site can build. It is picked up automatically; the content tree lands in `./docs-site` unless you say otherwise, and can be left out entirely if you want the SDK config alone. The content tree obeys the same rule as the config: nothing already on disk is overwritten. ##### Your existing links keep working This is the part no other migration guide offers. Your docs have inbound links you do not control — StackOverflow answers, blog posts, bookmarks, other people's READMEs — and moving a page breaks every one of them silently. So the migration **derives a redirect map**. A Fern page's URL comes from its navigation slug chain; a Glotto guide serves under `/guides/…`. Where those differ, you get a `docs.redirects` entry: ```yaml docs: redirects: - from: /introduction/overview to: /guides/introduction/overview ``` `glotto generate` turns that into two things, because one is not enough: 1. **`public/_redirects`** — the rule file Cloudflare Workers Static Assets and Netlify read. 2. **A static redirect page** at the old URL — a zero-second meta refresh with a canonical link at the target — so the promise holds on a host that reads no rule file at all. The stubs carry no `noindex`, deliberately: a zero-second refresh is read as a permanent redirect and **consolidates** the old URL's search ranking onto the new one, which is the whole point. If you also set `docs.deploy.target` to `vercel` or `netlify`, you get a third: that host's own redirect config (`vercel.json` or `netlify.toml`), so the `status` you declared is returned as a real HTTP status rather than an HTML refresh. It matters most on Vercel, which reads neither the rule file nor the stubs. A wildcard source is only translated into Vercel's dialect when the translation is exact — a trailing `*` is, a `*` in the middle is not — and `glotto generate` tells you about the latter rather than rewriting it into a rule matching URLs you did not write. Those two files are host config, not just redirect config, so regeneration leaves your own keys alone. If you add a build command, a headers block, or a framework override to `vercel.json`, only the `redirects` array is refreshed on the next `glotto generate` — every other key stays exactly as you wrote it, and the run tells you it kept them. `netlify.toml` is preserved the same way every other generated file you edit is: your version is kept and reported rather than overwritten, and `glotto generate --merge` merges the new redirects into it. In both cases `--force` gives you the generator's file back. A page whose URL **doesn't** change produces no entry — a redirect from a path to itself is a no-op loop, and one per page would bury the moves that matter. You can add your own entries to `docs.redirects` at any time; `glotto generate` reports a source that is not site-absolute, duplicated, or pointing at itself, and a source that collides with a page the generator emits is refused so a redirect can never shadow real documentation. ##### Your components are translated, and nothing is dropped Fern's callouts land on the two components the generated site ships: | Fern | Glotto | |---|---| | `` | `` | | `` / `` / `` / `` | `` | | `` | `` | | `` / `` | `` | | `` | `` or ``, by intent | The structural components have no Glotto counterpart, so each becomes the semantic HTML that carries the same meaning and renders with no component at all — `` → `
`/``, `` → an ordered list, `` → a link list, `` → `
`/`
`, `` → ``, `` and `` → titled fences and headed sections. Every substitution is named in the report. **A component we do not recognize — including your own, from `fern/components/` — is unwrapped, not deleted.** MDX resolves component names when the site builds, so leaving the tag would fail the build; removing the content would lose your prose. You keep the prose, the report names the component, and you re-create it as an Astro or React component if you want the presentation back (the generated site is Astro + React). A self-closing one has no children to keep, so it becomes an MDX comment carrying its original source — visible in the file, inert on the page, never silently gone. ##### Your endpoint embeds become links to the pages that replace them `` and `` are the one construct where "re-create it as a component" is the wrong answer: Glotto **generates** the page each one stood in for, with that operation's request and response examples and an interactive Try-It panel already on it. So the migration names that page as the remedy, and can rewrite the embed into a link to it. The route is not guessed. It is derived through the same pipeline that decides where a reference page goes, so a link and the page it points at cannot drift apart. An endpoint that names no operation in your OpenAPI document is **not** linked — it keeps the comment treatment and appears in the report with the endpoint named, because a migration that reports success while shipping dead links would be worse than one that says what it could not resolve. If your project declares `versions:`, the links carry the default version's route prefix, matching where the versioned site actually serves those pages. Fern's playground switch needs no migration at all: the Try-It panel ships on every reference page Glotto generates, so it is reported as already guaranteed rather than as something you are losing. ##### What the docs migration reports rather than translates | `docs.yml` | Glotto | Note | |---|---|---| | `instances[].custom-domain` | `docs.deploy.custom_domain` | Direct. | | `instances[].url` | — | The `*.docs.buildwithfern.com` subdomain is Fern's hosting — the thing you are leaving. | | `logo.light` / `.dark` / `.alt` / `.href` | `docs.logo` | Direct, field for field. | | `colors..light` | `docs.theme` | Becomes a `--glotto-*` custom property. | | `colors..dark` | — | `docs.theme` emits one `:root` block. Glotto ships its own dark palette; override it with a `:root[data-theme="dark"]` rule. | | `navigation` `section:` / `page:` | `src/guides/**` + `order:` | Your sidebar order is reproduced. | | `navigation` `- api:` | — | **Already guaranteed.** Glotto generates the API reference from your spec on every run. | | `navigation` `- link:` | — | Reported. Use the `header` layout slot. | | `versions` | `docs.versions` | Mapped, first marked default. Point each entry at its spec snapshot — Fern's `versions[].path` names a docs config, not a spec, so a `TODO` placeholder is emitted with a warning. | | `tabs` / `products` | — | Reported. These are Fern chrome shapes, not a version axis; folding them into `docs.versions` would restructure your site. | | `redirects` | `docs.redirects` | Carried across, alongside the derived entries. | | `favicon` | — | Reported. Drop your icon into the generated project's `public/` — files you add are never overwritten. | | `navbar-links` / `footer-links` | — | Reported. `BaseLayout` exposes named `header` and `footer` slots for exactly this. | | `layout` / `css` / `js` / `typography` | — | Reported. Fern's shell chrome; Glotto emits its own layout and exposes theme tokens plus the layout slots. | | `title` | — | Reported. The site titles itself from your OpenAPI `info.title` and your `organization.name`. | | Page `title` / `description` / `subtitle` / `slug` | guide frontmatter | `description` wins over `subtitle` (the guides collection has one prose field). A page with no `title` gets one synthesized, because a title-less guide fails the build. | | Page `keywords` / `image` / `hide-toc` / `layout` | — | Reported per page. | #### Next steps The migrated config is validation-clean by construction, so the next step is the only one: ```sh glotto generate # your SDKs, docs site, and MCP server ``` The migrated `glotto.yml` fills any required field your Fern config cannot supply — your organization name and contact, and your production base URL, which Fern keeps in `fern.config.json` and its API definition rather than in `generators.yml` — with a clearly-marked `TODO` placeholder and a warning in the report. Replace those, then generate. ### Migrate from Stainless Source: https://glotto.dev/docs/migrate-from-stainless/ Convert a stainless.yml to glotto.yml with one command, review the migration report, and move your SDKs, docs, and MCP server across — custom code kept. Stainless is winding down its hosted platform. You keep your generated code — but the pipeline that regenerates it on every spec change is going away. Glotto picks up where it leaves off: a **superset of Stainless's SDK languages** (all nine, plus React Native, Swift, Rust, Dart, Elixir, and Terraform), the same **Astro-based docs** approach, the same five **spec transforms**, release PRs across **GitHub, GitLab, and Bitbucket**, and a multi-mode **MCP server including Code Mode and dynamic tools** — the architecture Stainless pioneered. More than the feature list, you land on the part that's hard to rebuild in-house: every regeneration is **verified, not just emitted** — compile and contract statuses, per-target drift, and custom-code preservation, recorded in a [verification report](/docs/verification-report) delivered to CI, the release PR, and the console. Generating a client is the easy part; keeping every artifact provably in agreement with a spec that changes weekly is what the pipeline is for. The conversion is mechanical, complete, and reported line by line. #### What Glotto needs from you - Your `stainless.yml`. - Your **OpenAPI document**. Stainless stores the spec out-of-band (uploaded or fetched on their side), so export it from your repo or dashboard first — the migrated config needs a real `openapi.source` path. Optionally, the OpenAPI document's Stainless **extensions** (`x-stainless-*`) come across too — see [Bring your renames across](https://glotto.dev/docs/migrate-from-stainless/#bring-your-renames-across) and [Bring your Terraform attribute shaping across](https://glotto.dev/docs/migrate-from-stainless/#bring-your-terraform-attribute-shaping-across) below. Your original document is never modified in place. #### Run it The conversion is in the published CLI — nothing to request, no account needed: ```sh npm i -g @glotto/cli glotto migrate stainless --openapi openapi.yaml ``` Pass `--openapi` whenever you have the document. Without it your `transforms` are translated from the shape of each command's value; with it every target is resolved against your real document using the same selector `glotto generate` uses, so a target that matches nothing is reported **while you are migrating** rather than breaking a build later. The flags are listed under [`glotto migrate`](/docs/cli#glotto-migrate). #### What you get back A `glotto.yml`, plus a **migration report** saying what happened to every key. The conversion copies every recognized key, normalizes what differs, and is **honest about the rest**: nothing is silently invented or silently lost. A config that declares an **AsyncAPI or GraphQL input block** migrates onto Glotto's native `asyncapi` / `graphql` input blocks (Glotto accepts these as first-class inputs alongside OpenAPI). `glotto.yml` takes exactly one input source per config, so when several are declared one is kept (`openapi` over `asyncapi` over `graphql`) and each dropped block is named in the migration report — the other surface becomes a separate `glotto.yml`. #### Bring your renames across Stainless keeps per-language member, parameter, and enum-value renames **inside the OpenAPI document** as the `x-stainless-naming` (property/member renames — e.g. `public` → `isPublic` for Java), `x-stainless-param` (method-parameter renames — e.g. dodging a Python-reserved argument), and `x-stainless-renameMap` (enum-value renames — e.g. `Ok` for the value `200`) extensions, and shapes models with `x-stainless-model` / `x-stainless-model-skip` (name a schema's model, or keep it inline) and inputs with `x-stainless-soft-required` (demanded by the SDK signature, not by the protocol) and `x-stainless-const` (supplied by the SDK, so the caller never passes it). Glotto keeps the same intent in `glotto.yml` instead, under the `naming`, `parameter_naming`, `enum_naming`, [`models`](/docs/glotto-yml-model-shaping#models), [`soft_required`](/docs/glotto-yml-model-shaping#soft_required), and [`auto_populate`](/docs/glotto-yml-model-shaping#auto_populate) blocks. Supply the OpenAPI document alongside your `stainless.yml` and those extensions are read and translated for you: - `x-stainless-naming` on a schema property → `naming...`. - `x-stainless-param` on an operation parameter → `parameter_naming...`. - `x-stainless-renameMap` on an enum schema → `enum_naming..`. - `x-stainless-model` / `x-stainless-model-skip` on a schema → [`models..name`](/docs/glotto-yml-model-shaping#models) / `models..inline`. - `x-stainless-nominal` on an enum schema → [`enums..nominal`](/docs/glotto-yml-model-shaping#enums). - `x-stainless-enum-deprecations` on an enum schema → [`enums..deprecated_values`](/docs/glotto-yml-model-shaping#enums). The extension is an array **parallel to the schema's `enum`**, so each element deprecates the member at the same position. A `false` element means "not deprecated" and is skipped; a `true` element means "deprecated, no message", which Glotto cannot express — every deprecation carries a message — so it is dropped with a warning telling you which member to add one for. - `x-stainless-soft-required` on an operation parameter, a `requestBody`, or a request-body schema property → [`soft_required.`](/docs/glotto-yml-model-shaping#soft_required)`.parameters` / `.body` / `.body_fields`. A `$ref`'d body schema is followed into `components`, so a shared request model's annotation reaches every operation that sends it. - `x-stainless-const` on an operation parameter or a request-body schema property → [`auto_populate.`](/docs/glotto-yml-model-shaping#auto_populate)`.parameters` / `.body_fields`. There is no whole-`requestBody` form: "the one legal value" is a property a scalar enum position has and a body does not. Target-language keys normalize to Glotto slugs just like `targets` (`node` → `typescript`, …). Anything that can't be mapped — an unsupported language, a parameter rename on an operation with no `operationId`, or a malformed value — is dropped and listed in the migration report, and the report's summary counts how many renames were ingested. Without `--openapi`, migration behaves exactly as before. Note that `renameMap` is written the other way round in each tool: Stainless keys by the new name (`Ok: 200`), Glotto keys by the wire value (`"200": Ok`). The migrator flips each pair for you. A rename pointing at a value the schema's own `enum` doesn't declare is dropped with a warning rather than silently carried over. > **Member renames on TypeScript, React Native, and Python.** > `parameter_naming` is honored by every SDK. **Member renames (`naming`) apply to 11 of the 13 > languages.** TypeScript and React Native keep the wire name (their DTOs are transparent — the > generated type *is* the JSON shape, so a member rename would need a (de)serialization remap they > don't carry), and Python applies member renames only in pydantic mode (`targets.python.pydantic`), > not the default dataclass output. `migrate` still ingests the renames for these targets, and > `glotto generate` warns (`GLOTTO_CONFIG_RENAME_NOT_APPLIED`) where one won't take effect — so you > can prune them or switch Python to pydantic mode. #### Bring your Terraform attribute shaping across Stainless expresses per-attribute Terraform behavior — whether an attribute is server-assigned, whether a collection round-trips as a set instead of a list, whether a field must always be sent on update — as three more OpenAPI extensions: `x-stainless-terraform-configurability`, `x-stainless-collection-type`, and `x-stainless-terraform-always-send`. Glotto expresses the same three facts the same way, as [spec extensions](/docs/terraform#attribute-shaping) rather than `glotto.yml` keys — so translating them means rewriting the spec itself, not the config. Ask for that and you get back a translated **copy of your OpenAPI document**, with everything else in it untouched and your original left unmodified: - `x-stainless-terraform-configurability` on a schema **property** → `x-glotto-terraform-configurability` on the same property, value carried through verbatim (`required` / `optional` / `computed` / `computed_optional`). - `x-stainless-terraform-always-send` on a schema **property** → `x-glotto-terraform-always-send` on the same property, value carried through verbatim (`true`). - `x-stainless-collection-type` on an **array schema node itself** (never its `items`) → `x-glotto-collection-type` on the same node, value carried through verbatim (`list` / `set`). Each key is renamed **where it sits** — nothing is moved, and a `$ref` is never followed. That matters for a property spelled as a pointer: Glotto reads the two property-level facts off the property exactly as it appears under `properties`, so a `{ $ref: …, x-stainless-terraform-configurability: computed }` property keeps its stamp beside the `$ref` and works. Collection type is the exception, because Glotto only reads it from a node that itself declares `type: array` — so `x-stainless-collection-type` beside a `$ref` is **reported rather than renamed**: the translated key would look right in your document and be read by nothing. Move it onto the array (the component the pointer names, or the property spelled inline) and re-run. A value outside the sets above — a `false` always-send, an unrecognized configurability, an annotation on a non-mapping node — is **reported and left unstamped** rather than carried through: an unrecognized value would be silently inert at generate time, so stamping it through would be worse than leaving it for you to fix by hand. An `x-glotto-*` your document already carries is never overwritten — an identical value is a silent no-op, so re-running the migration over its own output changes nothing, and a differing value keeps yours and is reported, naming both. The spec rewrite is **opt-in**. Without it the migration behaves exactly as it does otherwise — byte-identical output, and your OpenAPI document untouched. The rewrite keeps a YAML document's comments and key order; a JSON document is rewritten as JSON at its own indentation. Two things are worth knowing rather than discovering: a comment trailing a mapping key moves onto its own line, and a spec written as a multi-document YAML stream is refused outright rather than partially rewritten. Rewriting your spec **in place** is possible — it is the translated copy written back over its source — and it is subject to the same overwrite guard as every other output, so it never happens by accident. Often you need neither flag: Glotto reads the standard OpenAPI `readOnly` (→ a `Computed` attribute) and `uniqueItems` (→ a set rather than a list) directly, so a spec that already declares those gets the right shape with no annotation or migration at all. The spec rewrite is for the third fact, `x-stainless-terraform-always-send`, which has no standard OpenAPI spelling, and for the other two where your spec doesn't already carry the standard keywords. #### Read the migration report The output ends with a report listing exactly what needs your attention: - **Unmapped (dropped)** — Stainless keys with no Glotto equivalent are dropped from the output and each one is reported with its dotted path. Unknown sub-fields are never silently copied. - **Placeholders (needs review)** — required Glotto fields that can't be derived from the Stainless input are emitted as `TODO-…` placeholder values and flagged. The common one is `openapi.source`: point it at the OpenAPI document you exported above. Then finish the loop: ```sh glotto generate ``` A migrated config with resolved placeholders validates cleanly, and `generate` produces your SDKs across every configured target. See the [pipeline](/docs/pipeline) for what runs under the hood. #### Mapping reference What translates automatically, what needs review, and what is dropped. Every key the conversion handles has a row here — the table is the complete disposition list, so anything not named below falls under the catch-all in its final row. | Stainless surface | Disposition | | --- | --- | | `organization` (`name`, `contact`, `homepage`) | Translated. Missing required `name`/`contact` become `TODO-…` placeholders. | | `targets` | Translated, with language names normalized to Glotto slugs (`node`/`js`/`ts` → `typescript`, `react-native` → `react_native`, `c#`/`dotnet` → `csharp`, `golang` → `go`, …). Per-target publish config carries across opaquely. An unsupported language is dropped with a warning. | | `targets..module_path` / `targets..coordinates` | Translated to the recognized code-identity fields: `module_path` → `namespace` (the `go.mod` module path); `coordinates` (`group:artifact`) → `namespace` (= group) + `package_name` (= artifact). A malformed `coordinates` is dropped with a warning. See [`namespace`](/docs/glotto-yml#targets). | | `targets.openapi` (`production_repo`) | Translated onto Glotto's `spec_repo` target — not a target language, so it is handled separately from the rest of `targets` above. `production_repo` becomes `targets.spec_repo.repo`. The Glotto slug is `spec_repo` rather than the vendor's `openapi`, since Glotto ingests OpenAPI, AsyncAPI and GraphQL and `openapi` would misname two of the three (and would sit confusingly beside the top-level `openapi:` input block). A missing or blank `production_repo` drops `targets.openapi` entirely with a warning; any other `targets.openapi.*` key has no Glotto equivalent and is dropped with its own warning. | | `resources` / `methods` / `subresources` | Translated deeply, recursing through `subresources`. String methods (`get /path`) carry verbatim; method objects keep the `endpoint`, `paginated`, `streaming`, `polling` flags. The `mcp`, `type`/`to`, `deprecated` and `positional_params` keys are consumed into top-level blocks — see the rows below. Any other method flag is dropped with a warning. | | `client_settings` | `default_timeout`, `retry` (`max_attempts`, `initial_delay`, `max_delay`, `jitter`), and `auth` (`scheme`, `env_var`) translate. `base_url` folds into `environments.production`. | | `targets..options.mcp_server` | Translated: `package_name` → [`mcp.package_name`](/docs/glotto-yml-project-settings#mcp). `enable_all_resources: true` matches Glotto's register-everything default (consumed silently); `enable_all_resources: false` and `generate_cloudflare_worker` are dropped with notes. Narrow tools at runtime with the server's `--resource`/`--operation`/`--tag` filters. For `generate_cloudflare_worker`, **hosting and authorization carry over separately**: the emitted server already ships an embedded `fetchHandler` for Workers-style hosts and hosted MCP Cloud serves it managed, while Stainless's worker is *also* an OAuth authorization server (consent flow, API-key collection, token vault, and a generated client-properties input UI). If your API declares an OAuth `authorizationCode` flow, [`mcp.upstream_oauth`](/docs/mcp-server) covers that half. If it does not, Glotto emits no authorization server by design — [forward the caller's credential](/docs/mcp-server) instead. | | `resources.*.methods.*.mcp` | Translated: a curated `description` → `mcp.operations..description` and a curated `tool_name` → `mcp.operations..name`, both keyed by the canonical `_` operation identity (the rename flows through the single tool-name derivation, so the emitted server, hosted gateway catalog, and RBAC policies stay in lockstep). `mcp: true` matches the default (silent); `mcp: false` is dropped with a pointer note (narrow tools at runtime with the server's filters); a `tool_name` that isn't a valid MCP tool name (`^[a-zA-Z0-9_-]{1,64}$`) or repeats an already-mapped rename is dropped with a warning. | | `resources.*.methods.*.type: alias` + `to` | Translated to the top-level [`aliases`](/docs/glotto-yml-api-surface#aliases--deprecated) block — `aliases. = ` — at any subresource depth, so the superseded method name keeps working and breaking-change detection stops reporting the rename as breaking. Both names carry **verbatim**: they resolve at generate time against your spec, not at migrate time, and an entry that matches no operation is reported then (a `--openapi`-resolved operationId would go stale the next time you rename one). A `type:` other than `alias`, a `type: alias` with no `to:`, a bare `to:`, a self-alias (`to:` naming its own method — it could never materialize), and a second method colliding on an already-mapped alias name are each dropped with a warning. | | `resources.*.methods.*.deprecated` | Translated to the top-level [`deprecated`](/docs/glotto-yml-api-surface#aliases--deprecated) block, keyed by the same method name — so deprecating an alias by its own name works, which is the intended migration shape (the old name keeps working *and* warns). A message string carries verbatim; a per-language map has its language keys normalized to Glotto slugs. Glotto has no message-less deprecation, so a map with no `default` gets one **synthesized from its first message** rather than being dropped — listed under *Placeholders (needs review)* in the report, since nothing was dropped and the wording is yours to confirm, and a `deprecated: true` is dropped with a warning naming the missing message. An unsupported language key or a non-string message is dropped with a warning; its well-formed siblings still map. | | `resources.*.methods.*.positional_params` | Translated to the top-level [`positional_params`](/docs/glotto-yml-client-behavior#positional_params) block — **needs `--openapi`**. Glotto keys that block by `operationId`, and a Stainless method key (`retrieve`) is Stainless's name for the method inside its own resource tree, not the operationId; so the migrator reads the method's `endpoint:` for the verb and path and takes the `operationId` the OpenAPI declares there. Without `--openapi` — or when that endpoint is absent from the document, or its operation declares no `operationId` — the order is dropped with a warning naming the remedy. Stainless's key is not per-language and Glotto's is, so one declared order is emitted under **every target you configured**; a language whose SDK takes no positional arguments (Ruby, whose path arguments are keyword arguments) simply ignores it. The order carries verbatim, and a short list stays short — Glotto reads it as a partial prefix, so arguments you did not name keep their derived positions behind the ones you did. The whole order is dropped with a warning — never partly applied — when it names anything that is not one of that endpoint's path parameters: Stainless also promotes body and query parameters to positional arguments, which Glotto's block does not express. A non-list value, a non-string or empty name, a repeated name, and an empty list are each dropped with a warning. | | `environments` | Translated — plain-URL and `{ url }` forms both resolve. If none survive, a placeholder `production` entry is emitted. | | `openapi.source` | Usually a **placeholder**: Stainless uploads the spec out-of-band, so the path can't be derived. Point it at your exported OpenAPI document. | | `openapi.code_samples.formats` | Translated. | | `asyncapi` (top-level) | Translated onto Glotto's native [`asyncapi`](/docs/glotto-yml) input block — both the mapping form and the `asyncapi: ./events.yaml` string shorthand, which becomes `asyncapi.source`. A missing source becomes a `TODO-path-to-asyncapi-spec` placeholder; an unrecognized sub-key, or a value that is neither a mapping nor a path, is dropped with a warning at its own dotted path. **Only one input source survives per config** — see the row below. | | `graphql` (top-level) | Translated onto Glotto's native [`graphql`](/docs/glotto-yml) input block, with the Stainless spellings normalized: `schema` → `source` and `autogenerate_operations` → `autogenerate`; `operations` carries verbatim. The `graphql: ./schema.graphql` string shorthand becomes `graphql.source`. An alias that duplicates the canonical key it maps to is dropped with a warning and never overwrites it; a missing source becomes a `TODO-path-to-graphql-schema` placeholder. **Only one input source survives per config** — see the row below. | | Several input blocks at once (`openapi` + `asyncapi` + `graphql`) | `glotto.yml` accepts exactly one input source, so the migrator keeps one by presence precedence — `openapi` over `asyncapi` over `graphql` — and each losing block is **dropped with a warning naming the winner**, never silently. Migrate the other surface into a separate `glotto.yml`. | | `transforms` | Read from **both** spellings — the top-level key and `openapi.transforms`, where Stainless's own examples put it. The five shared semantic ops translate 1:1 — `rename_schema`, `flatten_composition`, `dedupe_inline_objects`, `extract_ref`, `fix_invalid_example`. Stainless's **generic JSONPath commands** translate into [`apply_overlay`](/docs/transforms#apply_overlay) entries, one entry per command, in their original order, with each command's `reason` carried as the action's `description`: `remove` becomes `remove: true`; `append` becomes an `update` (Overlay appends when the target selects an array); `update` and `merge` become an `update` (Overlay's `update` *is* a recursive merge). A command whose value is **not an object** targets a scalar, which Overlay deliberately leaves undefined — so the target is shortened by one segment and the value wrapped in that key: `….schema.type` with `"string"` becomes `….schema` with `update: { type: string }`. **Pass `--openapi` and the translation gets exact**: each target is resolved against your document with the same selector `glotto generate` uses, so whether it selects an object, a list, or a scalar is *known* rather than inferred from the value — an `update` that replaces a scalar with an object is rewritten to the form that applies instead of failing at your next generate, an `update` on a list replaces it rather than silently appending, and a target that matches nothing (or matches nodes of differing kinds) is reported while you are migrating rather than breaking the build later. `move` and `copy` also translate with `--openapi`, as a `remove` plus an `update` carrying the value read from your document — the emitted action's `description` records that the value is a **snapshot** taken at migration time, so you know to re-check it if the upstream spec moves on. Dropped with a warning naming the reason: `move` and `copy` **without** `--openapi` (an Overlay action's `update` is a literal value, and Overlay cannot reference another node, so there is no value to write — re-run with the flag, or re-express them by hand); a `target` using JSONPath outside [the supported subset](/docs/transforms#apply_overlay); a non-object value whose target ends in the document root, a wildcard, an array index, a filter, or a descendant segment, leaving no single key to lift; and — with `--openapi` — an `append` onto something that is not a list, a `merge` onto a list (Overlay would append, but a merge could equally mean replace, so it is not guessed), and a `remove` targeting the document root. Every dropped command is reported individually at its own path, carrying your `reason` — none is silently lost, and none is emitted optimistically to fail later at generate time. | | `x-stainless-naming` / `x-stainless-param` (OpenAPI extensions) | With `--openapi`, translated into `glotto.yml` `naming` / `parameter_naming`. Unsupported languages, parameter renames on an operation with no `operationId`, and malformed values are dropped with a warning; the report counts what was ingested. | | `x-stainless-renameMap` (OpenAPI extension) | With `--openapi`, translated into `glotto.yml` `enum_naming` (the pair is inverted: Stainless keys by the new name, Glotto by the wire value). A non-mapping extension, an empty rename target, a non-scalar value, and a value the schema's `enum` doesn't declare are each dropped with a warning. | | `x-stainless-model` / `x-stainless-model-skip` (OpenAPI extensions) | With `--openapi`, translated into `glotto.yml` [`models`](/docs/glotto-yml-model-shaping#models) — `name` (the emitted model name) and `inline` (emit no standalone type). Neither extension is per-language, so both map to the single-value form. A non-string or empty model name, and a non-boolean skip, are each dropped with a warning. | | `x-stainless-nominal` (OpenAPI extension) | With `--openapi`, translated into `glotto.yml` [`enums`](/docs/glotto-yml-model-shaping#enums) — `nominal` (its own named type, or an alias over the primitive). Not per-language, so it maps to the single-value form; a non-boolean value is dropped with a warning. **The defaults are opposite** — Stainless aliases an enum unless told otherwise, Glotto gives it a named type — and only enums you actually annotated are translated. Glotto never synthesizes a directive you didn't write, so review the enums that carried no extension and add `nominal: false` where you want the Stainless shape. | | `x-stainless-enum-deprecations` (OpenAPI extension) | With `--openapi`, translated into `glotto.yml` [`enums`](/docs/glotto-yml-model-shaping#enums) — `deprecated_values`, which annotates the member with your target language's own deprecation construct. The extension is an array **parallel to the schema's `enum`**, so position — not a key — selects the member; a `false` element is skipped. Dropped with a warning: a `true` element (Glotto has no message-less deprecation, so add a message to migrate it), a value that isn't an array, an element that is neither a string nor a boolean, an empty message, and an element past the end of the `enum` (or on a schema declaring none). An array shorter than the `enum` is fine — the members it doesn't reach simply aren't deprecated. | | `x-stainless-soft-required` (OpenAPI extension) | With `--openapi`, translated into `glotto.yml` [`soft_required`](/docs/glotto-yml-model-shaping#soft_required), keyed by `operationId` — from a parameter, a `requestBody`, or a body-schema property (following a `$ref` into `components`). Only `true` translates; `false` is a no-op. A non-boolean value, a nameless parameter, and an annotation on an operation with no `operationId` are each dropped with a warning. | | `x-stainless-const` (OpenAPI extension) | With `--openapi`, translated into `glotto.yml` [`auto_populate`](/docs/glotto-yml-model-shaping#auto_populate), keyed by `operationId` — from a parameter or a body-schema property. Only `true` translates; `false` is a no-op, since the block only ever REMOVES an input and there is no "un-const" to express. A non-boolean value, a nameless parameter, and an annotation on an operation with no `operationId` are each dropped with a warning. Whether a named position is actually eligible — its schema must permit exactly one value with a sendable wire form — is decided at generation time and reported there. | | `x-stainless-terraform-configurability` / `x-stainless-collection-type` / `x-stainless-terraform-always-send` (OpenAPI extensions) | With the opt-in spec rewrite (plus `--openapi`), translated into a rewritten copy of your OpenAPI document as `x-glotto-terraform-configurability` / `x-glotto-collection-type` / `x-glotto-terraform-always-send`, value carried through verbatim — see [Bring your Terraform attribute shaping across](https://glotto.dev/docs/migrate-from-stainless/#bring-your-terraform-attribute-shaping-across). Glotto expresses per-attribute Terraform shaping as spec extensions rather than `glotto.yml` keys, so this is the one translation that writes a second output file rather than folding into the config. A value outside the closed set is reported and left unstamped; an `x-glotto-*` you already wrote is never overwritten. Without `--openapi-out`, unchanged: re-annotate by hand, or rely on the standard OpenAPI `readOnly` (→ a `Computed` attribute) and `uniqueItems` (→ a set rather than a list) where your spec already declares them. | | `unspecified_endpoints` (top-level) | Translated to Glotto's target-agnostic [`exclude`](/docs/glotto-yml-api-surface#exclude) deny-list, **verbatim and in source order** — each entry carries across in the `"post /internal_endpoint"` spelling you already wrote, because `exclude` accepts that positional form alongside the `operationId` one. Nothing is resolved at migrate time, so an entry stays correct even if the operation is later renamed. A non-list value is dropped with a warning; a non-string or empty entry is dropped with a warning at its own index while its well-formed siblings still map. The block is emitted only when at least one entry maps — never an empty `exclude: []`. | | `settings` | Partly translated: `license` → top-level `license`, and `detect_breaking_changes` → [`settings.detect_breaking_changes`](/docs/glotto-yml-project-settings#settings). `mock_server` is dropped with a pointer note — Glotto's mock is not a config toggle — and `per_endpoint_security` is dropped because Glotto delivers it with no toggle to set. Any other `settings.*` key is dropped with a warning naming it. | | `code_owners` (top-level) | Translated onto Glotto's [`code_owners`](/docs/glotto-yml-project-settings#code_owners) block, **verbatim and in source order** — order is semantic on both sides, since a CODEOWNERS file resolves last-match-wins, so nothing is sorted or deduplicated. Owner syntax is deliberately *not* rewritten: which spelling is legal depends on the forge each target's repo lives on, which Glotto knows from `targets..repo_provider` and the migration does not — so a GitHub handle carried into a GitLab-targeted config is reported there, at its pointer, with the provider named. Where Stainless emits a file for GitHub only, Glotto covers all four providers: GitHub and GitLab get a committed CODEOWNERS from `glotto generate`, and Bitbucket and Azure Repos are configured through their APIs. Dropped with a warning: a non-mapping block, a rule whose value is not a list, a non-string or empty owner (at its own index, while its siblings still map), and a rule that maps no owners at all. | | `pagination` (top-level) | Dropped with a warning — Glotto declares pagination per-method via the `paginated` flag instead. | | Any other top-level key | Dropped with a warning naming the key. | #### What carries over Everything the wind-down puts at risk has a home — including the three capabilities earlier revisions of this guide tracked as open gaps, each since shipped (tracked to completion in the open): - **Every Stainless SDK language and more** — TypeScript, Python, Go, Java, Kotlin, Ruby, C#, PHP, Terraform, plus React Native, Swift, Rust, Dart, and Elixir. - **Docs** — an Astro site you own, with multi-language snippets, search, theming, and try-it ([guide](/docs/generated-docs-site)). - **MCP** — both architectures, per-operation tools *and* **Code Mode** (`execute` + `search_docs`), with filters and OAuth ([guide](/docs/mcp-server)); the generated multi-mode server self-hosts anywhere Node or Docker runs, and managed hosting ships with the hosted platform. - **Release flow** — release PRs on spec change across GitHub, GitLab, and Bitbucket, with [drift detection](/docs/drift-detection) keeping committed output honest. - **Custom code preservation** — the `lib/` directory *and* patch preservation ([guide](/docs/custom-code)). If you hand-patched your Stainless SDKs, migration does not cost you those edits: hand-edits to generated files survive regeneration via a checksum-keyed three-way merge, with conflicts surfaced in the release PR rather than overwritten. - **Registry publishing** — a released SDK ships to its package registry (npm, PyPI, crates.io, RubyGems, Maven Central, …) as part of [the release flow](/docs/multi-vcs-release). And one thing the old pipeline never gave you: **the proof**. Every regeneration produces a [verification report](/docs/verification-report) — pinned inputs, per-target drift and integrity, compile and contract statuses — with a downloadable signed attestation. #### Next steps - [Getting started](/docs/getting-started) — install the CLI and run the full loop. - [The glotto.yml reference](/docs/glotto-yml) — every key your migrated config can use. - [CLI reference](/docs/cli) — `migrate`, `validate`, `generate`, `drift-check`. - [How Glotto compares](/docs/sdk-generation-comparison) — the idiomatic-output and cross-language parity story, versus the other generators. ### Multi-VCS release flow Source: https://glotto.dev/docs/multi-vcs-release/ Glotto opens regenerate-and-release PRs across GitHub, GitLab, and Bitbucket through one provider-agnostic VCS abstraction. Glotto is **provider-agnostic from day one**. A single `VcsProvider` abstraction lets it push regenerated SDKs and open release PRs across **GitHub, GitLab, and Bitbucket** — including self-managed instances via `host` config and PAT fallbacks. **Gitea** and generic-git are on the roadmap. Self-hosted GitLab, poorly served by GitHub-app-first competitors, is a first-class target. #### How a release happens 1. A spec change is pushed; the provider's webhook is **ingested and normalized** into a provider-agnostic `GlottoVcsEvent` — provider-specific shapes never leak past the normalizer. 2. The **release-PR orchestrator** detects the spec diff, regenerates via the [pipeline](/docs/pipeline), and opens a PR with a changelog against the SDK target repo. 3. The per-provider **[drift-check](/docs/drift-detection)** workflow keeps committed output honest on every PR. The orchestration layer programs against `VcsProvider` + `GlottoVcsEvent` only, so adding a provider doesn't touch the release logic. #### Three branches: `generated` → `next` → `main` Every SDK repository Glotto manages holds the same three branches, and each one answers a different question: | Branch | Holds | Written by | | --- | --- | --- | | `generated` | the **pristine** generator output for the current spec — never merged, never hand-edited | every regeneration | | `next` | `generated` three-way-merged with your custom code, collecting changes for the next release | every regeneration | | `main` | released code | merging the release PR | `generated` is what makes custom code survive regeneration: it is the merge *base*, so Glotto can tell your edits apart from its own previous output instead of guessing. That is why nothing ever lands on it but a fresh generation — see [custom code](/docs/custom-code) for the merge itself. The release PR is always `next` → `main`. Merging it is the release. You can rename any of the three per target with [`targets..release`](/docs/glotto-yml#targets), which is also the migration path for a repository already on other branch names — a config line, not a migration. When a target sits in a subdirectory of a shared repository (`repo_path`), `generated` and `next` each take a per-language suffix so two SDKs in one repository never share a branch. #### Two repositories: staging and production Every SDK target has **two** repositories, and they answer different questions. The **staging repository** is hosted by Glotto, one per project per language, named `/--`. It is created with your project, and every build lands there first: the pristine output on `generated`, with `next` and `main` fast-forwarded to the same revision. It carries **no custom code, ever** — which is what makes it the honest answer to "what did Glotto generate from this spec?". You can install from it directly, with your own forge credentials: ```bash npm i github:/-- ``` Two things to know before you run that. The staging repository is created **private**, so the command needs a credential with read access to it — an authenticated remote, not an anonymous clone. And the tree it installs is the *pristine* SDK: any custom code you keep in your production repository is not in it, by design. **A hand-edit pushed to a staging branch stops the next build** rather than being merged or overwritten. Glotto fast-forwards `next` and `main` and never forces a ref, so a branch whose tip is not on the build line makes the next build fail with `staging_diverged`, naming the branch. Recovery is moving that branch back onto the build line — a force-push by someone with write access on the hosted repository. Glotto does not do it for you: a staging repository that accepted hand-edits would be a second production repository, and "which one is right?" would return. The **production repository** is yours (`targets..repo`). It is where the full release flow above runs — `generated`, the three-way merge into `next`, the release PR `next` → `main`. Selecting it needs an **organization-level connection for its provider** first: connect your VCS, then pick the repository. Glotto reads the repository's default branch when you select it and records it as `targets..release.base_branch` when it is not `main`, so a `trunk` repository integrates on `trunk` without any further configuration. Staging is GitHub-only today. GitLab, Bitbucket and Azure Repos remain fully supported as **production** repositories; hosted staging repositories on those forges are not offered yet. ##### What a build reports Each build of each target records one row, and its status says exactly how far it got: | Status | Means | | --- | --- | | `building` | the build has started; nothing has been pushed yet | | `build_failed` | generation did not complete (or your plan is out of builds) — nothing was pushed anywhere | | `staging_failed` | the build could not be shown on staging, so it was **not** proposed to your repository | | `staged` | staging holds the build; there is no production repository yet, or the release is set to `manual` | | `production_failed` | staging holds the build and is intact; the release on your own repository did not complete | | `released` | the release PR on your repository was opened or refreshed | ##### What the orchestrator opens Nothing below is typed by hand: both panes are what `runReleaseFlow` — the same orchestrator step 2 describes — decided when a spec was pushed. It is running here against the in-memory `VcsProvider` the release suite drives, so what you are reading is the release Glotto *decided on*, not a transcript of one forge's API. Which forge it lands on is the part the abstraction makes uninteresting, and the per-provider suites are what hold that. **a spec pushed to `` `main` `` for the first time — the orchestrator opens the release PR** A spec pushed to main for the first time. The regenerated SDK, on the next branch, as a PR against main. **regenerated + byte-diffed in CI** `runReleaseFlow (@glotto/core-vcs)` `examples/stainless-migration/inputs` `3906fa591adb` files in the release 19 branch next ```text title release: v0.1.0-alpha.1 rule first release; alpha: no verification report head next base main state open files (19) README.md package.json sdk.test.ts src/client.ts src/core/client-core.ts src/core/model-fields.ts src/core/runtime.ts src/core/types.ts src/errors.ts src/index.ts src/lib/index.ts src/models/pet.ts src/models/store.ts src/models/vaccination.ts src/resources/pets-vaccinations.ts src/resources/pets.ts src/resources/stores.ts tsconfig.json tsconfig.test.json ``` Push a spec that gained one operation, and the same flow re-releases — with the SDK following: **the spec gains `` `GET /health` `` — the same flow re-releases, and the SDK follows** The spec gained GET /health. The client and its README moved; nothing else did. **regenerated + byte-diffed in CI** `runReleaseFlow (@glotto/core-vcs)` `examples/stainless-migration/inputs` `c7245baf0840` files in the release 20 branch next ```text title release: v0.1.0-alpha.1 rule from the release PR title head next base main state open files (20) README.md ← changed package.json sdk.test.ts ← changed src/client.ts ← changed src/core/client-core.ts src/core/model-fields.ts src/core/runtime.ts src/core/types.ts src/errors.ts src/index.ts ← changed src/lib/index.ts src/models/pet.ts src/models/store.ts src/models/vaccination.ts src/resources/health.ts ← changed src/resources/pets-vaccinations.ts src/resources/pets.ts src/resources/stores.ts tsconfig.json tsconfig.test.json ``` #### Where the version number comes from The release PR's version is **derived**, not chosen, and the PR body's `Version:` line names the rule that produced it — so a reader never has to go and find out why a number is what it is. Four inputs, read in this order: 1. **The PR title wins.** A release PR is titled `release: vX.Y.Z`, and a version written there is honored verbatim — nothing below is consulted. It is the one place a human sets `1.0.0` or a channel Glotto does not know about. 2. **The bump comes from the contract diff.** An operation or model **removed or changed** is a major bump (a minor one below `1.0.0`); something **added** is a minor bump; no API change is a patch. Below `1.0.0` the derivation never crosses to `1.0.0` — `0.9.0` with a removed operation becomes `0.10.0` — because that line is a product decision, not an arithmetic one. 3. **The prerelease channel comes from your latest verification report.** A **failed** compile or contract check — or a hand-edited managed file — is `-alpha.N`. Checks that have **not run** are `-beta.N`. Only a report where everything passed produces a plain `X.Y.Z`. 4. **No report is `-alpha.N`, not a plain version.** A bare version is a claim that the build was verified, and "we could not find the report" is not that claim. A project that has not set up check ingestion therefore sees `-beta.N` once its reports arrive and `-alpha.N` until then; a diff Glotto could not compute is a patch bump whose rule line says so, never "no API change". Two consequences worth knowing: - **An open release PR keeps the version it was opened with.** The channel is decided when the PR opens and frozen for its lifetime, exactly as the patch number already is — because rule 1 reads the title the PR already carries. Once your checks pass, edit the title to the version you want (or close the PR so the next build opens a fresh one). - **A prerelease never becomes the default install.** An `-alpha.N` / `-beta.N` npm publish goes out under that channel's dist-tag (`alpha`, `beta`, or npm's `next` for any other identifier), never `latest`. pip, RubyGems, NuGet, Hex and pub already treat a prerelease suffix as opt-in. #### Review ownership — one block, four forges The same abstraction covers **who reviews** the SDK repos Glotto manages. Declare it once in [`code_owners`](/docs/glotto-yml-project-settings#code_owners) and each target's forge receives it the way that forge implements review ownership — which is not the same way in any two of them: | Provider | How ownership arrives | Path scoping | | --- | --- | --- | | GitHub | `.github/CODEOWNERS`, emitted by `glotto generate` | Per rule | | GitLab | `.gitlab/CODEOWNERS`, emitted by `glotto generate` | Per rule | | Bitbucket | Repository default reviewers, via code-owner application | **None** — repo-wide | | Azure Repos | A required-reviewers branch policy, via code-owner application | Per rule | The honest deliverable is *review ownership per provider*, not "a CODEOWNERS file", and the part that matters is what happens where the mapping is imperfect. Bitbucket's default reviewers cannot be scoped to a path, so code-owner application tells you **which of your patterns it had to flatten** instead of reporting a plain success. An owner no workspace member matches comes back named, with the reason, while the owners that did resolve still apply. And a target whose SDK lives under a [`repo_path`](/docs/glotto-yml#targets) subtree gets no file at all — GitHub and GitLab read CODEOWNERS only from the repository root — which `glotto generate` and `glotto generate` both say out loud rather than emitting a file nothing will ever open. That last property is the point. A governance feature that quietly does nothing on half your repositories is worse than one you know you have to configure by hand. #### Preview builds — try the SDK before you merge The same normalized webhook powers **preview builds**: when a pull request is opened on your spec repo, Glotto regenerates every SDK target and posts one comment on the PR carrying, per target, its build status, a file-level diff against the last released build, and a **working install command**. | Target | How you install a preview | | --- | --- | | TypeScript, React Native | `npm install ''` | | Python | `pip install ''` | | The other ten languages | Download the archive, then use a local-path dependency (command below) | For those languages, run `curl -L '' | tar xz`, then point the project at the unpacked local path. Install links are unguessable and **expire after 14 days** — treat one as a secret, since anyone holding it can install that build. Previews are **opt-in per project** (`preview.enabled`) and off by default. This is the guarantee made tangible: rather than trusting that regeneration will do the right thing at release, you hold the regenerated artifact while the change is still reviewable. #### When your spec lives at a URL The flow above starts with a **push**. If your OpenAPI document is published at a URL rather than committed to a repository Glotto is connected to, there is no push to react to — so Glotto can **watch the URL instead**. Point [`openapi.source`](/docs/glotto-yml) at your published document, as you already would: ```yaml openapi: source: https://api.example.com/openapi.yaml ``` Then enable polling for the project: set `auto_poll` on the project's verification source (with `poll_interval_minutes` if you want a cadence other than the hourly default). On its cadence Glotto reads **your repository's own `glotto.yml`**, resolves the URL that file names, and fetches it. When the document has changed, it records the revision and runs exactly the release above — the same regeneration, the same three-way merge with your custom code, the same release PR. Push-triggered and poll-triggered releases converge on **one** open release PR, so running both is the intended configuration rather than a conflict. Four things worth knowing before you turn it on: - **It is opt-in per project and off by default.** A poll that finds a change regenerates, and regeneration counts against your plan — so no existing project starts doing this because the feature shipped. - **An unchanged spec costs nothing.** Each poll sends a conditional request; an origin that answers "not modified", or that serves bytes identical to the last revision, produces no regeneration and consumes no quota. The default cadence is hourly, configurable per project. - **The address stays in your repository.** Glotto never stores a copy of your spec URL — it reads `openapi.source` from your `glotto.yml` every time. Changing where your spec lives is a commit you review, not a setting in someone else's database, and it means a local `glotto generate` and a hosted regeneration can never disagree about which document they used. - **The document has to be reachable without credentials.** `openapi.source` takes a URL, not a URL plus headers, so there is nowhere to put an API key — a spec behind an `Authorization` header cannot be polled. This is the same limit a local `glotto generate` against that URL already has, not one polling adds. If your spec is private, commit it to a connected repository and use the push flow above. - **Breaking-change detection still needs a committed baseline.** A URL serves one current document, so there is no older per-branch version to diff against — unlike a spec in git, where the previous commit is the baseline. The [breaking-change check](/docs/breaking-changes) covers this case by diffing against a baseline document you commit. Polling keeps your SDKs current; it does not by itself give a URL source the history a repository has. ### Pagination Source: https://glotto.dev/docs/pagination/ Iterate results automatically or fetch one typed page at a time. Paginated list methods expose the language's iteration surface. In TypeScript, `for await` fetches pages as you consume their items: **TypeScript** (regenerated + byte-diffed in CI `a0f8af4b462c`) ```ts import { Client } from 'glotto-sdk'; const client = new Client({ token: '' }); for await (const item of client.pets.listPets()) { console.log(item); } ``` **React Native** (regenerated + byte-diffed in CI `a1fefe7d3458`) ```ts import { Client } from 'glotto-sdk'; const client = new Client({ token: '' }); for await (const item of client.pets.listPets()) { console.log(item); } ``` **Python** (regenerated + byte-diffed in CI `2242160f8958`) ```python from glotto_sdk import Client client = Client(token="") for item in client.pets.list_pets(): print(item) ``` **Go** (regenerated + byte-diffed in CI `f0c565cc46f8`) ```go package main import ( "context" "fmt" sdk "example.com/glotto-sdk-go" ) func main() { ctx := context.Background() client := sdk.NewClient(sdk.WithToken("")) for value, err := range client.Pets.ListPetsIter(ctx) { if err != nil { panic(err) } fmt.Println(value) } } ``` **Java** (regenerated + byte-diffed in CI `12c05e098737`) ```java import com.glotto.Client; public class Snippet { public static void main(String[] args) throws Exception { Client client = Client.builder().token("").build(); try (var stream = client.pets().listPets()) { stream.forEach(System.out::println); } } } ``` **Kotlin** (regenerated + byte-diffed in CI `0f5e53ae2522`) ```kotlin import com.glotto.Client import kotlinx.coroutines.runBlocking import kotlinx.coroutines.flow.collect fun main() = runBlocking { val client = Client(token = "") client.pets.listPets().collect { println(it) } } ``` **C#** (regenerated + byte-diffed in CI `c5013ce0e88b`) ```csharp using Glotto; var client = new Glotto.Client(""); await foreach (var item in client.Pets.ListPets()) { Console.WriteLine(item); } ``` **PHP** (regenerated + byte-diffed in CI `1acd16081b46`) ```php '); foreach ($client->pets->listPetsIterator() as $item) { var_dump($item); } ``` **Ruby** (regenerated + byte-diffed in CI `a2b854f6593f`) ```ruby require 'glotto' client = Glotto::Client.new(token: '') client.pets.list_pets().each do |item| puts item end ``` **Rust** (regenerated + byte-diffed in CI `716e9ec181d1`) ```rust use futures::StreamExt; let client = Client::default().with_token(""); let mut stream = StreamScope::new(client.pets().list_pets().into_stream()); while let Some(item) = stream.next().await { println!("{:?}", item?); } stream.close(); ``` **Swift** (regenerated + byte-diffed in CI `0cc357352baf`) ```swift import Foundation import GlottoSdk let client = Client(token: "") let stream = client.pets.listPetsScoped() defer { stream.close() } for try await item in stream { print(item) } ``` **Dart** (regenerated + byte-diffed in CI `67567d6df435`) ```dart import 'package:glotto_sdk/glotto_sdk.dart'; final client = Client(token: ""); try { await for (final item in client.pets.listPets()) { print(item); } } finally { client.close(); } ``` **Elixir** (regenerated + byte-diffed in CI `b97af402f1d7`) ```elixir client = Glotto.new(token: "") Glotto.Pets.list_pets(client) |> Enum.each(&IO.inspect/1) ``` > These are byte-for-byte outputs from the same per-language snippet emitters used by > `glotto generate`, locked to a paginated petstore fixture by a drift test. Your generated version > substitutes the package identity, operation name, parameters, authentication scheme, and pagination > shape derived from your spec and `glotto.yml`. Glotto supports **cursor**, **cursor-id**, **page**, **offset**, and **link-header** pagination. An incomplete declaration stays an ordinary request method: the SDK does not invent the missing navigation fields. #### Fetch one page Each supported paginator also has a manual page operation. It fetches one page and exposes its items, response metadata, HTTP response headers, and whether another page exists. Reading those values sends no requests. Fetch the next page explicitly when your application is ready; the next-page call accepts request overrides. The generated README shows the names and types for your language and API. The automatic iterator uses this same page-fetch operation, so both approaches share authentication, retries, errors, and pagination rules. A typed full-response accessor can report a decoding error if the server returns malformed metadata; reading it does not make another request. An absolute next link is the server's complete navigation URL. The SDK follows it without adding the previous page's filters again. Explicit query overrides on your next-page call can augment that URL. ### PHP Source: https://glotto.dev/docs/php/ A Composer-installable PHP SDK with strict_types, resource objects, and a swappable PSR-18 transport — typed ApiError exceptions and lazy paging. The PHP SDK is an idiomatic, Composer-installable client emitted from the same `GlottoIR` as every other target. Files declare `strict_types=1`, models are typed-property objects with `fromJson`, and PHPDoc is generated from the operation prose in the spec. Manual pages expose typed items, the full response and explicit continuation. A trailing `RequestOptions` controls headers, deadlines, retries, extra parameters and cooperative cancellation. Binary operations return an owned `Core\BinaryDownload` with bounded `readAll`, `pipe`, response metadata and `close`; `foreach` closes on break or error. See [pagination](/docs/pagination), [request retries and controls](/docs/retries), [streaming](/docs/streaming), and [file transfers](/docs/file-transfers). #### Quickstart ```bash composer require your-org/petstore ``` ```php '); $result = $client->pets->get('...'); var_dump($result); ``` #### Resource objects Operations are camelCase methods on resource objects — `$client->pets->get($id)`, `$client->pets->photos->add(...)` — the Stripe-PHP service-accessor shape, rather than flat snake_case methods. #### PSR-18 transport The client sends requests through an injectable **PSR-18** HTTP client (Guzzle by default), so the transport is swappable and mockable — the PHP-ecosystem standard, the analogue of C#'s injectable `HttpClient`. #### Typed errors Non-2xx responses throw `ApiError`, an `\Exception` hierarchy carrying the parsed, typed error body; discriminated-union response bodies resolve to the right concrete type. See [Errors](/docs/errors). #### Pagination List methods return a lazy iterator (`foreach`) that walks every page, advancing the cursor for you. See [Pagination](/docs/pagination). #### Retries & backoff Transient failures (`5xx`, `429`, transport errors) retry with exponential backoff and jitter, configurable per client. See [Retries & timeouts](/docs/retries), [Streaming](/docs/streaming), and [Authentication](/docs/authentication). #### Unknown response fields Your API can add a response field without it being a breaking change — but a generated class has no promoted property to put it in, and its `fromJson` factory reads only the fields it declares. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized. ```php $pet = $client->pets->createPet($body); // A field your API started returning after this SDK was generated. $species = $pet->extraFields()['species'] ?? null; // Re-encoding preserves it — a read-modify-write never silently drops it. $json = json_encode($pet); ``` `extraFields()` returns an `array`, so nested objects and lists survive intact, and retention is recursive. One caveat from PHP's JSON decoder: `json_decode($body, true)` represents an empty JSON object and an empty array identically, so an unknown field whose value is `{}` round-trips as `[]`. Every other shape — nested objects, lists, scalars, `null` — round-trips unchanged. Your existing construction still compiles: the storage is a private field assigned after construction, so the promoted constructor is untouched. The retained fields are read-only by design. To *send* a field your spec doesn't model yet, use the per-call extra-body escape hatch rather than writing to the retained bag. ### The pipeline Source: https://glotto.dev/docs/pipeline/ How glotto generate composes spec ingest, normalization, IR build, codegen, and release into one deterministic flow. `glotto generate` is the end-to-end driver. It **composes** the pipeline stages — it never re-implements them — into a single deterministic flow: 1. **Resolve + ingest + normalize the spec.** The OpenAPI / AsyncAPI / GraphQL source is parsed and normalized into a **byte-stable canonical spec** (stable server URLs, sorted keys, stable example IDs). Determinism here is what makes regeneration reproducible. 2. **Build the IR + apply transforms.** `buildGlottoIR` turns the canonical spec into a [Glotto IR](/docs/glotto-ir), applying `glotto.yml#/transforms`. 3. **Dispatch to enabled targets.** The IR is handed to each engine listed in `glotto.yml#/targets` (`@glotto/codegen-*`) — SDKs, the docs site, the MCP server. 4. **Write the output.** Because step 1 is deterministic, the same spec + `glotto.yml` always produce byte-identical output — which is what makes [drift detection](/docs/drift-detection) and committed codegen possible. #### Seeing it hold This page is the overview; each step's proof lives with the step. Rather than restate them here, go to the two pages that own them — both render what Glotto's own machinery printed, not a description of it: - **[Drift detection](/docs/drift-detection)** — the check that fails a PR when committed output stops matching the generator, shown both in sync and catching a real change. - **[The verification report](/docs/verification-report)** — the per-regeneration attestation: pinned input hashes, per-target drift, managed-file integrity, and compile status. #### Two commands - **`glotto generate`** runs the whole flow and writes the result. - **drift detection** runs it in memory and fails if the committed output has drifted (see [Drift detection](/docs/drift-detection)). See the [CLI reference](/docs/cli) for every flag — [`glotto generate`](/docs/cli#glotto-generate) and [drift detection](/docs/drift-detection). ### Polling Source: https://glotto.dev/docs/polling/ Waiting for an async operation to finish with the generated poll helpers. For long-running, asynchronous operations — a job you kick off and then wait on — the SDKs give you two complementary surfaces: a **generated `poll()` companion** beside each operation your spec marks pollable, and a **generic waiter** for any custom predicate. **TypeScript** (regenerated + byte-diffed in CI `a0f8af4b462c`) ```ts import { Client } from 'glotto-sdk'; const client = new Client({ token: '' }); const result = await client.pets.pollGetPet('string'); ``` **React Native** (regenerated + byte-diffed in CI `a1fefe7d3458`) ```ts import { Client } from 'glotto-sdk'; const client = new Client({ token: '' }); const result = await client.pets.pollGetPet('string'); ``` **Python** (regenerated + byte-diffed in CI `2242160f8958`) ```python from glotto_sdk import Client client = Client(token="") result = client.pets.poll_get_pet(pet_id='string') ``` **Go** (regenerated + byte-diffed in CI `f0c565cc46f8`) ```go package main import ( "context" "fmt" sdk "example.com/glotto-sdk-go" ) func main() { ctx := context.Background() client := sdk.NewClient(sdk.WithToken("")) result, err := client.Pets.PollGetPet(ctx, "string") if err != nil { panic(err) } fmt.Println(result) } ``` **Java** (regenerated + byte-diffed in CI `12c05e098737`) ```java import com.glotto.Client; public class Snippet { public static void main(String[] args) throws Exception { Client client = Client.builder().token("").build(); var result = client.pets().pollGetPet("string"); System.out.println(result); } } ``` **Kotlin** (regenerated + byte-diffed in CI `0f5e53ae2522`) ```kotlin import com.glotto.Client import kotlinx.coroutines.runBlocking import kotlinx.coroutines.flow.collect fun main() = runBlocking { val client = Client(token = "") val result = client.pets.pollGetPet("string") println(result) } ``` **C#** (regenerated + byte-diffed in CI `c5013ce0e88b`) ```csharp using Glotto; var client = new Glotto.Client(""); var result = await client.Pets.PollGetPet("string"); Console.WriteLine(result); ``` **PHP** (regenerated + byte-diffed in CI `1acd16081b46`) ```php '); $result = $client->pets->pollGetPet('string'); var_dump($result); ``` **Ruby** (regenerated + byte-diffed in CI `a2b854f6593f`) ```ruby require 'glotto' client = Glotto::Client.new(token: '') result = client.pets.poll_get_pet(pet_id: 'string') puts result ``` **Rust** (regenerated + byte-diffed in CI `716e9ec181d1`) ```rust let client = Client::default().with_token(""); let result = client.pets().poll_get_pet("string", WaitForOptions::default()).await?; ``` **Swift** (regenerated + byte-diffed in CI `0cc357352baf`) ```swift import Foundation import GlottoSdk let client = Client(token: "") let result = try await client.pets.pollGetPet(petId: "string") ``` **Dart** (regenerated + byte-diffed in CI `67567d6df435`) ```dart import 'package:glotto_sdk/glotto_sdk.dart'; final client = Client(token: ""); try { final result = await client.pets.pollGetPet("string", poll: const WaitForOptions()); } finally { client.close(); } ``` **Elixir** (regenerated + byte-diffed in CI `b97af402f1d7`) ```elixir client = Glotto.new(token: "") {:ok, result} = Glotto.Pets.poll_get_pet(client, "string") ``` #### The generated `poll()` companion When the spec marks an operation as pollable, the engine emits a companion beside it that re-fetches on an interval until the resource reaches its terminal state, then returns the typed result. It honors a deadline and (where idiomatic) an `AbortSignal` / `context` so a poll loop never runs unbounded. Because the companion owns the fetch, it sees the HTTP response — so it also honors the server's **`Retry-After`** cadence hint between attempts, clamped to your `maxDelayMs` so a large hint can never stall a wait past the budget you set. That is the one thing the generic waiter below cannot do: you hand it a predicate over an already-decoded value, so the headers are gone by the time it runs. If your API advertises a poll cadence, the companion is the surface that follows it. The companion is named for the method it waits on, spelled the way that language spells names — a method key `get` emits `pollGet` in TypeScript, React Native, Swift and Dart; `PollGet` in Go and C#, whose methods are exported or PascalCased; `poll_get` in Python, Ruby, Rust, PHP and Elixir; and `get_job` likewise emits `pollGetJob` / `PollGetJob` / `poll_get_job`. It is never a bare `poll()`. Its return follows the language's own convention too: TypeScript resolves the typed result and throws on timeout, while Elixir returns `{:ok, result}` / `{:error, :timeout}` like every other call in that SDK. **It ships in every SDK we generate** — the generic waiter below remains available for any predicate the spec does not model. A few per-language shapes worth knowing: - **Swift** — `async throws`, taking a trailing `poll: WaitForOptions = WaitForOptions()` after the usual request options, and waiting with `Task.sleep`, so cancelling the surrounding task cancels the wait (there is no `AbortSignal` to pass). - **Java** — an overload pair beside the operation: `client.jobs().pollGet(id)` for the defaults, or `pollGet(id, intervalMs, maxAttempts, timeoutMs, maxDelayMs)` to set the budget — plus a `pollGetAsync` twin returning a `CompletableFuture`, matching the async twin every other Java method carries. #### The generic waiter For anything the spec doesn't model, every SDK also exports a generic waiter — `waitFor(predicate, options)` (TypeScript), `wait_until(fetch, predicate, …)` / `await_until` (Python), `WaitUntil(ctx, fetch, predicate, …opts)` (Go), and the equivalent elsewhere. You supply the predicate; it polls with the configured `intervalMs`, `timeoutMs`, and `maxAttempts`, returning when the predicate is satisfied and raising a typed timeout error (`PollingTimeoutError`) when it isn't. #### Tuning Both surfaces take the same knobs — poll **interval**, overall **timeout**, and **max attempts** — so you can bound how long a wait runs. Exceeding the timeout or attempt cap raises a typed error rather than hanging. In Go the overall budget is your `context.WithTimeout` rather than a `timeoutMs` option; in the C#, Java, Kotlin, PHP, and Ruby SDKs `timeoutMs` is opt-in — passing it adds a wall-clock deadline on top of the attempt cap, and leaving it off keeps the wait attempt-bounded. #### Backing off between polls By default the wait polls on a fixed interval. Set **`maxDelayMs`** (`max_delay_ms` in the snake_case SDKs, `WithPollMaxDelay` in Go) to switch to capped exponential backoff: the delay starts at `intervalMs` and doubles each attempt up to the cap — `min(maxDelayMs, intervalMs * 2^(attempt-1))`. The schedule is deterministic (no jitter), so a wait is reproducible run-to-run. Python and Go poll with this backoff curve out of the box; for the generic waiter everywhere else it turns on when you pass the cap. The **companion** always has a cap — it defaults `maxDelayMs` to 30 seconds even where the generic waiter leaves it off — because the cap is also what bounds a `Retry-After` hint, so there is always something for a large hint to be clamped against. ### Python Source: https://glotto.dev/docs/python/ An httpx-based Python SDK with typed dataclasses or pydantic v2 models and both async and sync clients — iterator pagination, SSE streaming, and typed errors. The Python SDK is an `httpx`-based client with typed models, emitted from the same `GlottoIR` as every other target. It ships both async and sync surfaces so it fits a FastAPI service or a plain script equally well. #### Quickstart ```bash pip install petstore ``` ```python import os from petstore import Client client = Client(token=os.environ["PETSTORE_TOKEN"]) # a single call pet = client.pets.create_pet(name="Rex") # the sync client yields a plain Iterator[Pet] for pet in client.pets.list_pets(): print(pet.name) ``` #### Async + sync clients The default `Client` is synchronous over `httpx.Client`; `AsyncClient` mirrors the same methods over `httpx.AsyncClient` for callers using `await`. Both are generated from the one IR, so their surfaces stay in lockstep. #### Typed models Request and response shapes use dataclasses by default. Set `targets.python.pydantic: true` to generate `pydantic` v2 models with validation. Both modes return model instances and resolve supported discriminated unions to their variants. #### Serializing models Import `to_dict` or `to_json` from your SDK package to serialize a model with the same rules used for request bodies: ```python from petstore import to_dict, to_json payload = to_dict(pet) text = to_json(pet) ``` Both helpers preserve wire field names, nested models, unknown response fields, and retained temporal values. Optional fields left unspecified by `create(...)` or response decoding stay omitted; explicitly supplied `None` remains JSON null. The helpers work with dataclasses and the opt-in Pydantic models. #### Iterator pagination Paginated list methods return iterators that walk every page, the generated method advancing the cursor. The sync `Client` yields a plain `Iterator` — `for pet in client.pets.list_pets():` — and `AsyncClient` yields an `AsyncIterator` you consume with `async for`. See [Pagination](/docs/pagination). #### Retries & backoff Transient failures (`5xx`, `429`, transport errors) retry with exponential backoff and jitter; non-idempotent writes only retry when idempotency is enabled. Tune `max_attempts`, `initial_delay_ms`, `max_delay_ms`, and `jitter` per client. See [Retries & timeouts](/docs/retries). #### SSE via async iterators Server-sent-event endpoints yield typed events through an async iterator — `async for event in client.…()` — decoded from the `text/event-stream` framing. See [Streaming](/docs/streaming). #### Typed errors Non-2xx responses raise typed exceptions carrying the parsed, typed error body, so you `except RateLimitError` rather than inspecting status codes by hand. See [Errors](/docs/errors). #### Unknown response fields Your API can add a response field without it being a breaking change — but a generated dataclass hydrates only its declared fields, so the key would be dropped. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized. ```python pet = client.pets.create_pet(body) # A field your API started returning after this SDK was generated. species = pet.extra_fields().get("species") ``` `extra_fields()` returns a plain `dict`, so nested objects and lists survive intact, and retention is recursive: a nested model keeps its own unknown fields. Both emit modes carry the same surface — `extra_fields()` is spelled identically whether your SDK was generated with dataclass or pydantic models, so a caller never has to know which one they are holding. The retained fields are read-only by design. To *send* a field your spec doesn't model yet, use the per-call extra-body escape hatch rather than writing to the retained bag. #### File uploads `multipart/form-data` operations build an `httpx` `files=`/`data=` request from their fields for you, and an `application/octet-stream` body is sent raw as the request `content`. Binary fields come straight from the spec's `format: binary` signal, so uploads work without hand-assembling the multipart body. #### Pages, request controls, and files Paginated methods also expose [manual pages](/docs/pagination#fetch-one-page), so you can inspect one response and request its successor explicitly. Request overrides apply to the initial call and to each requested next page; see [Retries & timeouts](/docs/retries) for precedence and cancellation. [Binary downloads](/docs/file-transfers) return an owned byte response with metadata, a bounded read helper, and incremental consumption. Close the response when you stop early. Your generated README demonstrates these operations with your API's names and the language's native calling conventions. ### React Native Source: https://glotto.dev/docs/react-native/ A Hermes-safe, Metro-friendly React Native SDK, not a re-skin of the web client: secure token storage, NetInfo-aware retries, background-aware streams. Glotto generates a first-class React-Native SDK — not a re-skin of the web client. It shares the same `GlottoIR` as the TypeScript target but emits an independent, Hermes-safe, Metro-friendly package. #### Quickstart ```bash npm install @your-org/petstore ``` ```tsx import { Client } from '@your-org/petstore'; // the token comes from your secure store rather than a build-time env var const client = new Client({ token }); // a single call const pet = await client.pets.createPet({ name: 'Rex' }); // pagination is an async iterator, same surface as the TypeScript SDK for await (const pet of client.pets.listPets()) { console.log(pet.name); } ``` #### Secure token storage The bearer token can be backed by a secure-storage adapter instead of memory: - `expo-secure-store` (Expo) - Keychain (`react-native-keychain`) - MMKV (`react-native-mmkv`) You inject the adapter; the SDK reads/writes the token through it. #### Resilient retries on mobile The [retry policy](/docs/retries) becomes network-aware when you inject the optional deps: - **NetInfo** (`@react-native-community/netinfo`) — pause retries while offline, resume on reconnect. - **AppState** — pause retry backoff while the app is backgrounded, resume on foreground. The same injected `AppState` also pauses long-lived `waitFor` polling and SSE/NDJSON streaming while backgrounded (see below), so nothing does work off-screen. Both are optional: no NetInfo/AppState, no hard dependency. #### Streaming & hooks - **Streaming** uses an optional `ReadableStream` polyfill exported from the `/streams` subpath, plus NDJSON via async generators. A frame whose payload isn't valid JSON surfaces a parse error to your loop (never a raw string mistyped as your model); declare terminal sentinels like `data: [DONE]` via `streaming.on_event`. See [Streaming](/docs/streaming). - **Backgrounding a stream**: inject `AppState` on the `Client` (the same option as the retry pause) and a long-lived SSE/NDJSON stream pauses at the top of its read loop while the app is backgrounded — it stops consuming and yielding events off-screen and resumes on foreground. Caller `abort` still tears the stream down immediately. No `AppState`, no behavior change. - **Streaming on Expo**: React Native's built-in `fetch` buffers responses, so an SSE/NDJSON call over it fails with an actionable error instead of a silently empty stream. On Expo, opt into streaming with the `/expo` subpath's typed `expo/fetch` adapter: ```ts import { expoStreamingFetch } from '/expo'; const client = new Client({ fetch: expoStreamingFetch }); ``` The `/expo` subpath is the only module that imports Expo — bare React Native apps never resolve it (pass any other streaming-capable `fetch` the same way). - **TanStack Query 5 hooks** are available from the `/hooks` subpath for idiomatic data fetching with `useQuery`/`useInfiniteQuery`. #### Testing with Jest The SDK package is ESM-first, so Jest's default `node_modules` ignore needs an allowance for it — with either the `react-native` or the `jest-expo` preset: ```js // jest.config.js module.exports = { preset: 'jest-expo', // or 'react-native' transformIgnorePatterns: [ 'node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|)/)', ], }; ``` Because every native seam is injected (NetInfo, AppState, secure storage, `fetch`), unit tests pass plain objects — no native-module mocks are required to exercise the client. #### Unknown response fields Your API can add a response field without it being a breaking change — but a generated interface gives you no typed way to reach it. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized. ```ts const pet = await client.pets.createPet(body); const extra = petExtraFields(pet); if ('species' in extra) { console.log(extra.species); } ``` Each model gets its own reader — `petExtraFields(pet)`, `tagExtraFields(tag)` — typed to that model, so passing the wrong one is a compile error. As in the TypeScript SDK, the data was always retained at runtime (the decode is a cast); the readers are what make it reachable and keep it guaranteed rather than incidental. The retained fields are read-only by design. To *send* a field your spec doesn't model yet, use the per-call extra-body escape hatch rather than writing to the retained bag. #### Platform integration - **Hermes-compatible** output (no `eval`, no `Function` constructor, no `Proxy`). - An optional **Expo config plugin** wires the OAuth deep-link scheme and the iOS keychain entitlement, and can pin the app's JS engine via its `jsEngine` prop. Glotto emits it when the API's auth capabilities need it. An API without OAuth or bearer-style/secure-storage auth capability—for example, one using only custom or API-key auth—does not receive a `./plugin` subpath by default. To opt any React Native target in explicitly, set `targets.react_native.expo_plugin: true`, then import the public subpath in your Expo config: ```ts import withGlotto from '/plugin'; ``` - **AbortSignal** is never polyfilled — the SDK detects and warns if a polyfill is installed. - A **Metro-friendly** package shape: side-effect-free, ESM-first, tree-shakable per resource, with the manifest's `react-native` field pointing at shipped TypeScript source so Metro consumes it directly. #### Pages, request controls, and files Paginated methods also expose [manual pages](/docs/pagination#fetch-one-page), so you can inspect one response and request its successor explicitly. Request overrides apply to the initial call and to each requested next page; see [Retries & timeouts](/docs/retries) for precedence and cancellation. [Binary downloads](/docs/file-transfers) return an owned byte response with metadata, a bounded read helper, and incremental consumption. Close the response when you stop early. Your generated README demonstrates these operations with your API's names and the language's native calling conventions. ### Retries & timeouts Source: https://glotto.dev/docs/retries/ Automatic retries with backoff, and how to tune them. Clients retry transient failures automatically, with exponential backoff and jitter. Tune it per-client at construction: **TypeScript** (regenerated + byte-diffed in CI `a0f8af4b462c`) ```ts import { Client } from 'glotto-sdk'; const client = new Client({ maxAttempts: 4, initialDelayMs: 200, maxDelayMs: 5000, jitter: true, timeoutMs: 30000, }); ``` **React Native** (regenerated + byte-diffed in CI `a1fefe7d3458`) ```ts import { Client } from 'glotto-sdk'; const client = new Client({ maxAttempts: 4, initialDelayMs: 200, maxDelayMs: 5000, jitter: true, timeoutMs: 30000, }); ``` **Python** (regenerated + byte-diffed in CI `2242160f8958`) ```python from glotto_sdk import Client client = Client( max_attempts=4, initial_delay_ms=200, max_delay_ms=5000, jitter=True, timeout_ms=30000, ) ``` **Go** (regenerated + byte-diffed in CI `f0c565cc46f8`) ```go package main import ( "time" "context" "fmt" sdk "example.com/glotto-sdk-go" ) func main() { ctx := context.Background() client := sdk.NewClient(sdk.WithToken("")) result, err := client.Pets.CreatePet(ctx, sdk.PetCreate{Name: "", Species: "cat"}, sdk.WithTimeout(10 * time.Second), sdk.WithMaxRetries(2), sdk.WithHeader("X-Trace", "example")) if err != nil { panic(err) } fmt.Println(result) } ``` **Java** (regenerated + byte-diffed in CI `12c05e098737`) ```java import com.glotto.Client; public class Snippet { public static void main(String[] args) { Client client = Client.builder().token("").maxAttempts(4).initialDelayMs(200).maxDelayMs(5000).jitter(true).timeoutMs(30000).build(); } } ``` **Kotlin** (regenerated + byte-diffed in CI `0f5e53ae2522`) ```kotlin import com.glotto.Client fun main() { val client = Client(token = "", maxAttempts = 4, initialDelayMs = 200, maxDelayMs = 5000, jitter = true, timeoutMs = 30000) println(client) } ``` **C#** (regenerated + byte-diffed in CI `c5013ce0e88b`) ```csharp using Glotto; var client = new Client(new GlottoClientOptions { MaxAttempts = 4, InitialDelayMs = 200, MaxDelayMs = 5000, Jitter = true, TimeoutMs = 30000, }); ``` **PHP** (regenerated + byte-diffed in CI `1acd16081b46`) ```php ', maxAttempts: 4, initialDelayMs: 200, maxDelayMs: 5000, jitter: true, timeoutMs: 30000); ``` **Ruby** (regenerated + byte-diffed in CI `a2b854f6593f`) ```ruby require 'glotto' client = Glotto::Client.new(token: '', max_attempts: 4, initial_delay_ms: 200, max_delay_ms: 5000, jitter: true, timeout_ms: 30_000) request_options = Glotto::RequestOptions.new(timeout_ms: 5000, max_retries: 0) ``` **Rust** (regenerated + byte-diffed in CI `716e9ec181d1`) ```rust let client = Client::default().with_token("").with_max_attempts(4).with_initial_delay_ms(200).with_max_delay_ms(5000).with_jitter(true).with_timeout_ms(30000); ``` **Swift** (regenerated + byte-diffed in CI `0cc357352baf`) ```swift import Foundation import GlottoSdk let client = Client(token: "", maxAttempts: 4, initialDelay: 0.2, maxDelay: 5, jitter: true, timeoutMs: 30000) let options = RequestOptions(headers: ["X-Trace": "example"], timeoutMs: 10000, maxRetries: 2) let result = try await client.pets.createPet(body: PetCreate(name: "", species: PetCreateSpecies.cat), options: options) ``` **Dart** (regenerated + byte-diffed in CI `67567d6df435`) ```dart import 'package:glotto_sdk/glotto_sdk.dart'; final client = Client(token: "", maxAttempts: 4, initialDelayMs: 200, maxDelayMs: 5000, jitter: true, timeoutMs: 30000); ``` **Elixir** (regenerated + byte-diffed in CI `b97af402f1d7`) ```elixir client = Glotto.new(token: "", max_attempts: 4, initial_delay_ms: 200, max_delay_ms: 5_000, jitter: true, timeout_ms: 30_000) ``` #### What gets retried By default the client retries **safe requests** — `GET`/`HEAD` — plus `5x` responses, `429`, and transport errors. **Mutations are not retried blindly:** a `POST` is retried only when idempotency is enabled (`idempotency: true` at construction — see [Idempotency](/docs/idempotency)), so a retry can't double-apply a write. #### Configuring defaults Set the defaults for everyone in `glotto.yml#/client_settings/retry` (`max_attempts`, `initial_delay`, `max_delay`, `jitter`); the constructor options above override them per-client. #### Request timeouts Each attempt is bounded by an overall request timeout. It defaults to **30 seconds**, and `client_settings.default_timeout` sets a different spec-wide default — a duration string of the same grammar as the retry delays (`60s`, `500ms`); a value outside that grammar, or a zero timeout, is rejected at validation. The generated client bakes the resolved value as its client-level default. Resolution runs from **per-call → method → nearest resource → client instance → spec-wide `client_settings` → engine default**. Configure resource and method preferences with [`default_request_options`](/docs/glotto-yml-client-behavior#default_request_options). Every language exposes request controls through its native options, keyword arguments, context, or cancellation handle. The generated README shows your SDK's spelling. A per-call retry limit counts retries **after** the initial attempt: zero makes one attempt. Request-specific headers and timeouts do not change the client defaults for later calls. A buffered request's timeout includes reading its response body. For SSE, NDJSON, and binary downloads, the timeout covers establishment through the first byte or an empty response. A WebSocket's opening timeout ends at the successful upgrade. The established stream can outlive that timeout; caller cancellation and explicit closure remain active. An SDK does not reconnect or replay a stream after exposing its first byte. #### Capping total retry time `max_attempts` bounds how many tries happen; `retry.max_elapsed` bounds how *long* they take overall. Set it (a duration string, e.g. `max_elapsed: 90s`) and every SDK adds an overall wall-clock deadline spanning all attempts: before committing to a backoff wait, the client checks that the post-backoff resume time still fits the budget — if it doesn't, the last response or error surfaces immediately, as if attempts were exhausted. The generated client exposes it as a constructor knob (`maxElapsedMs` in TypeScript/React Native/Dart, `max_elapsed_ms`/`with_max_elapsed_ms` in Python, Ruby, Elixir, and Rust, `MaxElapsedMs` in C#, `maxElapsedMs` in Java/Kotlin, `$maxElapsedMs` in PHP, `maxElapsed` seconds in Swift, `WithMaxElapsed` in Go), so a caller can still override the spec-wide default per client. Leave `max_elapsed` unset and no deadline applies — attempts and per-attempt timeouts are the only bounds, exactly as before. ### Ruby Source: https://glotto.dev/docs/ruby/ An idiomatic Ruby gem with snake_case resource objects, immutable Data.define models, persistent connections, and rescuable typed error classes. The Ruby SDK is an idiomatic gem emitted from the same `GlottoIR` as every other target. It uses snake_case methods, keyword args, immutable `Data.define` models with `from_json`, and YARD doc comments generated from the operation prose in the spec. Manual pages expose items, response metadata and explicit `next_page` navigation. Per-call `request_options:` controls headers, deadlines, retries, extra parameters and cancellation. Binary operations return an owned `BinaryDownload` with bounded `read_all`, `pipe`, response metadata and `close`; block iteration releases the response on break or error. See [pagination](/docs/pagination), [request retries and controls](/docs/retries), [streaming](/docs/streaming), and [file transfers](/docs/file-transfers). #### Quickstart ```bash gem install petstore ``` ```ruby require 'petstore' client = Petstore::Client.new(token: '') result = client.pets.get(pet_id: '...') puts result ``` #### Resource objects Operations are methods on resource objects — `client.pets.get(id)`, `client.pets.photos.add(...)` — the Stripe-Ruby service-accessor shape, rather than flat methods on `Client`. #### Persistent connections Requests reuse a persistent per-host connection (via `net-http-persistent`) instead of opening a fresh socket per call, so throughput holds up under load and across threads. #### Typed errors Non-2xx responses raise `ApiError`, a `StandardError` hierarchy carrying the parsed, typed error body, so you `rescue RateLimitError` rather than inspecting status codes. Discriminated-union bodies resolve to the right variant. See [Errors](/docs/errors). #### Pagination List methods return an `Enumerator` that walks every page lazily, advancing the cursor for you. See [Pagination](/docs/pagination). #### Retries & backoff Transient failures (`5xx`, `429`, transport errors) retry with exponential backoff and jitter, configurable per client. See [Retries & timeouts](/docs/retries), [Streaming](/docs/streaming), and [Authentication](/docs/authentication). #### Unknown response fields Your API can add a response field without it being a breaking change — but a generated `Data` class has only the members it declares, and its `from_json` factory reads only those. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized. ```ruby pet = client.pets.create_pet(body) # A field your API started returning after this SDK was generated. species = pet.extra_fields["species"] # Re-encoding preserves it — a read-modify-write never silently drops it. json = JSON.generate(pet.to_h) ``` `extra_fields` returns a frozen `Hash`, so nested objects and arrays survive intact, and retention is recursive. Use `pet.with(name: "Rex")` to create an updated immutable value. The copy keeps unknown response fields and retained timestamps for members you did not change. Replacing a timestamp explicitly replaces its retained wire value too. If a declared member already uses `with`, the copy helper uses the first free name starting with `with_2`, preserving the member's reader. The retained fields are held in an instance variable rather than as a `Data` member, so `members`, `inspect`, `==` and pattern matching on your models are all unchanged — and both `Pet.new(id: …)` and `Pet.new('p1', …)` still work. The retained fields are read-only by design. To *send* a field your spec doesn't model yet, use the per-call extra-body escape hatch rather than writing to the retained bag. ### Rust Source: https://glotto.dev/docs/rust/ An idiomatic Rust crate built on serde types: Result-returning operations with an ApiErrorKind enum to match on, cursor pagination, and SSE streaming. The Rust SDK is an idiomatic crate emitted from the same `GlottoIR` as every other target. Operations under a declared resource hang off an accessor — `client.pets().list_pets()` — while an operation you leave ungrouped stays hoisted on `impl Client`. Request/response shapes are `serde` types, so you work with real structs rather than bytes. #### Quickstart ```bash cargo add petstore ``` ```rust use futures::StreamExt; let client = Client::new("https://api.petstore.example") .with_token(std::env::var("PETSTORE_TOKEN")?); // a single call let pet = client.pets().create_pet(NewPet::new("Rex".into())).await?; // StreamScope owns the paginator until close or scope exit. let mut stream = StreamScope::new(client.pets().list_pets()); while let Some(pet) = stream.next().await { println!("{}", pet?.name); } stream.close(); ``` #### Typed models Each `GlottoIR` model becomes a `serde` `Deserialize`/`Serialize` type; operations decode and return the typed response and accept a typed request body. Discriminated unions resolve to the right enum variant. #### Typed errors Operations return a `Result`; the `Err` is an `ApiError` struct carrying the parsed error body, plus an `ApiErrorKind` enum so callers `match err.kind()` against `ApiErrorKind::NotFound` rather than matching status codes. See [Errors](/docs/errors). #### Pagination Paginated list methods expose a walker that fetches each page as you iterate, advancing the cursor for you. Wrap the stream in `StreamScope` and call `close()` after an early loop break. The wrapper also releases its inner stream on EOF, a yielded error, or Rust scope exit, including when consumer code returns through `?`. Keeping a bare stream requires explicitly dropping it when you stop reading. See [Pagination](/docs/pagination). Manual page companions expose typed items, metadata and explicit `next_page()` navigation. The lazy `response()` accessor decodes the full wrapper without another request. See [manual pagination](/docs/pagination). #### Retries & backoff Use `RequestOverrides` with an operation’s `_with_options` companion or builder to override headers, timeouts, retry counts and idempotency. Existing `RequestOptions` values remain accepted. See [request options and defaults](/docs/retries). Transient failures (`5xx`, `429`, transport errors) retry with exponential backoff and jitter, configurable per client. See [Retries & timeouts](/docs/retries). #### Streaming Server-sent-event endpoints return a typed event stream decoded from the `text/event-stream` framing. NDJSON endpoints yield each decoded line through the same stream interface. Use `StreamScope` for deterministic cleanup, as with pagination. Stream opening shares the client retry policy, authentication and hooks. The request timeout ends at the first response byte; later frames can arrive after that timeout. WebSocket upgrades use the injected HTTP client and end their opening deadline at the accepted upgrade. See [Streaming](/docs/streaming) and [Authentication](/docs/authentication). #### Unknown response fields Your API can add a response field without it being a breaking change — but a closed struct would drop it, and `serde` discards what it does not recognize. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized. ```rust let pet = client.pets().create_pet(body).await?; // A field your API started returning after this SDK was generated. if let Some(species) = pet.extra_fields().get("species") { println!("{species}"); } // Re-encoding preserves it — a read-modify-write never silently drops it. let json = serde_json::to_string(&pet)?; ``` `extra_fields()` returns an `&ExtraFields` over `serde_json::Value`s, so nested objects and arrays survive intact, and retention is recursive. ##### Constructing a model Every generated object model has a `new(...)` constructor taking its required fields. It initializes optional fields to `None` and retained fields to an empty bag, so you do not need to name the internal retention field in a struct literal. A required nullable field still needs an explicit argument: pass `None` to send its JSON null. Models whose caller-supplied fields are all optional also implement `Default`. This includes empty models and union variants whose only required field is their generated discriminator. Use `Model::default()` for an empty request or `Model { field: Some(value), ..Default::default() }` to set selected fields. Referenced models and enum types do not need their own `Default` because the optional field itself defaults to `None`. An optional collection set to `Some(Vec::new())` sends an empty array; leaving it `None` omits the key. For a decoded model, `Model { field: Some(value), ..original.clone() }` preserves the other fields and retained response values. Required-field models and enum types do not implement `Default`; construct them with their required arguments or a named enum variant. To *send* a field your spec doesn't model yet, use the per-call extra-body escape hatch rather than the retained bag. #### Binary downloads Binary endpoints return an owned `BinaryResponse` with response metadata. `read_all(max_bytes)` bounds delivered bytes and `copy_to(&mut sink)` writes through `AsyncWrite`. These helpers consume the owner; close or drop an incremental reader when leaving early. Mixed JSON/binary operations expose an explicit result branch. See [file transfers](/docs/file-transfers). #### File uploads `multipart/form-data` operations build the multipart body from their fields for you; an `application/octet-stream` operation takes a positional `body: Vec` argument sent raw with the right `Content-Type`. ### How Glotto compares Source: https://glotto.dev/docs/sdk-generation-comparison/ Two questions decide an SDK generator: is the output idiomatic, and how do you keep every language in parity? How Glotto compares to the alternatives. An SDK is not a typed wrapper around HTTP calls. A good one auto-paginates, retries with jittered backoff, exchanges auth tokens transparently, surfaces errors as the language's native exception type, and gives callers compile-time confidence that request shapes are correct — and it does that across a dozen languages, each with its own idea of "idiomatic." When you evaluate a generator, two questions decide it: 1. **Is the output idiomatic?** — code your users would have written by hand. 2. **How do you keep every language correct and in parity?** — so a feature isn't silently missing in one SDK. This page answers both, and shows where Glotto sits relative to the alternatives. #### The landscape Every tool optimizes for something. None of these is "wrong" — they make different trades. - **Open-source generators (e.g. OpenAPI Generator).** Enormous language coverage, zero cost, battle-tested. The trade is output quality: generated code is functional but rarely idiomatic, so teams that care about SDK developer experience end up post-processing or customizing heavily. - **Managed platforms (Stainless, Speakeasy, Fern).** Strong, polished output and a turnkey experience. The trade is control: you accept the platform's opinion about what your SDKs look like, and the pipeline that regenerates them lives in someone else's black box. - **Build-your-own IR frameworks (e.g. oagen).** You parse the spec into a clean, fully-resolved intermediate representation and write your own emitters. Maximum control — the trade is that you build and maintain the emitters yourself. **Glotto's bet:** keep the IR-and-emitters architecture that makes output idiomatic, ship the emitters so you don't build them, and back the whole thing with parity enforcement so "every SDK has it" is a tested guarantee rather than a hope. Plus the wedge the managed platforms don't cover: **React Native** as a first-class target, **GitLab/Bitbucket** alongside GitHub, and a multi-mode **MCP** server including Code Mode. #### Axis 1 — idiomatic output The insight every serious generator shares is that you don't template raw YAML. You parse the spec **once** into a normalized, fully-resolved intermediate representation — all the `$ref` chains, schema composition, and OpenAPI quirks handled centrally — and then each language's emitter consumes that clean data model and makes **real per-language decisions**. In Glotto the IR is [`@glotto/core-ir`](/docs/glotto-ir): a typed model that carries optionality, auth schemes, pagination strategy, retry config, and per-language naming as first-class fields. The emitters consume it and decide, idiomatically: | The IR says… | Python emits | Go emits | TypeScript emits | |---|---|---|---| | an **optional query param** | a keyword arg defaulted to `None` | a pointer field on the params struct (`*string`) | an optional property (`name?: string`) | | a **`oneOf`** | a discriminated union | a type with a discriminator check | a discriminated union narrowed on the tag | | a **documented 4xx/5xx** | an `ApiError` subclass | an error type with `errors.As` unwrap | an `ApiError` subclass | Those aren't template substitutions — they're decisions encoded in hand-written emitters, which is exactly why the output reads like code a person wrote. The difference from a build-your-own framework is that **Glotto ships the emitters** for every supported language. #### Axis 2 — code quality and cross-language parity Idiomatic output is table stakes. The harder problem — the one that decides whether you can trust a multi-language generator — is keeping every language **correct** and **in parity**, so a capability shipped for TypeScript isn't quietly missing in Dart. Glotto enforces that three ways, in CI, on every change: - **Fresh-compile verification.** The [`@glotto/compile-verification`](/docs/glotto-ir) harness fresh-generates each SDK from the IR and **compiles / type-checks it in its target language's toolchain** — not "it rendered," but "it builds." - **Cross-language contract tests.** One [contract manifest](/docs/pagination) of request/response scenarios is replayed by a per-language runner for **every** SDK against a mock server, asserting each language sends the same request and returns the same value. Behavioral parity, checked across languages from a single source of truth. - **Golden + drift gates.** Every engine's output is byte-locked by golden tests, and a dogfooded drift detection fails the build if committed output drifts from what the current generator produces. ##### The feature matrix — every SDK, every language The [platform plan's §3.1.2 feature matrix](/docs/retries) is a contract: a capability listed there must exist in **every** generated SDK. As of the link-header-pagination and v2-timeout parity closures, all 13 SDK languages — TypeScript, React Native, Python, Go, Java, Kotlin, C#, PHP, Ruby, Swift, Rust, Dart, and Elixir — satisfy it: | Capability | Across all 13 SDKs | |---|---| | Typed resources & methods | ✅ | | Auth (Bearer, API-key, OAuth2 + PKCE) | ✅ | | Retries with jittered exponential backoff | ✅ | | Per-client timeouts (connect + overall) | ✅ | | Pagination — cursor, page, offset, **link-header** | ✅ | | SSE streaming with cancellation | ✅ | | Webhook verification (HMAC / Standard / Stripe) | ✅ | | File uploads (multipart) | ✅ | | Polling helpers (`waitFor`) | ✅ | | Telemetry hooks | ✅ | | Rich types (discriminated unions, enums, nullable distinguished from optional) | ✅ | | Typed `ApiError` hierarchy | ✅ | | Per-method snippet emission | ✅ | The surface differs idiomatically by language — async iterators in TypeScript, generators in Python, channels in Go — but the capability is the same everywhere, and the contract runners prove it. #### More than SDKs The same IR fans out beyond client SDKs: an [Astro documentation site](/docs/generated-docs-site) with per-language snippet tabs, a multi-mode [MCP server](/docs/mcp-server) (including Code Mode), and a Terraform provider — all regenerated together when your spec changes. #### When Glotto is the right fit - You want **idiomatic, owned** SDKs without writing emitters yourself. - You ship to **React Native** and want a first-class target, not a browser-TS approximation. - You live on **GitLab or Bitbucket**, not only GitHub. - You want **parity you can prove** — compile-checked, contract-tested, drift-gated — rather than a vendor's word for it. Already on Stainless? See the [migration guide](/docs/migrate-from-stainless) — Glotto's SDK languages are a superset, and [migrating from Stainless](/docs/migrate-from-stainless) converts your config in one command. ### The published spec repo Source: https://glotto.dev/docs/spec-repo/ Your API specification kept in a git repo you own — base, post-transform and code-sample-decorated — provably current on every build, on any of four forges. Almost everything downstream of your API reads a spec document. A docs platform renders one, an agent grounds itself in one, a partner writes against one, and your API's change history *is* the sequence of them. The document that matters most is the one your SDKs were actually generated from — and that one has usually lived inside a build, unaddressable, while the copy everyone reads drifts away from it. The `spec_repo` target turns that document into a **standing guarantee**: a git repository you own that carries your specification, republished on every build, with a commit only when your API actually changed. What it gives you is not a serialization — anything can write a YAML file. It is that the URL a consumer polls is provably current, and provably the same document the SDKs were built from, across every build and every forge, forever. Enable it under `targets:` in `glotto.yml`: **The document your SDKs were built from, published** The target as a real demo declares it, and the index that one real generate run over that demo published. The README lists exactly the variants × formats the config selected — three variants in two serializations, six documents, named for what they are rather than when they were written. Derived from `examples/endpoint-migration/inputs` — every byte below is sliced from that demo or from one real generate run over it. **Your glotto.yml — the spec_repo target, in full** (`examples/endpoint-migration/inputs/glotto.yml` `targets → spec_repo`) ```yaml # The specification itself, published to a git repo you own alongside the SDKs. # `repo` / `repo_provider` / `repo_path` are the same release-repo coordinates # every target takes; `variants` and `formats` choose which documents land there. spec_repo: repo: acme/api-spec repo_provider: github repo_path: spec variants: [base, with_transforms, with_code_samples] formats: [yaml, json] ``` **glotto generate** **What Glotto emits — the published repo's index, listing exactly what it carries** (`spec_repo/README.md` `whole file`) ```md # Acme Data API — API specification The OpenAPI specification for Acme Data API (version 2.0.0), published and maintained by Glotto. ## Contents - `spec.base.json`, `spec.base.yaml` — the spec exactly as supplied to Glotto, with excluded operations removed. - `spec.with-transforms.json`, `spec.with-transforms.yaml` — the spec after every configured correction in `transforms:` — the document the SDKs were generated from. - `spec.with-code-samples.json`, `spec.with-code-samples.yaml` — the spec with per-operation SDK code samples embedded, for a documentation platform to render. ## How this repository is maintained Every file here is generated. Glotto regenerates the whole set from the source specification on each build and publishes it as a single commit, so a hand-edit to any file will be overwritten by the next build — send spec changes upstream to the source specification instead. A build that changes nothing publishes nothing, so every commit in this history is a real change to the API surface, and the commit message states what changed. The repository's git log is therefore a changelog of the API itself. ``` sha256 `d7e955a9c175c690f1d4bcd5fd23e04c25570d84e1f6142aa57e9e886beae29f` `variants` and `formats` are optional — omit them and you get every variant your input can produce, in YAML. `repo` is `owner/name` (Azure Repos: `org/project/repo`), `repo_provider` is one of `github` / `gitlab` / `bitbucket` / `azure-repos`, and `repo_path` publishes into a subtree of a monorepo. All three are the same release-repo coordinates every target takes, documented in the [`targets` reference](/docs/glotto-yml#targets). `glotto generate` then writes `/spec_repo/` and makes **no network write of any kind**: the emission is hermetic and deterministic, so a self-hosted build commits the tree with its own credentials and the hosted platform publishes it for you (below). #### What it emits | Path | What it is | | --- | --- | | `spec.base.yaml` / `spec.base.json` | Your spec exactly as you supplied it, with excluded operations removed. | | `spec.with-transforms.yaml` / `.json` | The same spec after every correction in your [`transforms`](/docs/transforms) block — **the document your SDKs were generated from.** | | `spec.with-code-samples.yaml` / `.json` | The spec with per-operation SDK code samples embedded as `x-codeSamples`, for a documentation platform to render. | | `README.md` | A generated index: your API's title and version, one line per published variant, and a statement that the tree is regenerated and republished on every build — so a reader knows not to hand-edit it. | One file per `variants` × `formats` pair, named for what it is rather than when it was written, so a consumer can hard-code a raw URL and keep it forever. The `with-transforms` variant is the one worth pointing your tooling at. It is not a reconstruction of what the SDKs were built from — Glotto folds your transforms **once** and hands the same document to the code generators and to this target, so "the published spec is what the SDKs implement" is an equality rather than an intention. #### Which variants your input can publish Availability follows what your pipeline actually produces, and a gap is always a loud refusal rather than a missing file. That distinction matters more here than anywhere else Glotto emits: a published spec repo is a public surface, so an absent document reads to a consumer as a fact about your API ("this API has no code samples") rather than as a Glotto refusal. | Input | Publishable variants | | --- | --- | | **OpenAPI** | All of them. `with_code_samples` additionally requires [`openapi.code_samples.formats`](/docs/glotto-yml) to be set — there is nothing to embed otherwise. | | **AsyncAPI** | `base` only. The transform engine is OpenAPI-shaped and never runs on an AsyncAPI document, so a `with-transforms` file there would be a copy of the base one published under a name asserting corrections were applied. The code-sample decorator is OpenAPI-only for the same reason. | | **GraphQL** | None. A GraphQL input never becomes a canonical spec document, so there is nothing to publish in any variant or format — the same reason the spec changelog declines it. This is a boundary of the target, not work in progress: no artifact is being withheld from you. | Requesting a variant your input cannot produce fails with [`GLOTTO_SPEC_REPO_VARIANT_UNAVAILABLE`](/docs/diagnostics-sdk-generation#the-spec-repo-target), naming which variant and why; selecting the target at all with a GraphQL input fails with [`GLOTTO_SPEC_REPO_INPUT_UNSUPPORTED`](/docs/diagnostics-sdk-generation#the-spec-repo-target). #### Published to a repo you own On the hosted platform, the tree is published on every build to the repo named by `repo` — a repository **you** own and have connected, on **GitHub, GitLab, Bitbucket or Azure Repos**. Glotto never creates the repository; it writes into one you already control, which is the right trust boundary for a service pushing to your forge. The publish lands as a commit directly on a branch rather than as a pull request you have to merge, because the artifact's whole value is a branch a docs platform can poll without lag. It is idempotent: the same generation published twice changes nothing. `repo_path` scopes the write to a subtree, so a spec repo can share a monorepo with anything else you keep there. If you later narrow `variants`, a previously published file stays on the branch — Glotto's publish path adds and updates, and deliberately never deletes on your behalf. The publish result **reports** every such leftover path rather than leaving you to notice it, so you can remove it deliberately. #### Every commit means something Two properties of the git history are decisions, not implementation details: - **A build that changes nothing makes no commit.** Publishing on every build means most builds have nothing to say, and an empty commit per build would turn the history — the point of the whole artifact — into noise you have to filter. - **The commit message is derived from the spec diff.** The previously published document is read back from the branch and compared against the new one, and the rendered spec changelog becomes the message body; a first publish gets an initial-publish subject and no body. There is no model in the loop, so the message is reproducible and cannot describe a change that did not happen. Together those make `git log` on the published repo a changelog of your API itself: every entry is a real change, and it says what changed. #### Kept true, not just generated Like every Glotto artifact, the spec repo is a deterministic emission — byte-stable across repeated runs, YAML rendering pinned so a renderer upgrade cannot silently re-wrap your document, locked by golden tests, and regenerated in lockstep with your SDKs, docs site, MCP server and CLI. The [drift gate](/docs/drift-detection) proves the committed copy never lags your spec. That is the guarantee on offer. Exporting a spec file once is trivial; keeping a published, polled, externally-referenced copy true against every change, on whichever forge you use, is the part that decays. Add an operation, correct a schema with a transform, enable code samples — the spec repo is provably current on the next build. ### Streaming Source: https://glotto.dev/docs/streaming/ Consume server-sent events and streamed responses. Streaming endpoints (SSE / NDJSON) return an async iterator of decoded events — `for await` over it: ```ts for await (const event of client.chat.createChatCompletion({ body })) { console.log(event); } ``` The generated method reads the response stream, splits frames, and yields each decoded event, so you never touch the `ReadableStream` directly. #### Opening and closing a stream Opening a declared stream uses the same request policy as an ordinary API call: authentication, retry limits, request options, and request telemetry all apply. The opening timeout ends at the first byte or successful WebSocket upgrade; it does not limit how long you may consume a healthy stream. Close or cancel when you stop consuming early. Use the scoped or closeable surface shown in your generated README, such as a context manager, `using` scope, or an explicit `close` in cleanup. A legacy iterator whose language cannot observe a plain loop break still needs explicit cancellation. Once the SDK exposes stream data, a later read failure surfaces to your code without reconnecting or replaying earlier data. #### Each event is decoded into its typed model Every yielded event is JSON-decoded into the endpoint's response model — the iterator's element type is that model, not a raw string. If a frame's payload is **not valid JSON**, the SDK **surfaces a parse error** to your loop rather than handing you a malformed value typed as your model — exactly like a non-streaming call surfaces a decode error. Wrap the loop if you want to handle it: ```ts try { for await (const event of client.chat.createChatCompletion({ body })) { console.log(event); // typed, JSON-decoded } } catch (err) { // a frame whose payload wasn't valid JSON, surfaced here } ``` This is consistent across every typed SDK (TypeScript, React Native, Python, Go, Rust, Swift, Dart, Elixir, PHP, Ruby): a malformed streaming frame is never silently dropped or yielded as a raw string. On PHP and Ruby — where the typed surface is the PHPDoc / YARD annotation that phpstan, psalm, and Sorbet read — the parsed-frame iterator (`streamJson()` / `stream_json`) yields the decoded model, and the raw string-frame iterator beside it (`stream()` / `stream`) is the deliberate escape hatch when you want the unparsed frame. #### Multiple event kinds: discriminated event unions (`event:` routing) Real-world streaming APIs often emit **several event kinds** on one stream, discriminated by the SSE `event:` field — `event: message_delta` carrying one schema, `event: message_completed` another. Declare the mapping on the operation with the `x-glotto-event-types` extension in your OpenAPI spec: ```yaml # openapi.yaml (on the streaming operation) x-streaming: sse x-glotto-event-types: message_delta: '#/components/schemas/MessageDelta' message_completed: '#/components/schemas/MessageCompleted' ``` Every SDK then yields a **discriminated union** of those models instead of a single event type (TypeScript: `AsyncGenerator` behind a named `StreamEvent` alias; Swift/Rust an `enum`, Java/Kotlin/C#/Dart a sealed type, Python a `Union`, and the dynamic engines yield the rehydrated model per event). Each frame is decoded as the schema its `event:` name maps to; a frame with **no** `event:` field routes as `message` (the SSE default), and a frame whose event name isn't in the map is **skipped** — the same posture as the browser `EventSource`, so servers can add new event kinds without breaking older SDKs. `on_event` rules (below) still run first, so sentinel handling composes with routing. #### Agent helpers: accumulating streaming tool-call deltas Agent-style APIs stream a tool call's arguments as **partial JSON fragments** spread across many events — an index identifying the call, `id`/`name` metadata arriving once, and argument fragments that only parse after the last one lands. Every SSE-streaming SDK ships a `ToolCallAccumulator` that does the reduction for you (the first of the agent-primitive helpers): feed it the delta fields off your typed event union, then take the assembled calls. ```ts const acc = new ToolCallAccumulator(); for await (const event of client.chat.createChatCompletion({ body })) { if (isToolCallDelta(event)) { acc.addDelta({ index: event.index, id: event.id, name: event.name, argumentsDelta: event.arguments_delta }); } } for (const call of acc.toolCalls()) { // call.name, call.id, call.arguments (parsed object) — call.argumentsJson keeps the raw bytes } ``` The semantics are identical in all 13 languages: deltas accumulate per index (`id`/`name` are set-once, argument fragments concatenate verbatim), `toolCalls()` is a repeatable snapshot sorted by index — safe to call mid-stream for partial UIs — and `arguments` is populated only once the accumulated string parses as a JSON object (a malformed or incomplete accumulation never throws; the raw string stays on `argumentsJson`). Because you wire the deltas yourself, the helper works with any wire dialect — OpenAI-style `tool_calls`, Anthropic-style `input_json_delta`, or your own — not just a blessed schema. #### The rest of the agent cohort `ToolCallAccumulator` is the one agent primitive tied to streaming. The others — token counting (`estimateTokens` and `Tokenizer`), context budgeting (`TokenBudget`), budget-aware retry (`RetryBudget`), vector math (`VectorMath`) and structured decode (`StructuredOutput`) — ride **response-shape** gates instead, so an API that never streams still emits the ones that fit it. See [Agent primitives](/docs/agent-primitives) for the full cohort and which shapes emit what. #### Terminal sentinels (`[DONE]`) and other non-JSON frames Many streaming APIs end with a non-JSON sentinel such as `data: [DONE]`. Because `[DONE]` isn't valid JSON, an **unconfigured** sentinel reaches the decode step and surfaces a parse error at the end of the stream. Declare it as a termination rule so the stream ends cleanly **before** any decode — that's what [`streaming.on_event`](/docs/glotto-yml-api-surface#streaming) is for: ```yaml # glotto.yml streaming: on_event: - { data: "[DONE]", action: done } # end the stream cleanly on this sentinel ``` `on_event` rules match the **raw** event payload, so `[DONE]` (and other non-JSON sentinels) match before the SDK tries to decode them. `action` is `done` (clean end, drains the stream), `break` (stop immediately), `fatal_error` (raise), or `skip` (drop the matched event and keep going). See [`streaming.on_event`](/docs/glotto-yml-api-surface#streaming) for the full rule grammar. If a stream carries benign noise you'd rather tolerate than terminate on — keepalive / control frames, vendor sentinels — use `skip` to drop those frames without ending the stream or raising. Because matching happens before decode, a `skip`-matched frame never reaches the parser: ```yaml # glotto.yml streaming: on_event: - { data: "[DONE]", action: done } # known terminator - { fallthrough: true, action: skip } # tolerate anything else unrecognized, keep going ``` Without a `skip` rule the strict default is unchanged — an unrecognized non-JSON frame still surfaces a parse error, so you only loosen tolerance for exactly the frames you opt into. #### Dual-mode endpoints (`stream: true`) Most AI APIs put both modes on **one endpoint**: a request field — usually `stream` — selects between a streamed sequence of chunks and a single buffered JSON body, and the two have different response types. Declare which field that is, and Glotto emits **two methods** from the one operation: ```yaml # glotto.yml streaming: dual_mode: createChatCompletion: param_discriminator: stream # the request field selecting the mode stream_event_model: ChatCompletionChunk # optional: the per-event model params_type_name: ChatCompletionParams # optional: names the shared params model method_suffix: streaming # optional: default "streaming" ``` ```ts // buffered — returns one ChatCompletion const completion = await client.chat.completions.createChatCompletion({ model, messages }); // streaming — yields ChatCompletionChunk for await (const chunk of client.chat.completions.createChatCompletionStreaming({ model, messages })) { process.stdout.write(chunk.delta); } ``` You never pass the discriminator yourself. Glotto **removes it from both methods' parameters** and sends the right value on the wire for you — `stream: false` from the buffered method, `stream: true` from the streaming one — so it is impossible to call the streaming method and get a buffered response back. Both methods share one params type, since with the discriminator gone their inputs are identical. The split happens in every language Glotto generates, always as **two methods** rather than an overload, so the surface reads the same whichever SDK your users pick (`create_chat_completion` / `CreateChatCompletionStreaming` / `create_chat_completion_streaming`, per language convention). The per-event type comes from your spec when the endpoint documents `text/event-stream` (or NDJSON) alongside `application/json` on the same response. When it doesn't, name it with `stream_event_model` — otherwise Glotto **refuses the entry and tells you** ([`GLOTTO_CONFIG_DUAL_MODE_REFUSED`](/docs/diagnostics)) rather than generating an iterator that yields the buffered body once per chunk. Everything else keeps describing the one endpoint your API actually has: your reference docs, MCP tools, mock server and breaking-change detection all still see a single operation, so turning `dual_mode` on never reports as an API change. #### Event APIs (AsyncAPI) For pub/sub APIs, Glotto emits `send`/`subscribe` methods — publish a typed payload, or register a handler: ```ts events.onPetAdded((pet) => console.log(pet.name)); await events.publishPetAdded({ name: 'Rex' }); ``` On React Native, streaming uses an optional `ReadableStream` polyfill — see the [React Native guide](/docs/react-native). ### Swift Source: https://glotto.dev/docs/swift/ An idiomatic Swift package of Codable types with throwing operations and an APIErrorKind enum, plus Keychain storage and connectivity-aware retries. The Swift SDK is an idiomatic package emitted from the same `GlottoIR` as every other target. Operations under a declared resource hang off an accessor — `client.pets.listPets()` — while an operation you leave ungrouped stays a method on `Client`. Request/response shapes are `Codable` Swift types, so you work with real types rather than `Data`. #### Quickstart ```swift // In your Package.swift, add the generated package to `dependencies` // (a local `.package(path:)` or your own published URL), then its library product: .product(name: "Petstore", package: "Petstore") ``` ```swift import Foundation import Petstore let client = Client( baseURL: "https://api.petstore.example", token: ProcessInfo.processInfo.environment["PETSTORE_TOKEN"]! ) // a single call let pet = try await client.pets.createPet(body: NewPet(name: "Rex")) // For an operation configured with pagination: let pets = client.pets.listPetsScoped() defer { pets.close() } for try await pet in pets { print(pet.name) } ``` #### Typed models Each `GlottoIR` model becomes a `Codable` struct; operations decode and return the typed response and accept a typed request body. Discriminated unions resolve to the right enum case. #### Typed errors Operations `throw` an `APIError` struct carrying the parsed error body, with an `APIErrorKind` enum so callers `switch error.kind` / `if case .notFound` rather than matching status codes. See [Errors](/docs/errors). #### Pagination `Scoped` companions return a `StreamScope` that fetches another page only when you advance beyond the current page. Put `close()` in a `defer` block to release it when your code breaks, returns, or throws. Existing `AsyncThrowingStream` methods remain available; cancel their consuming task when leaving early. See [Pagination](/docs/pagination). Manual `Page` companions expose typed items and explicit `nextPage()` navigation. The throwing `response()` accessor decodes the full wrapper without another request. See [manual pagination](/docs/pagination). #### Retries & backoff Pass `RequestOptions` to override headers, timeouts, retry counts and idempotency for an individual operation. Method defaults apply before caller overrides. See [request options and defaults](/docs/retries). Transient failures (`5xx`, `429`, transport errors) retry with exponential backoff and jitter, configurable per client. See [Retries & timeouts](/docs/retries). #### SSE, NDJSON, and WebSocket streaming SSE and NDJSON `Scoped` companions provide the same `StreamScope` ownership pattern as pagination. The opening deadline and retry policy apply until the first response byte. After that boundary, streams outlive the opening timeout and read failures surface without replay. WebSocket operations retain their typed connection API. Put `connection.close()` in `defer` when leaving a message loop early. The upgrade uses the configured client, and a successful 101 ends the opening deadline before the first application frame arrives. See [Streaming](/docs/streaming) and [Authentication](/docs/authentication). #### Linux transport requirements SDKs with binary downloads, WebSocket operations, GraphQL subscriptions or event channels require Swift 6.1 or newer and the system zlib development library on Linux (`zlib1g-dev` on Debian/Ubuntu). Their Linux transport uses SwiftNIO and NIOSSL; Apple defaults use Foundation. SDKs without these capabilities retain their Swift 6.0 requirement. For `Client` binary-download and WebSocket methods, supplying `session:` on Linux also requires an explicit `streamingTransport:`. Choose `NIOStreamingTransport()` or implement `StreamingTransport` for custom policy. The built-in transport verifies TLS certificates and hostnames and accepts optional PEM trust roots and a client certificate/key; it does not follow redirects or supply cookie storage, caching, proxies or authentication-challenge handling. A supplied `URLSession` continues to serve ordinary requests, and its policy is not copied into the streaming transport. `GraphQLTransport` and `EventTransport` remain the custom-transport interfaces for their respective clients. The generated README includes the applicable configuration example. #### Unknown response fields Your API can add a response field without it being a breaking change — but a generated `Codable` struct decodes only its declared `CodingKeys`, so the key would be dropped. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized. ```swift let pet = try await client.pets.createPet(body: body) // A field your API started returning after this SDK was generated. if let species = pet.extraFields["species"] { print(species) } // Re-encoding preserves it — a read-modify-write never silently drops it. let json = try JSONEncoder().encode(pet) ``` `extraFields` is a `let` holding `[String: JSONValue]`, so nested objects and arrays survive intact, and retention is recursive. Your existing construction still compiles: the memberwise initializer keeps its original signature and starts the model with an empty bag. The retained fields are read-only by design. To *send* a field your spec doesn't model yet, use the per-call extra-body escape hatch rather than writing to the retained bag. #### Binary downloads Binary endpoints return an owned `BinaryResponse` with response metadata. `readAll(maxBytes:)` bounds delivered bytes; `copy(to:)` accepts a `FileHandle` or async sink closure. Use `defer { download.close() }` after opening an incremental reader. Mixed JSON/binary operations expose an explicit result branch. See [file transfers](/docs/file-transfers). #### File uploads `multipart/form-data` operations build the multipart body from their fields for you; an `application/octet-stream` operation takes a positional `body: Data` argument sent raw with the right `Content-Type`. #### Keychain secure storage > **Opt-in.** The Keychain target is compile-verified against a real Apple toolchain (macOS) by the > scheduled `mobile-compile` CI lane — the > `#if canImport(Security)` Keychain store builds on a real Apple SDK. The Swift SDK above is unaffected. Opt in with `targets.swift.keychain: true` in your `glotto.yml` to add a pluggable secure token store to the bearer client — the same native-mobile secure-storage seam the React Native SDK ships: ```yaml targets: swift: keychain: true ``` The client gains an optional `tokenStore: SecureTokenStore?` parameter; when supplied, the bearer token is read from it per request (falling back to the static `token`). A Keychain-backed implementation (`KeychainTokenStore`, over the `Security` framework `SecItem` generic-password API) is generated for you. `SecureTokenStore` is a plain protocol, so you can supply your own store too. The Keychain implementation is `#if canImport(Security)`-guarded, so the SDK still compiles on Linux (where the `Security` framework is absent) — exactly like the engine's streaming Linux fallbacks. ##### Biometric-gated storage > **Opt-in, experimental.** `targets.swift.keychainBiometric: true` requires > `targets.swift.keychain: true` and is compile-verified on the scheduled `mobile-compile` lane. > The plain Keychain store above stays the > default and byte-identical. For credentials that must be released only after the user authenticates, add `targets.swift.keychainBiometric: true`: ```yaml targets: swift: keychain: true keychainBiometric: true ``` This additionally emits a `BiometricKeychainTokenStore` (alongside the plain `KeychainTokenStore`) whose Keychain item carries a `SecAccessControlCreateWithFlags(.biometryCurrentSet)` access control, so the OS presents the biometric prompt on access. An optional `LAContext` (LocalAuthentication-guarded) lets you reuse an existing authentication or customize the prompt. It rides the same `#if canImport(Security)` guard, so the SDK still compiles on Linux. #### Connectivity-aware retries & lifecycle-aware backoff > **Opt-in.** The concrete monitors are compile-verified against a real Apple toolchain (macOS) by the > scheduled `mobile-compile` CI lane — the > `#if canImport(Network)` / `#if canImport(UIKit)` monitors build on a real Apple SDK. The Swift SDK > above is unaffected. The retry loop can be made network- and lifecycle-aware — the two remaining native-mobile seams the React Native and Kotlin-Android SDKs ship. They are **two independent opt-in flags**: ```yaml targets: swift: connectivity: true # pause retries while the device is offline lifecycle: true # pause backoff while the app is backgrounded ``` With `connectivity` on, the `Client` gains an optional `connectivity: ConnectivityMonitor?` parameter. Before each (re)try, a known-offline state is treated as transient — the client waits and re-checks rather than burning the request. A concrete `NWPathMonitorConnectivity` (over the `Network` framework) is generated for you. With `lifecycle` on, the `Client` gains an optional `lifecycle: AppLifecycle?` parameter. While the app is backgrounded (where the OS freezes timers), the client pauses the backoff **without consuming a retry attempt** until the app returns to the foreground. A concrete `UIApplicationLifecycle` (over `UIKit` lifecycle notifications) is generated for you. `ConnectivityMonitor` and `AppLifecycle` are plain protocols, so you can supply your own implementations too. Both concrete monitors are `#if canImport(...)`-guarded (`Network` / `UIKit`), so the SDK still compiles on Linux where those frameworks are absent — exactly like the Keychain store above. ### Telemetry hooks Source: https://glotto.dev/docs/telemetry/ Observe every request with the client's log / metric / trace callbacks. Every generated client accepts an optional set of **telemetry hooks** — callbacks fired around each request so you can log, emit metrics, or open a trace span without wrapping the client. They are off unless you supply them, and add no overhead when absent. **TypeScript** (regenerated + byte-diffed in CI `a0f8af4b462c`) ```ts import { Client } from 'glotto-sdk'; const client = new Client({ hooks: { onRequest: ({ method, url }) => console.log('request', method, url), onResponse: ({ status, durationMs }) => console.log('response', status, durationMs), }, }); ``` **React Native** (regenerated + byte-diffed in CI `a1fefe7d3458`) ```ts import { Client } from 'glotto-sdk'; const client = new Client({ hooks: { onRequest: ({ method, url }) => console.log('request', method, url), onResponse: ({ status, durationMs }) => console.log('response', status, durationMs), }, }); ``` **Python** (regenerated + byte-diffed in CI `2242160f8958`) ```python from glotto_sdk import Client, TelemetryHooks hooks = TelemetryHooks( on_request=lambda ctx: print(ctx.method, ctx.url), on_response=lambda ctx: print(ctx.status, ctx.duration_ms), ) client = Client(hooks=hooks) ``` **Go** (regenerated + byte-diffed in CI `f0c565cc46f8`) ```go package main import ( "log" sdk "example.com/glotto-sdk-go" ) func main() { client := sdk.NewClient(sdk.WithHooks(&sdk.TelemetryHooks{ OnRequest: func(ctx sdk.TelemetryContext) { log.Printf("%s %s", ctx.Method, ctx.URL) }, OnResponse: func(ctx sdk.TelemetryContext) { log.Printf("%d %.0fms", ctx.Status, ctx.DurationMs) }, })) _ = client } ``` **Java** (regenerated + byte-diffed in CI `12c05e098737`) ```java import com.glotto.Client; import com.glotto.TelemetryHooks; public class Snippet { public static void main(String[] args) { Client client = Client.builder().token("").build(); client.setHooks(new TelemetryHooks( ctx -> System.out.println(ctx.method() + " " + ctx.url()), ctx -> System.out.println(ctx.status() + " " + ctx.durationMs()), null, null )); } } ``` **Kotlin** (regenerated + byte-diffed in CI `0f5e53ae2522`) ```kotlin import com.glotto.Client import com.glotto.TelemetryHooks fun main() { val hooks = TelemetryHooks( onRequest = { ctx -> println("${ctx.method} ${ctx.url}") }, onResponse = { ctx -> println("${ctx.status} ${ctx.durationMs}") }, ) val client = Client(token = "", hooks = hooks) println(client) } ``` **C#** (regenerated + byte-diffed in CI `c5013ce0e88b`) ```csharp using Glotto; var hooks = new TelemetryHooks( OnRequest: ctx => Console.WriteLine($"{ctx.Method} {ctx.URL}"), OnResponse: ctx => Console.WriteLine($"{ctx.Status} {ctx.DurationMs}")); var client = new Client(new GlottoClientOptions { Hooks = hooks }); ``` **PHP** (regenerated + byte-diffed in CI `1acd16081b46`) ```php print($ctx->method . " " . $ctx->url . PHP_EOL), onResponse: fn ($ctx) => print($ctx->status . " " . $ctx->duration_ms . PHP_EOL), ); $client = new Glotto\Client(token: '', hooks: $hooks); ``` **Ruby** (regenerated + byte-diffed in CI `a2b854f6593f`) ```ruby require 'glotto' hooks = Glotto::TelemetryHooks.build( on_request: ->(ctx) { puts "#{ctx.method} #{ctx.url}" }, on_response: ->(ctx) { puts "#{ctx.status} #{ctx.duration_ms}" } ) client = Glotto::Client.new(token: '', hooks: hooks) ``` **Rust** (regenerated + byte-diffed in CI `716e9ec181d1`) ```rust let hooks = TelemetryHooks { on_request: Some(Box::new(|ctx| println!("{} {}", ctx.method, ctx.url))), on_response: Some(Box::new(|ctx| println!("{:?} {:?}", ctx.status, ctx.duration_ms))), ..TelemetryHooks::default() }; let client = Client::default().with_token("").with_hooks(hooks); ``` **Swift** (regenerated + byte-diffed in CI `0cc357352baf`) ```swift import Foundation import GlottoSdk let hooks = TelemetryHooks( onRequest: { ctx in print(ctx.method, ctx.url) }, onResponse: { ctx in print(ctx.status as Any, ctx.durationMs as Any) } ) let client = Client(token: "", hooks: hooks) ``` **Dart** (regenerated + byte-diffed in CI `67567d6df435`) ```dart import 'package:glotto_sdk/glotto_sdk.dart'; final hooks = TelemetryHooks( onRequest: (ctx) => print('${ctx.method} ${ctx.url}'), onResponse: (ctx) => print('${ctx.status} ${ctx.durationMs}'), ); final client = Client(token: "", hooks: hooks); ``` **Elixir** (regenerated + byte-diffed in CI `b97af402f1d7`) ```elixir hooks = %{ on_request: fn ctx -> IO.inspect({ctx.method, ctx.url}) end, on_response: fn ctx -> IO.inspect({ctx.status, ctx.duration_ms}) end } client = Glotto.new(token: "", hooks: hooks) ``` #### The callbacks The `hooks` object accepts `onRequest`, `onResponse`, `onError`, and `onRetry` (named idiomatically per language — `onRequest`/`onResponse` in TypeScript, `on_request`/`on_response` in Python, `OnRequest`/`OnResponse` in Go). Each receives a telemetry context with the request method, URL, and — on the response side — the status and elapsed duration, so you can wire OpenTelemetry HTTP client spans or your own metrics pipeline. #### Turning hooks off from the environment Every generated client also reads **`OTEL_SDK_DISABLED`** from the process environment, once when the client is constructed. When it is set to `true`, the hooks you passed are ignored and no callback fires — so an operator can silence client-side telemetry in an environment without a code change or a redeploy of the calling service. ```bash OTEL_SDK_DISABLED=true ./your-service ``` **The value must be `true`, in any casing.** `true`, `TRUE` and `True` all disable telemetry, in every one of the generated SDKs. **Every other value leaves telemetry enabled**, including `1`, `yes`, `0`, `false`, an empty value, and the variable being unset. That `1` does *not* disable telemetry is deliberate and worth stating plainly, because it is the spelling most people try first. It is the rule [OpenTelemetry's own specification](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) sets for this variable: it declares `OTEL_SDK_DISABLED` to be of the Boolean type, and defines that type as true *only* for the case-insensitive string `true`, with implementations explicitly forbidden from accepting anything wider. Following that rule is what makes the switch mean the same thing in a Glotto SDK as it does in the rest of your OpenTelemetry stack — your language agent, your auto-instrumentation, your collector — so one variable set once behaves the same everywhere. A Glotto SDK that also honoured `1` would disable itself while the conformant implementations beside it kept exporting, which is worse than not honouring it at all. **A value the SDK cannot read is not silent.** Set it to anything that is neither `true` nor `false` — `1`, `yes`, `0`, a typo — and the client writes one line to your language's warning channel when it is constructed, naming the value it ignored: ``` glotto: OTEL_SDK_DISABLED is set to 1, which is not a boolean; telemetry stays enabled. Only true (any casing) disables it. ``` It goes to the warning sink, never to standard output, so it cannot corrupt a program whose stdout carries a protocol: `console.warn` in TypeScript and React Native, `logging` in Python, standard error in Go, Ruby, Java, Kotlin, C#, Rust, Swift and Dart, `error_log` in PHP, `IO.warn` in Elixir. Nothing is written when the variable is unset, empty, `true` or `false` — a correct configuration produces no output at all, so the line only ever appears while there is something to fix. Two further things follow from the kill switch being an *environment* setting rather than a config one, and both are deliberate: - **It is not a `glotto.yml` key**, and you will not find it in your configuration. `glotto.yml` describes what gets generated; this decides what an already-generated client does at runtime, in a particular deployment. Baking it into the config would make silencing telemetry a regeneration. - **It is read once, at construction.** Changing the variable in a running process does not affect a client that already exists — set it before the process starts. #### Hooks vs. telemetry headers These callbacks are a **client-side observability seam** and are distinct from `client_settings.telemetry_headers` — a separate, opt-in feature that makes every request *send* the `X-Glotto-Retry-Count` and `X-Glotto-Timeout` headers so your **server** can see the client's retry state and timeout budget. Hooks observe locally; telemetry headers tell the server. See the [`glotto.yml` reference](/docs/glotto-yml) for `telemetry_headers`. ### The generated Terraform provider Source: https://glotto.dev/docs/terraform/ A publishable terraform-plugin-framework provider for your API — resources, data sources, Registry docs and acceptance tests, all derived from your spec. If your API creates things customers want to manage as infrastructure — projects, webhooks, API keys, environments — they will eventually want to declare them in Terraform rather than script them by hand. From the same [Glotto IR](/docs/glotto-ir) as your SDKs, docs site and MCP server, Glotto generates a **complete, publishable Terraform provider**: a Go module built on `terraform-plugin-framework`, with a resource per resource in your API, data sources, Registry documentation, and an acceptance-test scaffold. Enable it in `glotto.yml`: ```yaml targets: terraform: {} ``` `glotto generate` then writes the provider module under `/terraform/`. #### What gets emitted A module you can `go build`, tag, and publish — not a scaffold to fill in: | Path | What it is | |---|---| | `go.mod` | the provider module, pinning `terraform-plugin-framework` | | `main.go` | the `providerserver` entrypoint | | `internal/provider/provider.go` | the provider wiring, an embedded HTTP client configured from provider config, and every resource, data source and ephemeral resource | | `internal/provider/*_test.go` | a `TF_ACC`-gated acceptance-test scaffold, one per managed resource, plus the shared harness | | `docs/` | the Terraform Registry docs tree — `index.md`, `resources/*.md`, `data-sources/*.md` | | `terraform-registry-manifest.json`, `.goreleaser.yml` | what the Registry's publishing flow expects | The Registry docs are emitted natively from the same schema derivation as the provider code, so they cannot drift from the schema they document — and no `tfplugindocs` or `terraform` binary is needed to produce them. #### Resources and data sources Each resource in your API becomes a managed resource type, named from the provider's type prefix and the resource's path — `glotto_webhook`, and `glotto_project_environment` for a subresource. Its CRUD operations map to Terraform's lifecycle from the operations your spec declares — create, read, update, delete — and resources are importable by id. The provider's own publishing identity is not yet configurable from `glotto.yml`: the type prefix is `glotto` and the Registry namespace defaults to `glotto-dev`. Surfacing those as per-target config is not supported yet. Where a resource has a read operation, it also becomes a **data source**, so a practitioner can reference something they did not create. Collection endpoints emit a list data source alongside the single-item one. An operation whose result is a short-lived credential — marked `x-glotto-terraform-ephemeral` in your spec — registers as an **ephemeral resource** instead, so its value never lands in Terraform state. Renewal and session-close hints (`x-glotto-terraform-renew-at`, `-ttl-seconds`, `-close-operation`) drive the corresponding framework callbacks. #### Attribute shaping Attributes are derived from your models, and the shaping follows standard OpenAPI where it can: - `readOnly: true` properties become `Computed` — server-assigned, never written from config. - `required` properties become `Required`; everything else is `Optional`. - `uniqueItems: true` arrays become sets rather than lists, so ordering is not spurious diff noise. Where the spec's own vocabulary isn't enough, the `x-glotto-terraform-*` extensions override the inference per property — and those overrides win, so you can correct a schema you don't control without editing it. Migrating from Stainless? Glotto translates the equivalent `x-stainless-*` extensions into these automatically — see [Bring your Terraform attribute shaping across](/docs/migrate-from-stainless#bring-your-terraform-attribute-shaping-across). #### Publishing The emitted module is the unit you publish: tag it and let the included `.goreleaser.yml` build the release artifacts the Terraform Registry expects, with `terraform-registry-manifest.json` declaring the protocol version. The provider carries a `LICENSE` file; Glotto stamps no licence terms into your provider's source. #### Kept true, not just generated Like every Glotto artifact, the provider is a deterministic projection of your spec: byte-stable output, locked by golden tests, and **compile-verified** — the generated Go module is built in CI on every change to the engine, so a provider that would not compile never ships. It is regenerated in lockstep with your SDKs and docs from the same IR, and the [drift gate](/docs/drift-detection) proves the committed provider never lags your spec. Add a resource, mark a field read-only, deprecate an endpoint — your provider is provably current on the next regeneration, and your practitioners' configurations keep describing a real API. ### Transforms reference Source: https://glotto.dev/docs/transforms/ The glotto.yml transforms that rewrite your OpenAPI spec for cleaner SDKs, with arguments and examples. The `transforms` block in [`glotto.yml`](/docs/glotto-yml) applies **in-config OpenAPI rewrites** before the SDK is generated. They clean up a spec for better SDKs — naming anonymous schemas, flattening compositions, fixing bad examples — **without editing your upstream OpenAPI source**. ```yaml # glotto.yml transforms: - rename_schema: { from: InvoiceDTO, to: Invoice } - dedupe_inline_objects: { threshold: 2 } - extract_ref: { path: /paths/~1pets/post/requestBody/content/application~1json/schema, name: CreatePetBody } ``` #### How transforms run - `transforms` is an **ordered list**; each entry is a single-key object `{ : }`, applied top to bottom. - They run on the **canonical spec, before** the IR is built and any code is generated — so every language target sees the rewritten spec. - Transforms are **fail-fast**: if a transform's target (a schema, a JSON-pointer, an example) does not exist, generation stops with an error rather than silently doing nothing. A transform that can't find its target is almost always a typo or a spec drift you want surfaced. The transforms below are available today. The first six are **spec-cleanup** operations — they make a correct spec generate a nicer SDK. The last two, [`apply_overlay`](https://glotto.dev/docs/transforms/#apply_overlay) and [`merge_document`](https://glotto.dev/docs/transforms/#merge_document), are the **spec-correction** escape hatches for a spec that is simply *wrong*. They differ in how a correction is addressed: `apply_overlay` targets nodes with JSONPath, `merge_document` addresses them by structural position — see [choosing between them](https://glotto.dev/docs/transforms/#which-correction-transform). #### `rename_schema` ```yaml - rename_schema: { from: InvoiceDTO, to: Invoice } ``` **Args:** `{ from: string, to: string }` Renames a component schema and **every `$ref` that points at it**. Use it to give a generated type a clean, idiomatic name (e.g. drop a `DTO`/`Model` suffix) across the whole SDK at once. #### `flatten_composition` ```yaml - flatten_composition: { schema: Charge.allOf } ``` **Args:** `{ schema: string }` — a dotted `"."` target. Flattens a composition keyword on a named schema into a single inline object — e.g. merges the members of an `allOf` so the SDK emits one flat type instead of an awkward intersection. #### `dedupe_inline_objects` ```yaml - dedupe_inline_objects: { threshold: 2 } ``` **Args:** `{ threshold: number }` — default `2`, minimum `2`. Hoists **inline object schemas that appear `threshold` or more times** into a single named component, replacing each occurrence with a `$ref`. This is the biggest lever for shrinking SDK surface area: without it, codegen emits a separate anonymous type per occurrence. #### `extract_ref` ```yaml - extract_ref: path: /paths/~1pets/post/requestBody/content/application~1json/schema name: CreatePetBody ``` **Args:** `{ path: string, name: string }` — `path` is an [RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901) JSON-pointer into the spec (note `~1` escapes `/`), `name` is the new component name. The targeted, one-at-a-time companion to `dedupe_inline_objects`: hoists the **single** inline schema at `path` to `#/components/schemas/` and leaves a `$ref` in its place. Use it when you know exactly which inline schema deserves a name. Fails if `` already exists. #### `fix_invalid_example` ```yaml - fix_invalid_example: { schema: Pet, replace_with: { id: 1, name: "Rex" } } - fix_invalid_example: { schema: Charge } # no replace_with → removes the example ``` **Args:** `{ schema: string, replace_with?: unknown }` Repairs a schema whose `example` is wrong (a bad example breaks generated docs and code samples). It rewrites **only** that schema's `example` key: set it to `replace_with` when present (including an explicit `null`), or remove the `example` entirely when `replace_with` is omitted. Fails if the schema — or its `example` key — doesn't exist, so a stale fix never silently no-ops. #### `lift_union_members` ```yaml - lift_union_members: { union: PaymentMethod } ``` **Args:** `{ union: string }` Names every **inline object member of a discriminated union** — lifting each to a `#/components/schemas/` `$ref` and extending the discriminator mapping — so the union becomes eligible for Glotto's discriminated-union code generation. Without it, a union whose variants are anonymous inline objects can't be narrowed by the generated SDK. Fails fast rather than producing a partial rewrite. #### `apply_overlay` ```yaml - apply_overlay: actions: - target: '$.paths..parameters[?(@.name == "account_id")].schema' description: Account IDs are strings, not numbers update: { type: string } ``` **Args:** an [OpenAPI Overlay 1.0.0](https://spec.openapis.org/overlay/latest.html) Overlay Object — `actions` (≥1), plus the optional `overlay`, `info` and `extends` fields a standalone overlay document carries, so you can paste one in verbatim. Instead of `actions`, you can point at an overlay document you already have with [`source`](https://glotto.dev/docs/transforms/#referencing-an-overlay-document) — exactly one of the two. The **generic escape hatch**. The six transforms above are cleanups: they assume your spec says what its author meant. `apply_overlay` is for when it doesn't — an auto-generated upstream source you can't edit, a parameter typed `integer` that really returns strings, a bogus `format`. Each action names a `target` and either **merges** an `update` into it or **removes** it. We chose the OpenAPI Overlay standard over inventing a proprietary command vocabulary, so your overlay knowledge is portable and your existing overlay documents mean the same thing here. ##### Actions Each entry in `actions` is `{ target, description?, update? | remove? }` — exactly one of `update` or `remove: true`, never both and never neither. - **`target`** — a JSONPath expression evaluated against the whole spec ([subset](https://glotto.dev/docs/transforms/#supported-jsonpath) below). - **`description`** — free text recording *why* the correction exists. Never affects the output. - **`update`** — when the target is an **object**, the properties to merge into it (nested objects merge recursively, so sibling keys survive); when the target is an **array**, a single entry to append. - **`remove: true`** — delete the target from the object or array containing it. Works on any node, including a scalar. Actions apply **in order**, each against the result of the last — so a later action may target something an earlier one created. ##### Referencing an overlay document If your corrections already live in an overlay file — the form Fern's `api.specs[].overlays` uses — reference it instead of pasting the actions in: ```yaml transforms: - apply_overlay: { source: ./corrections.yaml } # relative to your glotto.yml - apply_overlay: { source: https://specs.acme.com/corrections.yaml } - apply_overlay: source: git: { repo: https://github.com/acme/api.git, ref: main, path: overlays/fix.yaml } ``` An entry carries **either** `source` **or** `actions`, never both and never neither. A referenced overlay behaves exactly like an inline one from there on — same actions, same JSONPath subset, same fail-fast on a target that matches nothing. **Several overlays are several entries**, in the order you want them applied: ```yaml transforms: - apply_overlay: { source: ./types.yaml } - rename_schema: { from: InvoiceDTO, to: Invoice } - apply_overlay: { source: ./naming.yaml } # applies after the rename ``` A few things worth knowing: - The document is read **on every regeneration**, not cached between runs — that is what makes it a standing assertion rather than a one-time patch. Editing the file changes your SDK, and drift detection sees it. - A URL or git source is fetched through the same loader as `openapi.source`, with the same protections — private and cloud-metadata addresses are refused, redirects are capped. - An unreachable source, or a document that isn't a valid Overlay Object, **stops the build** (`GLOTTO_IR_TRANSFORM_OVERLAY_SOURCE_UNREADABLE` / `GLOTTO_IR_TRANSFORM_OVERLAY_DOCUMENT_INVALID`). An overlay with an empty `actions` list counts as broken, not as "apply nothing". - verification's report records each referenced overlay's origin and content hash, so the attestation reflects the document you actually built with — your `glotto.yml` only names it. ##### `extends` is checked where it can be, and recorded where it can't Overlay's `extends` field names the document an overlay was written for. Glotto never fetches it and never lets it select what to overlay — that is your `openapi.source`'s job — but it is your own assertion about which API these corrections belong to, so Glotto holds you to it where it can. It must be an absolute URL, so a typo doesn't pass silently. - **Your `openapi.source` is a URL** → `extends` is compared against it, ignoring the spellings that name the same document (a default `:443`, host case, a `#fragment`). A different document stops the build with `GLOTTO_IR_TRANSFORM_OVERLAY_EXTENDS_MISMATCH`, naming both URLs. If the overlay does belong to this API, delete `extends` — it is optional and Glotto doesn't use it. - **Your `openapi.source` is anything else** (a local file, a git ref, a `command`) → there is no URL to compare against, so the assertion is carried but **not** checked. Rather than let a green build imply otherwise, verification's report lists it under `inputs.overlayExtends` as `unverifiable`, with the reason. What was checked is part of the guarantee; what wasn't is too. ##### It fails when the target matches nothing This is the part worth reading twice, and it is a **deliberate divergence from the Overlay standard**, which says a non-matching action should be ignored. Glotto stops the build instead. If your correction silently stopped applying the day your upstream renamed that parameter, you'd ship an SDK with the wrong type and nothing anywhere would be red. An overlay is a standing assertion about your spec, and we hold you to it on every regeneration — the same fail-fast contract as the six transforms above. The divergence is one-directional: an overlay that works here works in any conformant Overlay tool. Only the reverse can fail, and it fails loudly, at generate time. ##### Correcting a scalar Overlay defines `update` for objects and arrays only, and Glotto doesn't invent semantics it leaves undefined. So instead of targeting the scalar: ```yaml # ✗ Rejected — `.type` is a string, and merging into a string has no defined meaning. - target: '$.paths..parameters[?(@.name == "account_id")].schema.type' update: string ``` target the **object that contains it** and merge the key: ```yaml # ✓ Target one segment shorter, and put the key in the update. - target: '$.paths..parameters[?(@.name == "account_id")].schema' update: { type: string } ``` The error you get names the shortened target and the key to merge, so the fix is mechanical. This is the form to use when translating a Stainless `command: update` transform, whose targets conventionally end at the scalar. ##### Supported JSONPath `target` supports [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html): | Construct | Example | |---|---| | Root | `$` | | Child | `$.paths`, `$['paths']`, `$["paths"]` | | Array index (negative counts from the end) | `$.servers[0]`, `$.servers[-1]` | | Array slice (start / end / step, any of them optional) | `$.servers[1:3]`, `$.servers[-2:]`, `$.servers[::-1]` | | Multi-selector (any selectors, applied in the order written) | `$['paths','components']`, `$.servers[0,2]` | | Wildcard | `$.paths.*`, `$.servers[*]` | | Descendant | `$..parameters`, `$..*` | | Filter (both spellings) | `$..parameters[?@.in == 'path']`, `$..parameters[?(@.in == 'path')]` | | Comparisons | `==` `!=` `<` `<=` `>` `>=` | | Booleans, negation, grouping | `&&`, `\|\|`, `!`, `(…)` | | Existence (the query may select many nodes) | `$..schema[?@.deprecated]`, `$..parameters[?@.examples[*]]` | | Document-rooted operand | `$..parameters[?@.name == $.x-default-param]` | | Functions | `length()`, `count()`, `value()`, `match()`, `search()` | | Unicode categories in a pattern | `match(@.name, '\p{L}+')`, `search(@.name, '[\p{Lu}\p{Nd}]')` | | Literals | `'text'`, `"text"`, `42`, `true`, `false`, `null` | `match()` matches the **whole** string and `search()` matches **any substring**, both against an [I-Regexp](https://www.rfc-editor.org/rfc/rfc9485.html) pattern — the regex dialect RFC 9535 is defined against. So the Stainless example above generalises to every `*_id` parameter at once: ```yaml transforms: - apply_overlay: actions: - target: "$.paths..parameters[?match(@.name, '.*_id')].schema" description: Every ID is a string, not a number update: { type: string } ``` Two things are refused rather than guessed at, and both refusals name the rule and the character position: - **A many-node query compared directly** — `[?@.tags[*] == 'x']`. RFC 9535 requires a comparison operand to be a literal or a single-node query, because "one of the tags" and "all of the tags" are different questions and the standard picks neither. Write `value(…)` when you mean the one node the query selects, or an existence test when you mean "any". - **Regex syntax outside I-Regexp** — backreferences, lookahead, lazy quantifiers (`a*?`), the anchors `^` / `$` (`match()` is already whole-string, so use `search()` for a substring), and Unicode **block** escapes such as `\p{IsBasicLatin}`, which I-Regexp excludes in favour of categories. These would all mean something different — or nothing at all — in another Overlay tool. `\p{…}` and `\P{…}` **Unicode category** escapes are supported — the one-letter groups `L`, `M`, `N`, `P`, `Z`, `S`, `C` and the two-letter categories they cover (`Lu`, `Ll`, `Nd`, `Pc`, `Sc`, …), with `\P{…}` as the complement. They resolve against a Unicode table Glotto vendors and pins, not against the runtime's, so the same `target` selects the same nodes on your machine and in the hosted build — which is the whole reason they can be offered at all. `\d`, `\s` and `\w` stay **ASCII** (`[0-9]`, `[ \t\n\r]`, `[0-9A-Za-z_]`). I-Regexp omits these classes precisely because they vary so much between regex flavours, and [RFC 9485 §5](https://www.rfc-editor.org/rfc/rfc9485.html) recommends `[0-9]` for `\d` rather than the broader Unicode reading. When you want the Unicode reading, write it: `\p{Nd}` matches a digit in any writing system, `\p{L}` any letter. Anything else outside the grammar is likewise a **parse error naming the construct and its position**, never a silent non-match. A JSONPath that *almost* parses would select the wrong nodes and corrupt your spec in a way nothing downstream could trace back; refusing costs you an error message instead. ##### Not yet supported A fetched overlay document is **not cached between runs** — an overlay is a standing assertion, so it is re-read and re-checked on every regeneration. The six cleanup transforms take scalar arguments and so have no `source:` form; there is no document to reference. #### `merge_document` ```yaml transforms: - merge_document: { source: ./overrides.yml } ``` A **merge document** is a partial OpenAPI document — structured exactly like the real one — that is deep-merged over your spec. Where `apply_overlay` says *"find this node and change it"*, a merge document says *"here is the shape I want; merge it in"*. It is the shape you already have if you arrived from Fern (`api.specs[].overrides`), or if your team keeps corrections as a partial spec applied with a merge tool. migrating from Fern carries those files across as `merge_document` entries — see [migrating from Fern](/docs/migrate-from-fern). ```yaml # ./overrides.yml — only the parts you're correcting. components: schemas: Account: properties: id: description: The account's opaque identifier. ``` ##### The document, inline or referenced Exactly one of `document:` (written inline) or `source:` (referenced) — never both, never neither. ```yaml transforms: - merge_document: { document: { info: { termsOfService: https://acme.com/terms } } } - merge_document: { source: ./overrides.yml } # relative to your glotto.yml - merge_document: { source: https://specs.acme.com/overrides.yml } - merge_document: source: git: { repo: https://github.com/acme/specs, ref: main, path: overrides.yml } ``` Several documents are **several entries**, in the order you write them — the same rule `apply_overlay` follows, and for the same reason: `transforms` is ordered, and the order is semantic. A referenced document goes through the same loader as your input spec, so it inherits the same URL and git-ref safety checks. ##### Merge rules - **Objects merge recursively** — a key you don't mention is left alone. - **Arrays replace**, they do not append. There is no way to say "the third one" in a merge document; if you need positional addressing, that is what `apply_overlay` is for. - **Scalars replace**, including an explicit `null`. A `null` sets the key to null; it does **not** delete it. (That is [JSON Merge Patch](https://www.rfc-editor.org/rfc/rfc7396) behaviour, which is deliberately *not* what this is — to delete a node, use `apply_overlay`'s `remove: true`.) - **A key your spec doesn't have is added**, creating intermediate objects as needed. This is the thing `apply_overlay` cannot do, and the reason both exist. ##### Anchoring the document with `at:` A correction to a deeply-nested schema costs you the whole nesting on the way down. `at:` names the address once, and the document beneath it is written against that node: ```yaml transforms: # Full shape — five levels of nesting restated to correct one field. - merge_document: document: components: schemas: Account: properties: id: description: The account's opaque identifier. # Anchored — the same correction, addressed once. - merge_document: at: $.components.schemas.Account.properties.id document: description: The account's opaque identifier. ``` The two produce **byte-identical** specs. `at:` is a JSONPath expression in the same [supported subset](https://glotto.dev/docs/transforms/#supported-jsonpath) `apply_overlay`'s `target:` uses, evaluated against the spec as it reaches this entry. One rule comes with it: **an anchor is an address, so it must resolve, and to exactly one node.** - Matching **nothing** stops the build with `GLOTTO_IR_TRANSFORM_TARGET_NOT_FOUND`. An anchor never creates the node it names: `at: $.components.securitySchemes` against a spec that has no `securitySchemes` is an error, not an invitation. Creating a node that deep is what the full-shape form is for, and it still does. - Matching **two or more** nodes stops the build with `GLOTTO_IR_TRANSFORM_TARGET_AMBIGUOUS`. A merge document is a correction to *one* place — it is written against a single node's shape — so a wildcard that fanned out is far more often a typo than an intent. When you do want one rule applied to every match, that is `apply_overlay`, which broadcasts by design. Beneath a resolved anchor nothing else changes: the merge rules above apply unaltered, a key the node doesn't have is still added, and the inert check below still looks at the whole spec. ##### Asserting a document only corrects, with `strict:` Adding a key is what makes a merge document more expressive than an overlay — and it is also how a typo disappears. Write `descriptoin` and the misspelling is added as a brand-new node: your correction never lands, nothing is red, and the mistake ships into every SDK. `strict: true` says *this document only corrects; it never extends*. Every **leaf** of the document — every scalar, array and `null`, plus any empty object — must already exist at the corresponding path in your spec. ```yaml transforms: - merge_document: strict: true document: components: schemas: Account: properties: id: descriptoin: The account's opaque identifier. ``` ``` transforms[0] 'merge_document': `strict: true` asserts every leaf of this document already exists, but the spec does not have 1 of the paths its leaves name: components.schemas.Account.properties.id.descriptoin. Fix each path — a misspelled key is the usual cause — or drop `strict:` if this entry is meant to extend the spec rather than only correct it. ``` A leaf whose path does not resolve stops the build with `GLOTTO_IR_TRANSFORM_MERGE_DOCUMENT_STRICT_VIOLATION`. Three things to know about it: - **Every offending path is listed, not just the first** — a hand-written override usually has more than one typo, and one per run is the slowest possible loop. - **The check runs before the merge**, so a violation leaves your spec exactly as it was. Nothing is half-applied. - **It composes with `at:`.** Leaf paths are resolved relative to the anchor, and reported that way. `strict:` defaults to `false`, so every existing entry keeps adding as it always has. It is also independent of the inert check below: the two ask different questions and can disagree about the same document. A document that replaces one leaf with the value already there is strict-clean and inert-failing; a document that adds one key and replaces another is inert-clean and strict-failing. ##### It fails when the document changes nothing If merging your document produces a spec identical to the one it was applied to, generation **stops** with `GLOTTO_IR_TRANSFORM_MERGE_DOCUMENT_INERT`. That means your upstream has caught up: it now asserts everything your override was correcting, so the override is dead. Left alone, a dead override is indistinguishable from a live one — it sits in your config forever, and nobody finds out if the upstream later reverts. Delete the entry (or point it at the document you meant). This is checked **per entry, not per key**: a document whose ten keys include one that still changes something is live, so keeping a deliberately complete override is fine. #### Which correction transform? | You have… | Use | |---|---| | An [OpenAPI Overlay](https://spec.openapis.org/overlay/v1.0.0.html) document (`actions[]`) | `apply_overlay` | | A partial OpenAPI document to merge in | `merge_document` | | A correction that must **add** a node your spec lacks | `merge_document` | | A correction that must **delete** a node | `apply_overlay` (`remove: true`) | | A correction addressing "every parameter named X", or one array element | `apply_overlay` | | A deep correction you'd rather not restate the nesting for, at **one** node | `merge_document` with `at:` | | One rule to apply at **every** node an expression matches | `apply_overlay` — a multi-match `at:` is an error, not a broadcast | | A correction that must only **fix** existing values, never add new ones | `merge_document` with `strict: true` | | A Fern project's `overlays:` / `overrides:` | one each — migrating from Fern picks correctly | Both are ordered entries in the same `transforms` list, so you can mix them freely. #### Where transforms sit `transforms` is an optional top-level key in `glotto.yml`; see the [`glotto.yml` reference](/docs/glotto-yml) for the full configuration surface, and the [pipeline](/docs/pipeline) concept for where the transform stage runs. ### TypeScript Source: https://glotto.dev/docs/typescript/ Glotto's reference target: an ESM-first, tree-shakable TypeScript client with typed error classes, async-iterator pagination, and SSE streaming. The TypeScript SDK is Glotto's reference target — a typed, tree-shakable client emitted from the same `GlottoIR` as every other language. It ships as an ESM-first npm package with a self-building manifest, so `npm install` then `import { Client }` is all it takes. #### Quickstart ```bash npm install @your-org/petstore ``` ```ts import { Client } from '@your-org/petstore'; const client = new Client({ token: process.env.PETSTORE_TOKEN }); // a single call const pet = await client.pets.createPet({ name: 'Rex' }); // pagination is an async iterator — `for await` walks every page for await (const pet of client.pets.listPets()) { console.log(pet.name); } ``` #### Typed errors Non-2xx responses reject with typed error classes, not bare `Error`s — the status family maps to a class (`BadRequestError`, `RateLimitError`, …) and the parsed response body is typed from the spec's error schema, so you can `catch` and narrow. See [Errors](/docs/errors). #### Cursor pagination List methods return an **async iterator** — `for await` walks every page, with the generated method advancing the cursor for you. Glotto emits the right walker for the API's strategy (cursor, page, offset, or link-header). See [Pagination](/docs/pagination). #### Retries & backoff Transient failures (`5xx`, `429`, transport errors) retry with exponential backoff and jitter; non-idempotent writes only retry when idempotency is enabled. Tune `maxAttempts`, `initialDelayMs`, `maxDelayMs`, and `jitter` per client. See [Retries & timeouts](/docs/retries). #### SSE streaming Server-sent-event endpoints return an async iterator of typed events, decoded from the `text/event-stream` framing — `for await (const event of client.…())`. A frame whose payload isn't valid JSON surfaces a parse error to your loop (never a raw string mistyped as your model); declare terminal sentinels like `data: [DONE]` via `streaming.on_event`. See [Streaming](/docs/streaming). #### Unknown response fields Your API can add a response field without it being a breaking change — but a generated interface gives you no typed way to reach it. The generated models keep it instead: a field the SDK wasn't generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized. ```ts const pet = await client.pets.createPet(body); // A field your API started returning after this SDK was generated. const extra = petExtraFields(pet); if ('species' in extra) { console.log(extra.species); } ``` Each model gets its own reader — `petExtraFields(pet)`, `tagExtraFields(tag)` — typed to that model, so passing the wrong one is a compile error rather than an empty object at runtime. The runtime keeps the data regardless: the decode is a cast, so the extra keys are already on the object and `JSON.stringify` writes them back. The readers are what make them *reachable* — without one you would need an unchecked cast, and nothing would stop a future change from dropping them. An index signature was deliberately not used: these same interfaces type your request bodies, and `[key: string]: unknown` would switch off excess-property checking across the whole SDK. The retained fields are read-only by design. To *send* a field your spec doesn't model yet, use the per-call extra-body escape hatch rather than writing to the retained bag. #### File uploads `multipart/form-data` operations accept platform file types (`Blob`/`File`/streams) and the client builds the multipart body, so binary uploads work without hand-assembling form parts. #### Pages, request controls, and files Paginated methods also expose [manual pages](/docs/pagination#fetch-one-page), so you can inspect one response and request its successor explicitly. Request overrides apply to the initial call and to each requested next page; see [Retries & timeouts](/docs/retries) for precedence and cancellation. [Binary downloads](/docs/file-transfers) return an owned byte response with metadata, a bounded read helper, and incremental consumption. Close the response when you stop early. Your generated README demonstrates these operations with your API's names and the language's native calling conventions. ### Connect a VCS provider Source: https://glotto.dev/docs/vcs-connect/ Connect GitHub, GitLab, or Bitbucket once, and Glotto tags your downstream repo to publish Go, Swift, and PHP SDKs and open release PRs for you. To publish your `go` / `swift` / `php` SDKs — and to open regenerate-and-release PRs — Glotto creates a `v` tag (and, later, a PR) directly in your downstream SDK repository through the provider's API. That needs a **connection**: a credential, scoped to your organization, that authorizes Glotto to act on the repos you choose. You connect **once per provider**, and the single connection covers every SDK repo under that account — there is no per-repo setup. > Glotto never stores a long-lived personal access token. Each provider uses its **machine-to-machine** > mode — a GitHub App installation, a GitLab OAuth grant, or a Bitbucket workspace OAuth consumer — and > the at-rest secret (where there is one) is encrypted with AES-256-GCM. A connection is **owner-only**: > only an organization owner can connect or disconnect a provider. #### What Glotto needs, per provider | Provider | How you connect | What Glotto asks for | |---|---|---| | **GitHub** | Install Glotto's [**GitHub App**](https://github.com/apps/glotto) on your org/account and pick the SDK repos | `contents: write`, `pull_requests: write`, `metadata: read` | | **GitLab** | Authorize Glotto's **OAuth application** (one consent screen) | the `api` scope | | **Bitbucket** | Create a workspace **OAuth consumer** and paste its key + secret | `repository: write` | #### GitHub — install the app 1. Start the connection from your organization. Glotto returns a GitHub **App install URL**. 2. GitHub shows the install screen: choose the account and the repositories Glotto may access (the SDK repos you publish to), then confirm. 3. GitHub redirects back to Glotto with the new **installation id**, which Glotto stores against your organization. That's the whole credential — a GitHub App install is not an OAuth token exchange, so there is nothing to paste. If you started from the studio Connections page, your browser is returned there. Glotto's App is published at [`github.com/apps/glotto`](https://github.com/apps/glotto), where you can review exactly what it asks for before you connect. **Begin the install from Glotto, not from that page** — an install started on GitHub arrives with nothing tying it to your organization, so Glotto has no way to record it and no connection appears. If that happens, uninstall it on GitHub and start again from Connections; nothing is lost. From then on Glotto mints short-lived installation tokens on demand; you can change which repos are shared, or uninstall the app, from your GitHub settings at any time. #### GitLab — authorize the OAuth app 1. Start the connection. Glotto returns a GitLab **`/oauth/authorize`** URL (PKCE-protected). 2. GitLab shows one consent screen for the `api` scope; approve it. 3. GitLab redirects back with an authorization `code`, which Glotto exchanges **server-side** for a refresh token. Only the refresh token is kept (encrypted); Glotto re-mints access tokens as needed and rotates the refresh token automatically. If you started from the studio Connections page, your browser is returned there. A denied or cancelled consent connects nothing — you can retry whenever you're ready. #### Bitbucket — submit a workspace consumer Bitbucket's two-legged (`client_credentials`) OAuth has no consent redirect, so you supply the credentials directly: 1. In your Bitbucket **workspace settings → OAuth consumers**, create a consumer with the **`repository: write`** permission. 2. Copy its **key** and **secret** and submit them to Glotto. The secret is encrypted at rest and is never shown again or returned by any API. #### Your own forge identity (optional) The connection above belongs to your **organization**, and Glotto reads with it. You can additionally link **your own** account on a provider, and Glotto will then *write* with yours — so your forge, not Glotto, decides whether a change you make is allowed, and records it as yours. That is what makes a Glotto-opened change count for the controls you already run: - **Branch protection** and **CODEOWNERS** see the person who made the change, not a bot. - The push is authorized by *your* repository permissions, not by an organization-wide grant. Linking is **per person and per organization**, and it needs no admin role — you link your own account, nobody can link it for you, and nobody else can use it. Unlinking takes effect immediately. Glotto stores only the encrypted grant plus the account name the provider reports; it never sees your password and never holds a personal access token. **If you have not linked**, nothing breaks and nothing is hidden from you. The change is pushed with the organization's credential and Glotto records **you** as the commit author, so the history is accurate — but the provider sees the organization, so branch protection and CODEOWNERS will not credit you. Glotto states which of the two carried your change on the same screen, every time. > A per-user grant does **not** make the commit *signed*. It is an ordinary unsigned commit, so it does > not satisfy a signed-commit requirement; what it changes is who the provider sees making the write. Availability differs by provider, and Glotto tells you which are offered on your deployment rather than letting you find out on click: | Provider | Personal identity | |---|---| | **GitLab** | Supported — one consent screen, the same OAuth application as the organization connection | | **GitHub** | Supported — one consent screen on the Glotto GitHub App; Glotto keeps the grant fresh, so you authorize once | | **Bitbucket** | Not applicable — its connection model has no per-person consent step | | **Azure Repos** | Not applicable — its connection is an organization access token | #### Managing connections You can list your organization's connections at any time — each shows the provider, whether it's enabled, and when it was connected, and **never** exposes a secret. Disconnecting a provider removes its stored credential; re-connecting overwrites it, which is also how you rotate the Bitbucket consumer secret. Once a provider is connected, publishing and the [multi-VCS release flow](/docs/multi-vcs-release) use it automatically — a release tags `v` on the target repo (idempotently), and a missing or disabled connection fails the publish with a clear, secret-free message telling you to connect first. #### Connecting is what unlocks a production repository A project builds from the moment it exists: every build lands on the Glotto-hosted **staging repository** for that language, which needs no connection of yours at all — Glotto hosts it. Selecting your **own** repository as a target's production repository is the upgrade a connected organization earns. Glotto refuses the selection until an enabled connection exists for that repository's provider, server-side and not only in the picker, so the answer arrives when you choose the repository rather than at the first failed build. Connect the provider, then select the repository; from then on each build also opens-or-refreshes the release PR there. See [the release flow](/docs/multi-vcs-release) for what the two repositories hold and what a build reports. ### The verification report Source: https://glotto.dev/docs/verification-report/ Every regeneration produces a deterministic report: pinned inputs, per-target drift, compile and contract statuses, delivered to CI and the release PR. Generating an SDK once is easy; *proving it's still correct on every regeneration, forever* is the hard part — and that proof is the **verification report**. Glotto regenerates your SDKs, docs site and MCP server in memory, compares against the committed output, and emits a deterministic report that records, for the exact inputs used: - **Pinned inputs** — the spec hash (canonical and raw), the `glotto.yml` config hash, and the tool version. Two runs over identical inputs produce byte-identical reports. - **Per-target results** — for each generated target: the **drift** status (does the committed output still match what the generator produces?), the **managed-file integrity** status (has a checksum-stamped generated file been hand-edited? — your [custom code](/docs/custom-code) in `lib/` is yours and is never flagged), and the **compile** and **contract** statuses (`not-run` / `ok` / `failed` — see [check results](https://glotto.dev/docs/verification-report/#real-compile--contract-statuses-the-checks-loop) below for how these become real). - **Two renderings** — a JSON document (`reportVersion: 1`, with a machine-readable `summary`) and a Markdown rendering for humans. Both land in your CI artifacts, and the Markdown is what appears on the pull request. Drift and integrity are deliberately distinct signals: drift is checksum-aware, so a hand-edited managed file with an intact embedded checksum does **not** drift — only the integrity check catches hand-edits. The report carries both, and verification exits non-zero when either fails. See [verification report](/docs/verification-report) in the CLI reference for the full flag set, including checkout-free verification of a remote branch (`--provider`). #### What a report looks like Nothing below is typed by hand. Every pane is what `renderVerificationReportMarkdown` emitted over the committed fixture this repo dogfoods, produced the same way your CI produces yours. **a clean regeneration of the committed SDK** A clean regeneration. Six targets, no drift, every checksum-stamped file intact. **regenerated + byte-diffed in CI** `renderVerificationReportMarkdown (@glotto/cli)` `packages/cli/tests/fixtures/drift-gate` `43c4d424bd49` targets 6 drifted 0 hand-edited managed files 0 ```markdown # Glotto verification report **Result: ✓ verified** ## Inputs | Input | Value | |---|---| | Spec | `./petstore.openapi.yaml` (openapi) | | Spec sha256 (canonical) | `a7da38d032e4642349d2e4531a12400d2634905b9d78dadc681b571c1d064760` | | Spec sha256 (raw) | `88fd4f083e7abc781f553ba21280d697d86116c40b9ced90f18a4f06f7e472f8` | | Config | `glotto.yml` | | Config sha256 | `a243677cd26512cf42a63076089373818acb76aeba92de35441555cd5945cc77` | | Tool | glotto 0.0.0 | ## Targets | Target | Kind | Files | Drift | Custom code | Compile | Contract | |---|---|--:|---|---|---|---| | cli | sdk | 9 | ✓ in-sync | none | not-run | not-run | | graph | sdk | 4 | ✓ in-sync | none | not-run | not-run | | mcp | mcp | 38 | ✓ in-sync | ✓ intact (31 managed) | not-run | not-run | | root | root | 1 | ✓ in-sync | none | not-run | not-run | | spec_repo | sdk | 10 | ✓ in-sync | none | not-run | not-run | | typescript | sdk | 18 | ✓ in-sync | ✓ intact (11 managed) | not-run | not-run | ``` `compile` and `contract` read `not-run` above because building a report never runs a toolchain — the report says what it verified and what it did not, rather than implying more. Real statuses enter as a declared input, and then the same report carries them: **the same regeneration, with the TypeScript target's compile status folded in from a real `` `tsc --noEmit` `` run** The same regeneration, with the TypeScript target's compile status folded in from a real tsc run. One status attested; the rest still say not-run, because they were not. **regenerated + byte-diffed in CI** `renderVerificationReportMarkdown (@glotto/cli)` `packages/cli/tests/fixtures/drift-gate` `ea9ae2632587` targets 6 compile statuses attested 1 failed checks 0 ```markdown # Glotto verification report **Result: ✓ verified** ## Inputs | Input | Value | |---|---| | Spec | `./petstore.openapi.yaml` (openapi) | | Spec sha256 (canonical) | `a7da38d032e4642349d2e4531a12400d2634905b9d78dadc681b571c1d064760` | | Spec sha256 (raw) | `88fd4f083e7abc781f553ba21280d697d86116c40b9ced90f18a4f06f7e472f8` | | Config | `glotto.yml` | | Config sha256 | `a243677cd26512cf42a63076089373818acb76aeba92de35441555cd5945cc77` | | Tool | glotto 0.0.0 | ## Targets | Target | Kind | Files | Drift | Custom code | Compile | Contract | |---|---|--:|---|---|---|---| | cli | sdk | 9 | ✓ in-sync | none | not-run | not-run | | graph | sdk | 4 | ✓ in-sync | none | not-run | not-run | | mcp | mcp | 38 | ✓ in-sync | ✓ intact (31 managed) | not-run | not-run | | root | root | 1 | ✓ in-sync | none | not-run | not-run | | spec_repo | sdk | 10 | ✓ in-sync | none | not-run | not-run | | typescript | sdk | 18 | ✓ in-sync | ✓ intact (11 managed) | ok | not-run | ``` And when a managed file has been hand-edited in place, the report is where you find out — drift alone will not tell you, because drift is checksum-aware: **a managed file hand-edited in place — `` `typescript/src/client.ts` `` no longer hashes to its embedded checksum** One managed file edited without restamping. Nothing drifted; the integrity check caught it and the report fails. **regenerated + byte-diffed in CI** `renderVerificationReportMarkdown (@glotto/cli)` `packages/cli/tests/fixtures/drift-gate` `3a6c95e8e1a5` targets 6 drifted 0 hand-edited managed files 1 ```markdown # Glotto verification report **Result: ✗ verification failed** ## Inputs | Input | Value | |---|---| | Spec | `./petstore.openapi.yaml` (openapi) | | Spec sha256 (canonical) | `a7da38d032e4642349d2e4531a12400d2634905b9d78dadc681b571c1d064760` | | Spec sha256 (raw) | `88fd4f083e7abc781f553ba21280d697d86116c40b9ced90f18a4f06f7e472f8` | | Config | `glotto.yml` | | Config sha256 | `a243677cd26512cf42a63076089373818acb76aeba92de35441555cd5945cc77` | | Tool | glotto 0.0.0 | ## Targets | Target | Kind | Files | Drift | Custom code | Compile | Contract | |---|---|--:|---|---|---|---| | cli | sdk | 9 | ✓ in-sync | none | not-run | not-run | | graph | sdk | 4 | ✓ in-sync | none | not-run | not-run | | mcp | mcp | 38 | ✓ in-sync | ✓ intact (31 managed) | not-run | not-run | | root | root | 1 | ✓ in-sync | none | not-run | not-run | | spec_repo | sdk | 10 | ✓ in-sync | none | not-run | not-run | | typescript | sdk | 18 | ✓ in-sync | ✗ modified (1 of 11 managed) | not-run | not-run | ### Modified managed files: `typescript` - `typescript/src/client.ts` — hand-edited (embedded @glotto:generated-checksum no longer matches) ``` #### Where the report lands The same report is delivered on four surfaces, so the proof is visible wherever you review a regeneration. ##### In CI, on every PR The CI workflows Glotto emits (the CI workflow Glotto emits) gate every PR/MR with verification and publish the report to the job summary and the build artifacts — on GitHub, GitLab, Bitbucket, and Azure alike. The check fails on drift **or** a hand-edited managed file, and the report is attached even when the job fails. See [Drift detection](/docs/drift-detection) for the gate itself. ##### On the release PR, as a comment Glotto's hosted verification can post the report directly on your open release PR — the place you actually review a regeneration. The semantics are deliberately conservative: - **Opt-in, default off.** Commenting writes to *your* repository, so it only happens when you enable the verification source's `pr_comment` flag — it is never on retroactively. - **One comment, refreshed in place.** The comment is marker-tagged per project: every succeeded verification run — and every [checks ingest](https://glotto.dev/docs/verification-report/#real-compile--contract-statuses-the-checks-loop) into the latest run — updates the *same* comment rather than stacking a new one per run. - **Best-effort.** A comment failure (no open PR on the verified branch, a provider error) never changes the verification run, the ingest result, or the API response — the report is the record; the comment is a convenience. ##### In the console The project's **surface health** panel shows the latest verification runs — verified or not, which targets drifted, per-target compile/contract statuses — rendered honestly from the stored report (a status that never ran shows as `not-run`, never as a green check). ##### As a signed attestation Any stored successful run can be downloaded as a **signed attestation**: a single self-contained JSON file — a DSSE v1 envelope, Ed25519-signed — that binds the exact report document to the run that produced it. Verify it offline against the public key served by the unauthenticated `GET /v1/verification/attestation-key` endpoint; the envelope is standard DSSE, so a stock crypto library is all an auditor needs. This is the artifact an enterprise buyer can file: cryptographic evidence that a given SDK surface was verified against a given spec. Verify it with one command — `glotto verify-attestation attestation.json` — which resolves the signing key by the fingerprint the envelope itself records (so an artifact stays verifiable across a key rotation), works [completely offline](/docs/cli#glotto-verify-attestation) from an archived key file, and reports a revoked key as a distinct fact from a bad signature. ##### Counter-signed by you, not just by us A single-party attestation is only as trustworthy as the party issuing it. You can **co-sign** an attestation with your own Ed25519 key, so the artifact carries a signature that does not depend on trusting Glotto: ```sh glotto verify-attestation attestation.json --countersign my-key.pem --upload ``` Three things this deliberately does: - **Verification runs first.** An envelope that failed verification is never counter-signed — the command has no mode that vouches for something it did not check. - **The signature is verified before it is stored.** Your co-signature is checked against the run's own payload server-side; an unverifiable one is rejected rather than filed, because an envelope that *looks* multi-party while proving nothing is worse than one that carries no counter-signature. - **A co-signature binds one exact payload.** If a later CI check-results upload changes the report, every counter-signature over the old bytes is dropped from the envelope and listed as `stale` rather than quietly served — re-run the command to sign the new payload. A counter-signature proves *who else vouched for these exact bytes*. It carries **no timestamp** and is not a trusted clock: Glotto deliberately ships no RFC 3161 or transparency-log integration, so nothing in the artifact claims to establish *when* it was signed. #### Real compile & contract statuses (the checks loop) verification never runs a compiler — your SDKs actually compile, and the contract suites actually run, in **your CI**, where the toolchains and the generated output live. So the compile/contract statuses enter the report as a declared input: your CI produces a checksVersion-1 **check-results document** recording per-target `ok`/`failed` outcomes, then either - folds it into the report before rendering, where a `failed` status makes verification fail; or - posts it to the hosted run — `POST /v1/projects/:projectId/verification-runs/:runId/checks` folds it into the stored report, and the updated statuses ripple to the console panel, the attestation, and the release-PR comment. The document can pin the spec hash it was produced against, so results from different spec bytes are rejected rather than silently folded in. #### Verified provenance in CI When your CI posts check-results to the hosted run, the control plane can verify **which CI run produced them** — not merely that a valid API token uploaded them. Your runner presents a provider-signed CI-run OIDC token alongside the upload; the server checks it against the provider's public keys and marks the run **verified**, and that verified origin is bound into the [signed attestation](https://glotto.dev/docs/verification-report/#as-a-signed-attestation) an auditor downloads. - **Turn-key on GitHub and GitLab.** The workflows Glotto emits wire this for you: the GitHub template grants the job `id-token: write` so the runner can mint an OIDC token, and the GitLab template pre-mints one via an `id_tokens:` block. With hosted mode enabled (a project id + API token), verified provenance is on by default — there is nothing else to configure. - **Manual on other CI.** On Bitbucket, Azure Pipelines, a self-hosted runner, or any custom CI, inject a pre-minted CI-run OIDC token into the environment variable **`GLOTTO_CI_OIDC_TOKEN`**; hosted mode presents it automatically. (GitHub's request-and-exchange path reads `ACTIONS_ID_TOKEN_REQUEST_*` instead, so it needs no explicit token variable.) - **The audience contract.** The token's `aud` claim must equal your Glotto API URL, which must in turn match the server's `CI_OIDC_AUDIENCE`. For the default hosted service that is `https://api.glotto.dev` — already the value the emitted templates use, so there is nothing to set. If you **self-host** the control plane, set the token audience (the GitLab `id_tokens:` `aud`, or `GLOTTO_API_URL` for GitHub's exchange) to your own API URL and configure the server's `CI_OIDC_AUDIENCE` to match. Provenance verification is optional and non-breaking: with no token the upload still succeeds — the run is simply recorded **unverified** rather than verified. #### Why this works The report is only as trustworthy as the regeneration behind it, and that's the point of the [deterministic pipeline](/docs/pipeline): a byte-stable canonical spec means regeneration is reproducible, so a drift result is a *real* change and a byte-identical report is a real guarantee — not noise. ### Webhooks Source: https://glotto.dev/docs/webhooks/ Verifying inbound webhook signatures with the helpers your SDKs emit when the spec declares webhooks. When your OpenAPI document declares a top-level `webhooks` block, every generated SDK ships standalone, tree-shakable **webhook verification helpers** — no client instance required. A spec that declares no webhooks gets none of this surface, so an API with no event delivery does not carry verifier code it can never use. They take the raw request body, the signature header, and your signing secret, verify the signature in constant time, and return the parsed payload (or throw on a bad or missing signature). Use them in your webhook handler before trusting a delivery. **TypeScript** (regenerated + byte-diffed in CI `a0f8af4b462c`) ```ts import { verifyHmacWebhook } from 'glotto-sdk'; const event = await verifyHmacWebhook('', '', ''); console.log(event); ``` **React Native** (regenerated + byte-diffed in CI `a1fefe7d3458`) ```ts import { verifyHmacWebhook } from 'glotto-sdk'; const event = await verifyHmacWebhook('', '', ''); console.log(event); ``` **Python** (regenerated + byte-diffed in CI `2242160f8958`) ```python from glotto_sdk import verify_webhook if not verify_webhook("", "", ""): raise ValueError("invalid webhook signature") ``` **Go** (regenerated + byte-diffed in CI `f0c565cc46f8`) ```go package main import ( sdk "example.com/glotto-sdk-go" ) func main() { if !sdk.VerifyWebhook("", "", "") { panic("invalid webhook signature") } } ``` **Java** (regenerated + byte-diffed in CI `12c05e098737`) ```java import com.glotto.Client; public class Snippet { public static void main(String[] args) { if (!Client.verifyWebhook("", "", "")) { throw new IllegalArgumentException("invalid webhook signature"); } } } ``` **Kotlin** (regenerated + byte-diffed in CI `0f5e53ae2522`) ```kotlin import com.glotto.Client fun main() { require(Client.verifyWebhook("", "", "")) { "invalid webhook signature" } } ``` **C#** (regenerated + byte-diffed in CI `c5013ce0e88b`) ```csharp using Glotto; if (!Client.VerifyWebhook("", "", "")) { throw new Exception("invalid webhook signature"); } ``` **PHP** (regenerated + byte-diffed in CI `1acd16081b46`) ```php ", "", "")) { throw new RuntimeException("invalid webhook signature"); } ``` **Ruby** (regenerated + byte-diffed in CI `a2b854f6593f`) ```ruby require 'glotto' unless Glotto::Client.verify_webhook("", "", "") raise "invalid webhook signature" end ``` **Rust** (regenerated + byte-diffed in CI `716e9ec181d1`) ```rust if verify_hmac_webhook(b"", "", "", WebhookEncoding::Hex).is_err() { panic!("invalid webhook signature"); } ``` **Swift** (regenerated + byte-diffed in CI `0cc357352baf`) ```swift import Foundation import GlottoSdk do { _ = try verifyHmacWebhook( payload: Data("".utf8), signature: "", secret: "" ) } catch { print("Invalid webhook signature: \(error)") } ``` **Dart** (regenerated + byte-diffed in CI `67567d6df435`) ```dart import 'package:glotto_sdk/glotto_sdk.dart'; try { verifyHmacWebhook(''.codeUnits, '', ''); } on WebhookException catch (error) { print(error); } ``` **Elixir** (regenerated + byte-diffed in CI `b97af402f1d7`) ```elixir case Glotto.Webhook.verify_hmac("", "", "") do {:ok, payload} -> IO.inspect(payload) {:error, :invalid_signature} -> raise "invalid webhook signature" end ``` #### HMAC signatures `verifyHmacWebhook` (TypeScript) / `verify_webhook` (Python) / `VerifyWebhook` (Go) compute an HMAC-SHA256 of the raw payload with your secret and compare it to the provided signature using a timing-safe equality check. The signature encoding (`hex` or `base64`) is selectable. A mismatch raises `WebhookVerificationError`; a success returns the parsed body. #### Standard Webhooks For providers that follow the [Standard Webhooks](https://www.standardwebhooks.com/) spec, the SDK also emits `verifyStandardWebhook` — it reads the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers, enforces a configurable timestamp tolerance (replay protection), and verifies the base64 signature. Stripe-style signatures are handled by a sibling helper where the spec advertises them. #### Why a standalone helper Verification is **crypto over raw bytes**, so it can't go through the typed client — it runs in your HTTP handler before any parsing. The helpers depend only on Web Crypto (no Node built-ins), so the same function works in a server, an edge runtime, or a serverless handler. ## Blog ### Engineering blog Source: https://glotto.dev/blog/ How the correctness engine is built, and what we learned building it. ## Changelog ### Changelog Source: https://glotto.dev/changelog/ Customer-facing changes to the generated SDKs, docs, and MCP server. ## Legal ### Privacy Policy Source: https://glotto.dev/legal/privacy/ How Glotto, Inc. collects, uses, and protects personal data on glotto.dev and in the hosted product, and how to make a data-subject request. **Glotto, Inc.** ("Glotto", "we") operates the website at `glotto.dev` and the hosted Glotto service. This policy explains what personal data we collect, why, how long we keep it, and who else processes it. This policy covers the marketing site and the hosted product. It does not cover the SDKs, docs sites, or MCP servers Glotto generates *for* you and that you host yourself — those run in your infrastructure, under your own policy. #### What we collect ##### Website visitors We use first-party, **cookieless** analytics. No cookies are set, nothing is written to browser storage, and no cross-site or cross-session profile is built — so there is no consent banner, because there is nothing to consent to. | Data | Why | Retention | | --- | --- | --- | | Page path (no query string, no fragment) | Understand which pages are read | Retained in aggregate by our analytics sub-processor | | Coarse event counts (page view, "get started" click) | Understand which parts of the site work | As above | | Client error type | Fix broken pages | As above | | IP address | Unavoidably visible to any web server; used to serve the request and for abuse prevention. It is **not** stored by us as an identifier | Transient | Each event carries an ephemeral identifier generated per page load and held only in memory. We do not stitch it to an identity, and we honour **Do Not Track** and **Global Privacy Control** — with either set, no analytics event is sent at all. ##### Account holders To create and operate an account we process: | Data | Why | Legal basis (GDPR) | | --- | --- | --- | | Email address | Authentication — we sign you in with a one-time magic link, so the address is load-bearing, not optional | Performance of a contract | | Organisation name and membership | Scope your projects and permissions | Performance of a contract | | Single sign-on identifiers, where your organisation uses SSO | Federated authentication | Performance of a contract | | Billing contact and payment status | Take payment and issue receipts | Performance of a contract; legal obligation for tax records | | Audit log entries — who did what, and when | Security, and the audit trail our customers' own reviews require | Legitimate interests | We do not sell personal data, and we do not use it to train machine-learning models. ##### Customer content Your API specifications, generated SDKs, documentation, and configuration are **your** content. We process them only to deliver the service you asked for — generating, verifying, and publishing your developer surface — and under your instructions. They are classified Confidential, encrypted in transit and at rest, and accessed by us only where least-privilege operation of the service requires it. If your specification or configuration contains personal data, we process it as a processor on your behalf. See the [Sub-processors](/legal/subprocessors) page for who else is involved. #### Where your data is processed The hosted service runs in Microsoft Azure's **East US 2** region (Virginia, United States) across every environment. The database has no public endpoint — it is reachable only from within our private network. Customers with data-residency or isolation requirements that this does not meet can be served by a dedicated single-tenant deployment; contact us. #### Who else processes it Every third party that processes customer or personal data on our behalf is listed, with what it receives and where it processes it, on the [Sub-processors](/legal/subprocessors) page. That page is kept in step with our infrastructure by an automated check, so it cannot quietly fall behind the systems it describes. #### How long we keep it - **Account, organisation, and project data** — for the life of the account. On account closure it is deleted, together with its single sign-on, billing, and usage records. - **Billing records** — retained as long as tax and accounting law requires, after account closure. - **Audit logs** — retained as an append-only trail, because their value is that they cannot be edited after the fact. - **Secrets and credentials** — kept only while in active use, and destroyed on revocation. - **Backups** — roll off on our standard backup retention window. #### How we protect it Data is encrypted in transit and at rest. Secrets live in a managed secrets store, never in source control, and an automated scan rejects any change that would commit one. Access is least-privilege and multi-factor. Outbound network access from our own services is denied by default and allowed only on the protocols the service actually needs, and URLs you supply to us are validated before we fetch them. #### Your rights Depending on where you live, you may have the right to access, correct, delete, export, or restrict processing of your personal data, to object to processing, and to withdraw consent. Under the GDPR you may also lodge a complaint with your supervisory authority. Under the CCPA/CPRA, we do not sell or share personal data as those terms are defined, and we will not discriminate against you for exercising a right. You can delete your account — and the personal data attached to it — from the product at any time. For anything else, or to make a request on behalf of someone else, email **privacy@glotto.dev**. We will verify the request and respond within the period the applicable law allows. To report a security vulnerability, email **security@glotto.dev** instead. #### Children The service is for software developers and organisations. It is not directed at children, and we do not knowingly collect personal data from anyone under 16. #### Changes We will update this page when our processing changes, and revise the "last updated" date above. Where a change materially affects you as a customer, we will also notify you directly. #### Contact Glotto, Inc. — **privacy@glotto.dev** ### Sub-processors Source: https://glotto.dev/legal/subprocessors/ Every third party that processes Glotto customer or personal data, what it receives, and the region it processes in — plus where your data lives. A **sub-processor** is a third party that processes customer or personal data on our behalf in order to deliver the Glotto service. This page lists all of them. It is not a marketing page: it is the artefact a security review asks for, and it is checked against our own infrastructure on every change, so it cannot quietly fall behind the systems it describes. A third party our services actually send data to, but that is missing from the table below, fails our build. #### Data residency The hosted service runs entirely in **Microsoft Azure, East US 2** (Virginia, United States). Every environment — development, staging, and production — uses that one region, because the database is integrated into a private network and must sit alongside it. The database has **no public endpoint**. Outbound network access from our own services is denied by default and permitted only on the protocols the service needs. If you need your data processed in another region, or isolated from other customers entirely, we can run a dedicated single-tenant deployment for you. Contact us. #### Current sub-processors | Sub-processor | Purpose | Data it receives | Processing region | | --- | --- | --- | --- | | Microsoft Azure | Compute, database, secrets, and object storage — the hosted service itself | Account and organisation data, customer specifications and generated artefacts | East US 2 (United States) | | Azure Monitor / Application Insights | Operational logging and monitoring | Service logs and metrics; no secrets | East US 2 (United States) | | Cloudflare | Edge network: DNS, WAF, and the tunnel our origin is reachable through | Request metadata and TLS termination; deploy and custom-domain provisioning for customer docs sites | Global edge network | | Postmark | Transactional email — the magic links you sign in with | Your email address and the message we send you | United States | | Stripe | Payment processing and billing | Billing contact and payment details, which Stripe collects directly | United States | | Anthropic | The in-product Ask-AI assistant | The question you ask and the documentation context needed to answer it | United States | | PostHog | Cookieless website analytics and client error reporting | Page paths, coarse event counts, and error types — no cookies, no profile, no personal identifiers | United States | | Google Workspace | Business email and correspondence | Email addresses and the content of correspondence you send us | United States | Payment card details are collected by Stripe directly and never reach our servers. #### Not sub-processors These third parties are involved in delivering the service but do not process customer data on our behalf, and are listed for completeness: - **Version-control hosts** — GitHub, GitLab, Bitbucket, or your self-hosted instance. We push your generated SDKs to the repository *you* nominate, using credentials *you* supply. That destination is your choice and your controller relationship, not ours. - **Package registries** — npm, PyPI, RubyGems, NuGet, pub.dev, and crates.io. When you run a publish, we upload the artefacts you asked us to publish, under your own registry credentials. - **Your identity provider** — where your organisation uses single sign-on, we talk to the provider you configure. It is your vendor, not ours. - **Internal tooling** that never touches customer data — our credential manager, for instance. #### Changes to this list We review our sub-processors at least annually. Where we add one, we update this page and notify affected customers **at least 30 days before** the new sub-processor begins processing customer data, per the notice terms in our data processing agreement. The "last updated" date at the top of this page is the date the list itself last changed. #### Questions Email **privacy@glotto.dev**. For a security questionnaire or a vulnerability report, use **security@glotto.dev**. ### Terms of Service Source: https://glotto.dev/legal/terms/ The terms governing use of Glotto's hosted service — accounts, your content, acceptable use, fees, warranties, and termination. These terms are a contract between you (or the organisation you act for) and **Glotto, Inc.**, a Delaware corporation. They govern the hosted Glotto service and the `glotto.dev` website. By creating an account or using the service, you agree to them. If your organisation has signed a separate written agreement with us — a design-partner agreement, an enterprise order form, or a data processing agreement — that agreement governs where it conflicts with these terms. #### 1. The service Glotto generates SDKs, documentation sites, and MCP servers from your API specification, and keeps them verifiably in step with it as that specification changes. We may improve, change, or discontinue features; where a change removes something you depend on, we will give reasonable notice through the [changelog](/changelog) and, for material changes, directly. #### 2. Accounts You need an account to use the service. You are responsible for the accuracy of your account details, for the security of the email address we authenticate you through, and for what the members of your organisation do under it. Tell us promptly at **security@glotto.dev** if you believe an account has been compromised. You must be able to form a binding contract to use the service, and you must not use it if a law that applies to you prohibits it. #### 3. Your content **You own your content.** Your API specifications, configuration, generated SDKs, documentation, and MCP servers remain yours. You grant us only the licence we need to operate the service for you: to host, process, transmit, and generate from your content, and to make it available to the people you authorise. You are responsible for having the rights to the content you give us and for it not being unlawful. We process customer content only to provide the service. We do not use it to train machine-learning models, and we do not sell it. Where your content contains personal data, our [Privacy Policy](/legal/privacy) and the [Sub-processors](/legal/subprocessors) page describe how it is handled and by whom. #### 4. Generated output Artefacts the service generates from your specification — SDKs, docs, MCP servers — are yours to use, modify, publish, and license as you see fit. Where an artefact is generated from one of our templates, the template's own licence is included with it. Glotto's verification machinery is designed to detect drift between your specification and your generated surface. It is a strong check, not a proof of fitness for your particular purpose: you remain responsible for reviewing and testing what you ship to your own users. #### 5. Acceptable use You must not: - use the service to build or distribute unlawful, infringing, or malicious software; - attempt to gain unauthorised access to the service, other customers' data, or the infrastructure behind it, or probe or load-test it without our written permission; - circumvent quotas, rate limits, or access controls; - resell or provide the service to third parties except as a documented feature allows; - use the service to send unsolicited messages, or to process data you have no right to process; - direct the service at network destinations you do not control or have permission to reach. We may suspend access without notice where continued use presents a security risk, a legal risk, or an immediate threat to the service's availability for others. We will restore access as soon as the cause is resolved. #### 6. Third-party services Where you connect the service to a third party — a version-control host, an identity provider, a package registry — your use of that third party is governed by its own terms, and you are responsible for the credentials you give us for it. The third parties that process data on **our** behalf are listed on the [Sub-processors](/legal/subprocessors) page. #### 7. Fees Paid plans are billed in advance on the interval shown at checkout, through our payment processor. Fees exclude taxes, which you are responsible for where they apply. Unless a separate agreement says otherwise, fees already paid are non-refundable, and a plan renews until cancelled. We will give notice before a price change takes effect for your account. Free and trial access may be limited or withdrawn. #### 8. Confidentiality Each of us may receive the other's non-public information. Neither will disclose it except to people who need it and are under a duty of confidence, and each will protect it with at least reasonable care. This does not cover information that is public through no fault of the recipient, was already known to it, or is independently developed — nor does it prevent a disclosure the law requires. #### 9. Warranties and disclaimers We will provide the service with reasonable skill and care. **Except as expressly stated, the service is provided "as is" and we disclaim all other warranties**, including implied warranties of merchantability, fitness for a particular purpose, and non-infringement. We do not warrant that the service will be uninterrupted or error-free. #### 10. Limitation of liability To the maximum extent the law allows, neither party is liable for indirect, incidental, special, consequential, or punitive damages, or for lost profits, revenue, or data. Our total liability arising out of these terms is limited to the fees you paid us in the twelve months before the event giving rise to the claim. Nothing here limits liability that cannot lawfully be limited — including for death or personal injury caused by negligence, or for fraud. #### 11. Term and termination These terms run until terminated. You may close your account at any time. We may terminate for a material breach that is not cured within 30 days of notice, or immediately where section 5 allows suspension and the cause is not resolved. On termination you may export your content for **30 days**, after which we delete it as described in the [Privacy Policy](/legal/privacy). Sections 3 (ownership), 8, 9, 10, and 12 survive. #### 12. General These terms are governed by the laws of the State of Delaware, without regard to its conflict-of-law rules, and the state and federal courts located in Delaware have exclusive jurisdiction — except that either party may seek injunctive relief wherever necessary to protect its intellectual property. You may not assign these terms without our consent; we may assign them to a successor in a merger or sale of assets. If a provision is unenforceable, the rest stands. A failure to enforce a right is not a waiver of it. These terms, with any separate written agreement between us, are the entire agreement on their subject. "Glotto", the Glotto wordmark, and the Glotto logo are trademarks of Glotto, Inc.; these terms grant no trademark rights. #### 13. Changes We may update these terms. Where a change materially affects you, we will give notice before it takes effect, and the "last updated" date above will change. Continuing to use the service after that date means you accept the revised terms. #### Contact Glotto, Inc. — **hello@glotto.dev**