HTTP API
gestaltd exposes a REST API and an optional MCP endpoint. When server.management is not configured, every route below is served from the public listener. When server.management is configured, /admin, /admin/api/v1/*, /metrics, /health, and /ready move to the management listener and return 404 on the public listener. Prefer the split-listener setup for production so the operator surface does not share the public listener.
App catalog and connection routes use /api/v1/apps. The CLI uses the app command (alias apps), while server config uses the apps map. See Config File for context.
Authentication model
Authenticated routes accept a session_token cookie or an Authorization: Bearer <token> header. Valid bearer tokens include Gestalt API tokens (gst_api_...) and tokens issued by the configured platform authentication provider when it supports direct token validation.
Unauthenticated Routes
| Method | Path | Purpose |
|---|---|---|
GET | /health | Basic liveness check. |
GET | /ready | Readiness check. Returns 503 until providers and datastore are ready. |
GET | /admin and /admin/* | Admin UI shell plus static assets. Gestalt serves this from server.admin.ui when configured, otherwise from the root providers.ui bundle when that bundle contains admin/index.html, and otherwise from the built-in fallback shell. The shell includes a Prometheus dashboard and a app authorization workspace at ?tab=members. When server.admin.authorizationPolicy is set, Gestalt requires browser session authentication and the configured roles before serving the shell or its assets. On split public/management deployments, configure server.management.baseUrl so unauthenticated management /admin requests can round-trip through the public login flow and return to the management listener’s /admin route after callback. Use the same hostname as server.baseUrl, and keep both on https when the public listener uses https, so the session cookie can be reused across listeners. /admin still reads same-origin /metrics, which remains unauthenticated on the management listener. When server.baseUrl is set, the management admin UI links back to that public URL for Client UI; otherwise it omits that link. |
GET | /api/v1/auth/info | Returns platform authentication metadata, including whether interactive login is supported. |
POST | /api/v1/auth/login | Starts platform login and returns an absolute browser URL. |
GET | /api/v1/auth/login/callback | Completes platform login. |
POST | /api/v1/auth/logout | Clears the current platform session. |
GET | /api/v1/auth/callback | Completes app OAuth. |
POST | /api/v1/auth/pending-connection | Render or finalize a multi-candidate connection choice using a pending connection token. |
GET, POST, PUT, PATCH, or DELETE | /api/v1/{app}/{hostedPath} | Invoke a app-hosted HTTP binding declared by spec.http or apps.<name>.http. The route is verified by the binding’s security scheme, not by Gestalt session authentication. |
Authenticated User Routes
Apps
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/apps | List apps, aggregate status, icons, and nested connection metadata. Authentication types, instances, credential fields, and connection parameter hints are returned per item in connections[]. |
DELETE | /api/v1/apps/{name} | Disconnect a app. Use ?_connection=... and ?_instance=... to target one stored connection. |
GET | /api/v1/apps/{name}/operations | List operations for one app. Supports _connection and _instance selectors. |
GET or POST | /api/v1/{app}/{operation} | Invoke a app operation. Supports _connection and _instance. |
GET /api/v1/apps returns one object per app. Use top-level status,
credentialState, healthState, and actions for aggregate display state. Use
connections[] for connection-specific auth and credential metadata:
{
"name": "github",
"status": "ready",
"credentialState": "connected",
"connections": [
{
"name": "default",
"status": "ready",
"authTypes": ["oauth"],
"instances": [{"id": "acme", "name": "Acme"}],
"connectionParams": {},
"credentialFields": []
}
]
}App Connection Flows
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/auth/start-oauth | Start a app OAuth flow. |
POST | /api/v1/auth/connect-manual | Submit manual app credentials. |
Workflows
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/workflow/schedules | List user-owned workflow schedules. |
POST | /api/v1/workflow/schedules | Create a user-owned workflow schedule. |
GET | /api/v1/workflow/schedules/{scheduleID} | Inspect one workflow schedule. |
PUT | /api/v1/workflow/schedules/{scheduleID} | Update one workflow schedule. |
DELETE | /api/v1/workflow/schedules/{scheduleID} | Delete one workflow schedule. |
POST | /api/v1/workflow/schedules/{scheduleID}/pause | Pause one workflow schedule. |
POST | /api/v1/workflow/schedules/{scheduleID}/resume | Resume one workflow schedule. |
GET | /api/v1/workflow/event-triggers | List user-owned workflow triggers. |
POST | /api/v1/workflow/event-triggers | Create a user-owned workflow trigger. |
GET | /api/v1/workflow/event-triggers/{triggerID} | Inspect one workflow trigger. |
PUT | /api/v1/workflow/event-triggers/{triggerID} | Update one workflow trigger. |
DELETE | /api/v1/workflow/event-triggers/{triggerID} | Delete one workflow trigger. |
POST | /api/v1/workflow/event-triggers/{triggerID}/pause | Pause one workflow trigger. |
POST | /api/v1/workflow/event-triggers/{triggerID}/resume | Resume one workflow trigger. |
POST | /api/v1/workflow/events | Publish one workflow event. Gestalt normalizes the event and fans it out across the configured workflow providers. |
GET | /api/v1/workflow/runs | List workflow runs. Supports optional app and status query filters. |
GET | /api/v1/workflow/runs/{runID} | Inspect one workflow run. |
POST | /api/v1/workflow/runs/{runID}/cancel | Cancel one workflow run. The request body may include an optional reason. |
Schedule, event-trigger, and run responses return a target.steps array. Each
step has an id and exactly one of app or agent. App steps use
app.name, app.operation, and optional connection, instance, and
workflow-value input fields. Agent steps use agent.provider, optional
model, sessionKey, prompt, messages, tools, responseSchema, and
modelOptions. Schedule and trigger create/update requests use the same
shape.
{
"target": {
"steps": [
{
"id": "sync",
"app": {
"name": "roadmap",
"operation": "sync_items",
"input": {
"literal": { "mode": "incremental" }
}
}
}
]
}
}Agents
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/agent/sessions | Create one agent session. Accepts an optional idempotencyKey field and optional Idempotency-Key header. |
GET | /api/v1/agent/sessions | List agent sessions. Supports optional provider and state query filters. |
GET | /api/v1/agent/sessions/{sessionID} | Inspect one agent session. |
PATCH | /api/v1/agent/sessions/{sessionID} | Update one agent session. |
POST | /api/v1/agent/sessions/{sessionID}/turns | Create one agent turn inside a session. Accepts an optional idempotencyKey field and optional Idempotency-Key header. |
GET | /api/v1/agent/sessions/{sessionID}/turns | List turns for one session. Supports optional status. |
GET | /api/v1/agent/turns/{turnID} | Inspect one agent turn. |
POST | /api/v1/agent/turns/{turnID}/cancel | Cancel one agent turn. The request body may include an optional reason. |
GET | /api/v1/agent/turns/{turnID}/events | List stored events for one turn. Supports optional after sequence cursor and limit. |
GET | /api/v1/agent/turns/{turnID}/events/stream | Stream turn events as server-sent events. Supports optional after sequence cursor, limit, and until=terminal|blocked_or_terminal. |
GET | /api/v1/agent/turns/{turnID}/interactions | List provider-owned interactions for one turn. |
POST | /api/v1/agent/turns/{turnID}/interactions/{interactionID}/resolve | Resolve one pending interaction by submitting a resolution payload. |
Session creation accepts provider, model, optional clientRef, optional
metadata, optional workspace, and optional idempotencyKey.
workspace.checkouts[] entries contain url, optional ref, and relative
path. workspace.cwd is also relative and must point inside one of the
prepared checkouts.
Turn creation accepts model, messages, optional toolRefs, optional
toolSource, optional responseSchema, optional metadata, optional
modelOptions, and optional idempotencyKey. Missing
toolRefs[].appName or toolRefs[].operation returns 400. Conflicting
header/body idempotency keys also return 400, an in-progress idempotent
create returns 409, and an unconfigured agent surface returns 412.
Each messages[] entry accepts role, optional text, optional parts, and
optional metadata. parts[] supports type: text | json | tool_call | tool_result | image_ref plus the corresponding text, json, toolCall,
toolResult, or imageRef payload.
Turn event responses include raw type, source, visibility, and data
fields. They may also include an optional display projection with kind,
phase, text, label, ref, parentRef, input, output, and error.
Clients should prefer display when it is usable and fall back to the raw event
fields otherwise. Initial kind values are text, reasoning, tool,
interaction, status, and error; initial phase values are delta,
completed, started, progress, failed, requested, resolved, and
canceled. The display projection is not a privacy boundary; clients must
still honor each event’s visibility.
API Tokens
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/tokens | Create an API token. Plaintext is returned once. |
GET | /api/v1/tokens | List API tokens for the current user. |
DELETE | /api/v1/tokens/{id} | Revoke an API token. |
DELETE | /api/v1/tokens | Revoke all API tokens for the current user. |
Managed Subjects
Managed subjects are the API representation of
service accounts. Their canonical subject ID is always
service_account:<id>, and their upstream credentials and subject-owned API
tokens use that same subject ID. These management routes require a browser
session or an unscoped user-owned API token; scoped API tokens and subject-owned
API tokens cannot manage managed subjects.
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/authorization/subjects | List managed subjects the current user can view. |
POST | /api/v1/authorization/subjects | Create a managed subject. The creator becomes an admin of that managed subject. |
GET | /api/v1/authorization/subjects/{subjectID} | Inspect one managed subject. |
PATCH | /api/v1/authorization/subjects/{subjectID} | Update display metadata. Requires managed subject admin. |
DELETE | /api/v1/authorization/subjects/{subjectID} | Delete one managed subject, revoke its API tokens, remove its external credentials, and delete its authorization relationships. Requires managed subject admin. |
GET | /api/v1/authorization/subjects/{subjectID}/members | List users or subjects that can manage the managed subject. |
PUT | /api/v1/authorization/subjects/{subjectID}/members | Upsert a managed subject member by canonical subjectId or user email; role must be viewer, editor, or admin. Requires managed subject admin. |
DELETE | /api/v1/authorization/subjects/{subjectID}/members/{memberSubjectID} | Remove a managed subject member. Requires managed subject admin. |
GET | /api/v1/authorization/subjects/{subjectID}/grants | List app authorization grants for the managed subject. |
PUT | /api/v1/authorization/subjects/{subjectID}/grants/{app} | Grant a app role to the managed subject. Requires managed subject admin and the caller must already hold the requested app role, or app admin. |
DELETE | /api/v1/authorization/subjects/{subjectID}/grants/{app} | Remove a app authorization grant from the managed subject. Requires managed subject admin. |
GET | /api/v1/authorization/subjects/{subjectID}/external-identities | List external identities the managed subject may assume. Requires managed subject admin. |
PUT | /api/v1/authorization/subjects/{subjectID}/external-identities | Grant an external identity assumption relationship. Body includes type and id. Requires managed subject admin. |
DELETE | /api/v1/authorization/subjects/{subjectID}/external-identities | Remove an external identity assumption relationship. Body includes type and id. Requires managed subject admin. |
GET | /api/v1/authorization/subjects/{subjectID}/integrations | List apps with connection state resolved against the managed subject’s credential subject. Requires managed subject viewer. |
POST | /api/v1/authorization/subjects/{subjectID}/auth/start-oauth | Start a app OAuth flow that stores credentials on the managed subject. Requires managed subject editor. |
POST | /api/v1/authorization/subjects/{subjectID}/auth/connect-manual | Submit manual app credentials for the managed subject. Requires managed subject editor. |
DELETE | /api/v1/authorization/subjects/{subjectID}/integrations/{name} | Disconnect one app credential from the managed subject. Requires managed subject editor. |
GET | /api/v1/authorization/subjects/{subjectID}/tokens | List API tokens owned by the managed subject. |
POST | /api/v1/authorization/subjects/{subjectID}/tokens | Create a subject-owned API token. Plaintext is returned once. Requires managed subject admin. |
DELETE | /api/v1/authorization/subjects/{subjectID}/tokens/{id} | Revoke one subject-owned API token. Requires managed subject admin. |
DELETE | /api/v1/authorization/subjects/{subjectID}/tokens | Revoke all API tokens owned by the managed subject. Requires managed subject admin. |
The authorization provider stores managed-subject management as relationships on the managed_subject resource type. App runtime access is separate: a managed subject can only invoke an app after it has an explicit app grant, which is stored as the same provider-backed dynamic app relationship used by the admin authorization API. Policy default: allow applies to user callers, not to managed subjects. Credential connection management is also subject-scoped: OAuth, manual credentials, pending connection selection, and disconnects store or remove credentials under the managed subject’s canonical subject ID.
For the conceptual model and setup flow, see Service Accounts.
Operator Surfaces
| Method | Path | Purpose |
|---|---|---|
GET | /metrics | Prometheus scrape endpoint for emitted metrics. Requires Gestalt authentication only when served on the public listener. The recommended production pattern is to move it to server.management and protect that listener at the network or proxy layer. |
GET | /admin/api/v1/runtime/providers | List configured runtime providers. When support inspection succeeds, loaded providers include a derived profile with advertised behavior reported by the runtime and effective behavior resolved for this host deployment. error explains any current inspection failure. |
GET | /admin/api/v1/runtime/providers/{provider}/sessions | List active sessions for one runtime provider, including session id, lifecycle state, and the hosted app when known. Supports pageSize and pageToken, and returns sessions plus nextPageToken when more sessions are available. |
GET | /admin/api/v1/authorization/provider | Inspect the configured authorization provider, capabilities, and active model. |
GET | /admin/api/v1/authorization/models | List authorization provider models. Supports pageSize and pageToken. |
GET | /admin/api/v1/authorization/relationships | List authorization provider relationships for debugging. Supports modelId, subject, relation, resource, pageSize, and pageToken filters. |
GET | /admin/api/v1/authorization/apps | List apps that declare authorizationPolicy, including the bound policy name and mounted UI path when present. |
GET | /admin/api/v1/authorization/apps/{app}/members | List merged static and dynamic subject membership rows for one app. Rows include source, effective, mutable, and selector metadata. |
PUT | /admin/api/v1/authorization/apps/{app}/members | Upsert one dynamic subject membership row for a app by canonical subjectId or by user-only email alias and role. Rejects subjects who already have static authorization on that app. |
DELETE | /admin/api/v1/authorization/apps/{app}/members/{subjectID} | Remove one dynamic subject membership row for a app. Static rows are read-only and cannot be deleted through the API. |
GET | /admin/api/v1/authorization/admins/members | List merged static and dynamic built-in admin membership rows. |
PUT | /admin/api/v1/authorization/admins/members | Upsert one dynamic built-in admin membership row by canonical subjectId or by user-only email alias and role. Rejects subjects who already have static built-in admin authorization. |
DELETE | /admin/api/v1/authorization/admins/members/{subjectID} | Remove one dynamic built-in admin membership row. Static rows are read-only and cannot be deleted through the API. |
/admin/api/v1/... mirrors the built-in /admin protection model for global admin and debug routes: when server.admin.authorizationPolicy is unset, the routes are open wherever /admin is open; when it is set, they require the same browser session and role checks as /admin, but return JSON 401/403 instead of browser redirects. App member routes also accept callers with role admin on the specific target app, so app admins can manage dynamic grants for their own app without being built-in Gestalt admins.
Mutable membership APIs still list and delete by canonical subjectId. User-backed rows use user:<user_id> and non-human rows can use service_account:<id> or another canonical non-system subject ID. Dynamic membership writes accept either a canonical subjectId or an email alias. When email is used, Gestalt resolves it to the canonical user subject and creates the user record if needed before persisting the provider-backed grant.
PUT and DELETE may return 202 Accepted with status: "persisted_pending_reload" when the dynamic grant was written successfully but the local in-memory authorization snapshot has not reloaded yet. In that case, retry access checks only after the local snapshot converges.
MCP
If at least one app contributes MCP-visible tools or an upstream MCP surface, Gestalt also mounts:
| Method | Path | Purpose |
|---|---|---|
GET or POST | /mcp | Model Context Protocol endpoint. |
/mcp is authenticated with the same session or bearer-token middleware as the rest of the authenticated API.
Invocation Semantics
When you call:
/api/v1/{integration}/{operation}Gestalt authenticates the caller, resolves the target app and operation, then resolves credentials based on the connection’s mode. If the user has exactly one stored instance it is selected automatically; if there are multiple and none is specified, the call fails with an ambiguity error. OAuth tokens are refreshed when needed before the provider operation executes.
Parameter conventions
GET invocations read parameters from the query string; POST invocations read parameters from a JSON body. Use _connection to select a named connection when a app exposes more than one, and _instance to select one stored instance for that connection.
Examples
# Simple invocation
curl http://localhost:8080/api/v1/httpbin/get_headers
# GET with connection and instance selectors
curl 'http://localhost:8080/api/v1/slack/chat.postMessage?_connection=mcp&_instance=workspace-123&channel=C123&text=hello'
# POST with connection selector and parameters
curl -X POST http://localhost:8080/api/v1/slack/chat.postMessage \
-H 'Content-Type: application/json' \
-d '{"_connection": "mcp", "channel": "C123", "text": "hello"}'
# Create a workflow schedule
curl -X POST http://localhost:8080/api/v1/workflow/schedules \
-H 'Authorization: Bearer gst_api_...' \
-H 'Content-Type: application/json' \
-d '{
"cron": "0 */6 * * *",
"timezone": "America/New_York",
"target": {
"steps": [{
"id": "list-issues",
"app": {
"name": "github",
"operation": "issues.list",
"input": {
"literal": {
"owner": "valon-technologies",
"repo": "gestalt"
}
}
}
}]
}
}'
# Create a workflow trigger
curl -X POST http://localhost:8080/api/v1/workflow/event-triggers \
-H 'Authorization: Bearer gst_api_...' \
-H 'Content-Type: application/json' \
-d '{
"match": {
"type": "roadmap.item.updated",
"source": "roadmap",
"subject": "item"
},
"target": {
"steps": [{
"id": "notify",
"app": {
"name": "slack",
"operation": "chat.postMessage",
"input": {
"literal": {
"channel": "C123",
"text": "Roadmap item updated"
}
}
}
}]
}
}'
# Publish a workflow event
curl -X POST http://localhost:8080/api/v1/workflow/events \
-H 'Authorization: Bearer gst_api_...' \
-H 'Content-Type: application/json' \
-d '{
"type": "roadmap.item.updated",
"source": "roadmap",
"subject": "item",
"dataContentType": "application/json",
"data": {
"id": "item-1"
},
"extensions": {
"traceId": "trace-1"
}
}'
# List failed workflow runs for one app
curl 'http://localhost:8080/api/v1/workflow/runs?app=github&status=failed' \
-H 'Authorization: Bearer gst_api_...'
# Create an agent session
curl -X POST http://localhost:8080/api/v1/agent/sessions \
-H 'Authorization: Bearer gst_api_...' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: agent-session-1' \
-d '{
"provider": "managed",
"model": "gpt-5.4",
"clientRef": "roadmap-risk",
"workspace": {
"checkouts": [
{
"url": "https://github.com/valon-technologies/roadmap.git",
"ref": "main",
"path": "roadmap"
}
],
"cwd": "roadmap"
},
"metadata": {
"ticket": "RD-42"
}
}'
# Create a turn in that session
curl -X POST http://localhost:8080/api/v1/agent/sessions/session-123/turns \
-H 'Authorization: Bearer gst_api_...' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: agent-turn-1' \
-d '{
"model": "gpt-5.4",
"messages": [
{
"role": "system",
"text": "Be concise.",
"parts": [
{"type": "text", "text": "Be concise."}
]
},
{
"role": "user",
"text": "Summarize the roadmap risk.",
"parts": [
{"type": "text", "text": "Summarize the roadmap risk."},
{
"type": "json",
"json": {
"ticket": "RD-42"
}
}
],
"metadata": {
"ticket": "RD-42"
}
}
],
"toolRefs": [
{"appName": "roadmap", "operation": "sync"}
],
"responseSchema": {
"type": "object",
"properties": {
"summary": {"type": "string"}
},
"required": ["summary"]
},
"metadata": {
"ticket": "RD-42"
}
}'
# List active sessions for one provider
curl -H 'Authorization: Bearer gst_api_...' \
'http://localhost:8080/api/v1/agent/sessions?provider=managed&state=active'
# Inspect a session
curl -H 'Authorization: Bearer gst_api_...' \
http://localhost:8080/api/v1/agent/sessions/session-123
# Inspect a turn
curl -H 'Authorization: Bearer gst_api_...' \
http://localhost:8080/api/v1/agent/turns/turn-123
# List session turns
curl -H 'Authorization: Bearer gst_api_...' \
'http://localhost:8080/api/v1/agent/sessions/session-123/turns?status=running'
# List turn events after sequence 0
curl -H 'Authorization: Bearer gst_api_...' \
'http://localhost:8080/api/v1/agent/turns/turn-123/events?after=0&limit=100'
# Stream turn events until the turn reaches a terminal state
curl -N -H 'Authorization: Bearer gst_api_...' \
'http://localhost:8080/api/v1/agent/turns/turn-123/events/stream?after=0'
# Stream turn events until the turn is terminal or waiting on an interaction
curl -N -H 'Authorization: Bearer gst_api_...' \
'http://localhost:8080/api/v1/agent/turns/turn-123/events/stream?after=0&until=blocked_or_terminal'
# List pending turn interactions
curl -H 'Authorization: Bearer gst_api_...' \
'http://localhost:8080/api/v1/agent/turns/turn-123/interactions'
# Resolve a pending interaction
curl -X POST http://localhost:8080/api/v1/agent/turns/turn-123/interactions/interaction-123/resolve \
-H 'Authorization: Bearer gst_api_...' \
-H 'Content-Type: application/json' \
-d '{
"resolution": {
"approved": true
}
}'
# Cancel an agent turn
curl -X POST http://localhost:8080/api/v1/agent/turns/turn-123/cancel \
-H 'Authorization: Bearer gst_api_...' \
-H 'Content-Type: application/json' \
-d '{"reason":"operator requested"}'
# Typical session create response
# {
# "id": "session-123",
# "provider": "managed",
# "model": "gpt-5.4",
# "state": "active",
# "clientRef": "roadmap-risk",
# "createdAt": "2026-04-22T00:00:00Z",
# "updatedAt": "2026-04-22T00:00:00Z"
# }
# Typical turn create response
# {
# "id": "turn-123",
# "sessionId": "session-123",
# "provider": "managed",
# "model": "gpt-5.4",
# "status": "running",
# "messages": [
# {"role":"system","text":"Be concise.","parts":[{"type":"text","text":"Be concise."}]},
# {"role":"user","text":"Summarize the roadmap risk.","parts":[{"type":"text","text":"Summarize the roadmap risk."}],"metadata":{"ticket":"RD-42"}}
# ],
# "createdAt": "2026-04-22T00:00:00Z",
# "startedAt": "2026-04-22T00:00:00Z",
# "executionRef": "turn-123"
# }
# Typical interaction list response
# [
# {
# "id": "interaction-123",
# "turnId": "turn-123",
# "type": "approval",
# "state": "pending",
# "title": "Approve external call",
# "prompt": "Allow this turn to continue?",
# "request": {"host":"api.openai.com"}
# }
# ]
# Add a dynamic app member from the management/admin surface by email alias
curl -X PUT http://localhost:9090/admin/api/v1/authorization/apps/sample_app/members \
-H 'Content-Type: application/json' \
-d '{"email":"user@example.com","role":"viewer"}'
# Create a managed subject and grant it viewer access to a app
curl -X POST http://localhost:8080/api/v1/authorization/subjects \
-H 'Authorization: Bearer gst_api_...' \
-H 'Content-Type: application/json' \
-d '{"id":"release-bot","displayName":"Release Bot"}'
curl -X PUT http://localhost:8080/api/v1/authorization/subjects/service_account:release-bot/grants/github \
-H 'Authorization: Bearer gst_api_...' \
-H 'Content-Type: application/json' \
-d '{"role":"viewer"}'
curl -X POST http://localhost:8080/api/v1/authorization/subjects/service_account:release-bot/auth/connect-manual \
-H 'Authorization: Bearer gst_api_...' \
-H 'Content-Type: application/json' \
-d '{"integration":"github","credential":"ghp_..."}'
curl http://localhost:8080/api/v1/authorization/subjects/service_account:release-bot/integrations \
-H 'Authorization: Bearer gst_api_...'
curl -X POST http://localhost:8080/api/v1/authorization/subjects/service_account:release-bot/tokens \
-H 'Authorization: Bearer gst_api_...' \
-H 'Content-Type: application/json' \
-d '{"name":"release-bot","permissions":[{"app":"github","operations":["issues.list"]}]}'