Reference

API reference

Tiyi exposes one ConnectRPC API. The same contract serves the embedded Admin UI (JSON over HTTP), the tiyi CLI (JSON over HTTP), and the agent stream (gRPC bidi) — so anything the UI can do, a script can do too. 238 RPCs across 23 services, plus a small non-RPC HTTP surface.

Transport

Every procedure is a POST to /{package}.{Service}/{Method}, for example POST /tiyi.v1.SiteService/ListSites. Three wire protocols share the same routes:

ProtocolContent-TypeUse it for
Connect (unary)application/jsoncurl, scripts, CI — no framing and no generated code required.
Connect (streaming)application/connect+jsonserver-streaming and bidi RPCs from non-gRPC clients.
gRPC / gRPC-Webapplication/grpcgenerated gRPC clients; the agent stream.

The API listens on the management address (--addr, default 0.0.0.0:8080) and, in parallel, on a local Unix socket (--admin-socket, default /run/tiyi/admin.sock).

Authentication

Three auth surfaces, picked by deployment context:

SurfaceMechanismUsed by
Local admin socket OS file permissions on admin.sock tiyi CLI on the same host as the server. Requests run as a synthetic superadmin, which is why password recovery works with no credentials.
HS256 JWT Authorization: Bearer <jwt> over HTTP Admin UI, remote CLI, anything driving the API from off-host. Issued by AuthService.Login, renewed by AuthService.Refresh.
Agent stream token One-use enrollment token → long-lived stream token Agents on AgentStreamService.Connect. Tokens come from AgentService.IssueEnrollmentToken.

Login

$ curl -sS http://tiyi:8080/tiyi.v1.AuthService/Login \
    -H "Content-Type: application/json" \
    -d '{"username":"admin","password":"..."}'
{
  "accessToken": "<jwt>",
  "expiresAt": "2026-07-26T21:13:41Z",
  "user": {
    "id": "<uuid>",
    "tenantId": "<uuid>",
    "username": "admin",
    "roles": ["Administrator"],
    "accessCodes": ["site:read", "site:write", "..."]
  }
}

Three things that are easy to get wrong:

Default lifetimes are 8 hours for the access token (auth.access_token_ttl) and 7 days for the refresh token (auth.refresh_token_ttl). LDAP and RADIUS, when configured, are tried as fallbacks on the same Login call.

Local admin socket

$ curl -sS --unix-socket /run/tiyi/admin.sock \
    http://tiyi.local/tiyi.v1.SystemService/GetSystemSettings \
    -H "Content-Type: application/json" -d '{}'

http://tiyi.local is a placeholder — the socket path decides the destination. The socket is 0600 by default; --admin-socket-mode 0660 --admin-socket-group tiyi-admin shares it with a Unix group. Anyone who can open it has full control, so treat that group as root-equivalent.

CLI

$ export TIYI_API=https://tiyi.example.com   # omit to use the local socket
$ export TIYI_TOKEN="$TOKEN"
$ tiyi site list

Flags --api, --token, and --admin-socket override the environment per command.

Request conventions

Revisions

Versioned resources carry a revision that increments on every write. Send the revision you read; if another writer got there first the call fails with failed_precondition: revision conflict. Re-read, re-apply, retry. Omitting revision skips the check.

Routing and per-site policy overrides have their own RPCs (UpdateSiteRouting, UpsertSitePolicyOverride) so that a routing-unaware client cannot erase them with a plain UpdateSite.

Pagination

Bounded lists (sites, users, certs, policies) use offset pages: {"page":{"page":1,"pageSize":50}}, 1-based, default size 50, capped at 200. Log and event queries use cursors: pass the previous nextCursor and stop when hasMore is false. For direct page jumps set offset instead of cursor — capped at 100,000 rows, past which you should narrow the time range.

The 23 services

ServiceScope
AuthServiceLogin, logout, refresh, current user, access codes, password change.
SystemServiceHealth, settings, dashboard rollups, declarative apply, CRS/GeoIP/binary releases, upgrade runs.
MenuServicePermission-filtered navigation for the management UI.
UserServiceUser CRUD, role assignment, lock/unlock, password reset.
RoleServiceRole CRUD and the permission catalog.
SiteServiceSite CRUD, enable/disable, compiled-config preview, path routing, per-site policy override.
UpstreamServiceUpstream pool CRUD; deletion refused while in use.
CertServiceUpload, ACME issue/renew, download, DNS provider CRUD.
PolicyServiceWAF policy CRUD, layer updates, version snapshots and rollback, SecLang preview, test lab.
RuleOverrideServicePer-CRS-rule behaviour and scoped override lifecycle.
CustomRuleServiceCustom SecLang and visual-rule lifecycle, ordering, templates.
IpListServiceReusable IP data, scoped bindings, CSV import/export, lookup and precedence.
RateLimitServiceEndpoint and client-scope rate-limit resources.
CrsServiceCRS catalog browse, ingest, exclusion-package install and attach.
AgentServiceAgent CRUD, enrollment tokens, install scripts, commands, config bundles, metric samples.
AgentGroupServiceTag-based and explicit agent targeting with match preview.
AgentStreamServiceThe authenticated bidi agent session.
EvidenceUploadServiceRequest-evidence upload on its own stream, so large bodies never block control traffic.
TrustServiceClient-IP trust profile (tenant + site overrides), CDN snapshots, explain.
AlertServiceAlert rule and channel CRUD, ack/resolve, silences, notes, channel test send.
LogServiceSecurity/access/error event queries, retained evidence, exports, log policy, live tail.
AuditServiceTamper-evident audit query, chain status, and verification.
AIServiceOptional, default-off advisory proposals, copilot analysis, and chat.

