OpenAPI and client generation

Every page on this site is written by hand. The document your code should build against is not: Gate generates one OpenAPI 3.1 description from the route definitions themselves, so it cannot drift from what the server actually accepts.

Where the spec lives

Both paths are relative to /api/v1.

GET/openapi.jsonnone
GET/docsnone

/openapi.json is the document. /docs is Swagger UI over it, also reachable at /docs/api-reference so you can try calls from this site.

curl
curl -sS https://evonia-gate.annatarhe.com/api/v1/openapi.json -o gate.json

Neither route requires a credential, which makes the spec safe to fetch from CI or a build step without provisioning a key for it.

What it covers

One document describes both of Gate's surfaces.

PrefixServed byCovers
/api/v1GateOrganizations, projects, service accounts, RBAC, configuration, usage, billing
/api/authbetter-authSessions, sign-in, organizations, and the OAuth 2.1 and OIDC protocol routes

The two are merged, not concatenated, so the /api/auth half is namespaced to keep it apart from the control plane. Its operations are tagged Auth / Default, Auth / Organization, Auth / Oauth-provider, and Auth / Jwt — that casing is better-auth's — and its component schemas are prefixed Auth…: AuthUser, AuthSession, AuthOrganization. The prefix is not cosmetic; Organization means different things on the two surfaces and would otherwise collide.

Because one document spans both prefixes, its single servers entry is the origin and every path key carries its prefix in full — /api/v1/authorize, /api/auth/oauth2/token. A generated client needs https://evonia-gate.annatarhe.com as its base URL, not .../api/v1.

Security schemes

Three schemes are declared, and which one applies depends on who is calling.

SchemeCredentialUsed by
cookieAuthbetter-auth.session_token cookieBrowsers — the Gate dashboard and your own web app
apiKeyAuthX-API-Key: gate_sk_…Service accounts calling /api/v1
oauth2Authorization code or client credentialsTokens Gate issues to other products

oauth2 is the odd one out. It describes tokens your users and services carry to your APIs, which then verify them against Gate's JWKS — see Verifying tokens. It is not a way to call /api/v1; for that you want a service API key. Both of its flows advertise the same eight scopes, listed in Scopes and roles.

The vendored copy

The repository commits the generated document at openapi/gate.json, and a test regenerates and compares it on every run, so it cannot fall behind the routes. After an intentional API change, pnpm openapi:snapshot rewrites it.

That copy exists for you: a downstream repository can vendor or submodule it and pin an exact version, so builds do not need a reachable Gate and a deploy cannot silently change the types you compiled against.

It differs from a freshly fetched document in one place. The live servers entry reflects the GATE_BASE_URL the instance runs with, so a spec pulled from a Gate you started locally says http://localhost:3621; the committed copy is pinned to the production origin so it does not churn with whoever regenerated it.

Generate a client

npx openapi-typescript https://evonia-gate.annatarhe.com/api/v1/openapi.json -o src/gate-api.d.ts

TypeScript

openapi-typescript emits types only — no runtime, no client. You keep using fetch and index into the generated paths for the request and response shapes.

TypeScript
import type { paths } from "./gate-api";

type Authorize = paths["/api/v1/authorize"]["post"];
// `requestBody` is optional in the generated type, hence NonNullable.
type AuthorizeBody = NonNullable<Authorize["requestBody"]>["content"]["application/json"];
type AuthorizeResult = Authorize["responses"][200]["content"]["application/json"];

export async function authorize(body: AuthorizeBody): Promise<AuthorizeResult> {
  const response = await fetch("https://evonia-gate.annatarhe.com/api/v1/authorize", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-api-key": process.env.GATE_API_KEY ?? "",
    },
    body: JSON.stringify(body),
  });

  if (!response.ok) throw new Error(`authorize failed with ${response.status}`);
  // The envelope is part of the type: read `.data`, not the object itself.
  return (await response.json()) as AuthorizeResult;
}

Point the generator at openapi/gate.json instead of the URL if you have vendored the spec.

Go

oapi-codegen generates both the types and a client whose methods come from each operation's operationId. The go tool form above needs the generator in your go.mod tool directives; if it is not there yet, run it directly:

curl
go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest \
  -package gate -generate types,client openapi/gate.json > gate/client.go

Generating from the committed openapi/gate.json rather than the live URL keeps go generate hermetic, which matters more in Go than in TypeScript because the output is checked in.

Response conventions

Three things carry through from the API conventions into whatever the generator produces.

  • Successes are wrapped. Every 2xx body is { "data": … }, with two deliberate exceptions that carry no resource: GET /api/v1/health and the Stripe webhook receiver. The generated type includes the wrapper, so unwrap .data at your client boundary rather than at every call site.
  • Errors are problem JSON. type, title, status, and requestId, with errors added for validation failures. Match on type and status; the error catalog lists every one. Most generators type error bodies loosely, so validate them yourself.
  • operationId is stable and unique. It is what generators turn into method names — authorize, listOrganizations, putConfigEntry. A test enforces that every operation has one and that no two collide across the surfaces, and Gate treats renaming one as a breaking change, so a generated client survives a regeneration.