The exhaustive per-RPC reference — every request and response field, with the permission each RPC requires — is generated from the schema and ships in the source tree under docs/api/, alongside the protobuf definitions in proto/tiyi/v1/.

Errors

Errors return a canonical code, a human message, and sometimes typed details. Branch on code (and details[].debug.reason), never on message.

{
  "code": "unauthenticated",
  "message": "invalid session",
  "details": [{ "type": "google.rpc.ErrorInfo",
               "debug": { "reason": "TOKEN_INVALID", "domain": "tiyi.io" } }]
}
CodeHTTPTypical causeRetry?
invalid_argument400Input rejected by the validator.No — fix the request.
failed_precondition400Revision conflict, resource in use, or another state guard.Re-read, then retry.
unauthenticated401Missing, malformed, or expired token.Refresh, retry once.
permission_denied403Valid session without the required permission; also ACCOUNT_LOCKED.No.
not_found404Id does not exist or is soft-deleted.No.
already_exists409Uniqueness violated.No.
resource_exhausted429Write-admission queue is full.Yes — honour Retry-After.
unimplemented501Declared in the schema but not wired yet.No.
unavailable503SQLite busy or a dependency is down.Yes — backoff.
deadline_exceeded504Server-side timeout.Narrow the query.
internal500Unhandled server-side failure.No — report it.

Known ErrorInfo reasons: BAD_CREDENTIALS, TOKEN_INVALID, ACCOUNT_LOCKED.

Write admission

Mutating RPCs are queued through a write-admission gate so a burst of writers cannot overwhelm the embedded SQLite store. When the queue is full the call returns resource_exhausted with a Retry-After header in seconds. Reads are never queued. Bulk importers should limit write concurrency and respect that header.

RBAC

Each RPC declares the permission it needs — site:read, policy:write, log:export, system:apply, and so on across 57 permissions. The server checks the JWT subject's roles against the permission table; a role holding tiyi:superadmin passes every check. Administrator is the only built-in role and holds everything, so build least-privilege roles with tiyi role or the Roles page, and inspect the live catalog with RoleService.ListPermissions rather than trusting a frozen list.

Seven procedures are reachable without a session: SystemService.Health, AuthService.Login / Refresh / Logout, AgentService.Enroll, AgentStreamService.Connect, and EvidenceUploadService.Upload. The last three are gated by enrollment or stream tokens instead.

Streaming RPCs

Streaming works over gRPC or the Connect streaming protocol. For Connect, set Content-Type: application/connect+json and frame each message with a five-byte envelope: one flag byte, then a big-endian uint32 length. A failed stream still returns HTTP 200 — the status lives in the trailing frame (flag 0x02), so a client that only checks the HTTP code will miss the error.

Reconnect with backoff. None of these streams replay history, so re-query for anything missed during a gap.

Non-RPC HTTP endpoints

EndpointListenerAuthPurpose
GET /healthzbothnoneLiveness/readiness probe: 200 healthy, 503 otherwise, JSON body either way.
GET /download/tiyibothnoneServes the running binary so a new agent can bootstrap before it has credentials.
/api/v1/telemetry/*bothJWT + telemetry:readExact counters: qps, series, topk, apitree, and the API-inventory action route.
GET /metricssocket onlysocket permissionsPrometheus/OpenMetrics scrape for the observation pipeline.
GET /debug/*socket onlysocket permissionsDiagnostic counters for logsink, alerts, AI, and licensing. Not a stable contract.

The management listener also serves the Admin UI, and unknown paths fall back to the SPA with HTTP 200 — so a successful curl against a path that only exists on the socket is returning HTML, not data. Check the Content-Type.

Generating a client

Point your codegen at proto/tiyi/v1 in the source tree. Known-good combinations are connect-go (what the CLI uses), connect-es / @connectrpc/connect-web (what the Admin UI uses), and any standard gRPC client for streaming-heavy work. Because unary Connect is plain JSON over HTTP, a small fetch or requests wrapper is also a perfectly good client.

The package is tiyi.v1; field numbers are never reused and removed fields stay reserved, so older clients keep parsing newer responses. New optional fields and new RPCs arrive in minor releases — treat unknown response fields as ignorable.

API contract

These docs describe the operator-facing API and should agree with the installed binary — file an issue if they don't.