Admin API
The Connect-RPC API behind the admin, how to call it with curl or a generated client, API tokens and their profiles, and what a token may and may not do.
On this page
The admin UI talks to the panel through a Connect-RPC API. Scripts use the same API, with the same role checks, validation and audit log, by sending an API token instead of a session cookie. AI agents use the same tokens through the MCP server.
Where the API lives#
Every procedure is a POST to the admin URL plus api/ and the procedure path:
<admin URL>api/mistgate.admin.v1.<Service>/<Method>
https://panel.example.com/<prefix>/api/mistgate.admin.v1.UserService/ListUsersThe admin URL is the one mistgate setup printed (Settings → Domains shows it to the owner): with a secret prefix, a secret host or a separate listener. The API is only reachable there; on the public side the same path gets the decoy site.
The handlers are connect-go handlers, so the Connect protocol (JSON or binary protobuf), gRPC and gRPC-Web all work. The examples here use Connect with JSON.
Services#
The API is defined in proto/mistgate/admin/v1/. Generated code is in the repository: Go in gen/mistgate/admin/v1 (packages adminv1 and adminv1connect), TypeScript in web/src/gen.
| Service | File | What it covers |
|---|---|---|
AuthService |
auth.proto |
Sign-in, passkeys, password, sessions, step-up, the audit log, captcha settings |
InstanceService |
instance.proto |
Brand settings, the admin and subscription addresses |
FleetService |
fleet.proto |
The overview and the event feed |
NodeService |
node.proto |
Nodes: list, details, settings, install commands, restarts, logs, retirement |
ProfileService |
profile.proto |
Protocols, profiles and profiles on nodes |
GroupService |
group.proto |
Groups |
UserService |
user.proto |
Users, their traffic, devices and subscription link |
DeviceService |
device.proto |
AmneziaWG devices and their keys |
SubscriptionService |
subscription.proto |
Subscription settings and app rules |
DnsService |
dns.proto |
DNS presets |
HealthService |
health.proto |
Alerts, client-eye checks, the doctor and its fixes |
UpdateService |
update.proto |
Release bundle, node updates and rollouts |
WarpService |
warp.proto |
WARP accounts of nodes |
AwgService |
awg.proto |
Helpers of the AmneziaWG profile editor |
ApiTokenService, ApprovalService |
integrations.proto |
API tokens and the owner's approvals |
The comments in the proto files are the reference for every field. Conventions (common.proto):
- Ids are opaque prefixed strings:
nod_…,usr_…,prf_…,grp_…,dev_…,inb_…(a profile on a node). - Times are Unix seconds in fields ending in
_unix; 0 means unset. Byte counters are unsigned 64-bit. - In JSON, field names are lowerCamelCase (
pageSize), enums are their names (USER_FILTER_ONLINE) and 64-bit integers are strings (numbers are accepted on input). - Updates use optional fields: a field that is absent stays unchanged.
- Errors are Connect codes with short messages:
not_found,already_exists,invalid_argument,failed_precondition,aborted(somebody changed it first),permission_denied,unauthenticated,resource_exhausted.
API tokens#
The owner creates tokens in Integrations → API tokens → New token (a fresh step-up is needed):
| Field | Rule |
|---|---|
| Name | 1 to 64 characters, unique among the live tokens ignoring case. It is what the audit log shows. |
| Access | The profile: Read only, Operator or Admin. |
| Lifetime | 90 days by default, at most 365. A token always expires; to renew one, make a new token and revoke the old one. |
| Requests per minute | 120 by default, 1 to 600. |
The secret looks like tk1_ followed by 43 URL-safe characters. It is shown once; the panel keeps only its SHA-256 and the last four characters (to tell tokens apart in the list). At most 50 live tokens exist at a time. The list shows when and from where each token was last used, and through which channel (the API or MCP).
Revoke stops a token at once: its next request is refused and the changes it planned through MCP and did not apply are cancelled. Revoking needs a step-up.
Authentication#
Send the token in one Authorization header with the Bearer scheme:
Authorization: Bearer tk1_...A request with a Bearer header is judged by the token alone; a cookie is never looked at. Refusals:
| Status | Body code |
Message |
|---|---|---|
| 401 | unauthenticated |
not signed in (missing, malformed or unknown token), token revoked, token expired |
| 403 | permission_denied |
this call is not available to API tokens, this token's profile cannot do this, this needs the owner's approval; it is not available over the API |
| 429 | resource_exhausted |
too many requests for this token, slow down, with Retry-After |
Calling the API#
With curl#
TOKEN=$(head -n1 ~/.config/mistgate/token)
ADMIN=https://panel.example.com/<prefix>/
curl -sS "${ADMIN}api/mistgate.admin.v1.UserService/ListUsers" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"filter": "USER_FILTER_ONLINE", "pageSize": 20}'A change looks the same. Extending two users by 30 days (operator profile or higher):
curl -sS "${ADMIN}api/mistgate.admin.v1.UserService/ExtendUsers" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"userIds": ["usr_...", "usr_..."], "days": 30}'An error comes back as JSON with an HTTP error status:
{"code": "permission_denied", "message": "this token's profile cannot do this"}With a generated client#
The Go packages are importable from the module github.com/mistgate/mistgate (there are no tagged releases yet: pin a commit). The client's base URL is the admin URL plus api:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"strings"
"connectrpc.com/connect"
adminv1 "github.com/mistgate/mistgate/gen/mistgate/admin/v1"
"github.com/mistgate/mistgate/gen/mistgate/admin/v1/adminv1connect"
)
// bearer adds the API token to every request.
type bearer struct{ token string }
func (b bearer) RoundTrip(r *http.Request) (*http.Response, error) {
r = r.Clone(r.Context())
r.Header.Set("Authorization", "Bearer "+b.token)
return http.DefaultTransport.RoundTrip(r)
}
func main() {
raw, err := os.ReadFile(os.Getenv("MISTGATE_TOKEN_FILE"))
if err != nil {
log.Fatal(err)
}
token := strings.TrimSpace(strings.SplitN(string(raw), "\n", 2)[0])
hc := &http.Client{Transport: bearer{token}}
// The admin URL that `mistgate setup` printed, plus "api".
base := "https://panel.example.com/<prefix>/api"
users := adminv1connect.NewUserServiceClient(hc, base)
resp, err := users.ListUsers(context.Background(), connect.NewRequest(&adminv1.ListUsersRequest{PageSize: 10}))
if err != nil {
log.Fatal(connect.CodeOf(err), err)
}
for _, u := range resp.Msg.GetUsers() {
fmt.Println(u.GetId(), u.GetName(), u.GetStatus())
}
}For another language, generate a client from proto/ with buf generate and the Connect or gRPC plugin of that language.
Token profiles#
A profile acts as an admin role: Read only as read-only, Operator as helper, Admin as owner. On top of the role, a token may call only the procedures on a fixed allow-list (internal/panel/auth/policy_tokens.go); everything else is refused whatever the profile. Nothing on the list returns a credential.
| Procedure | Read only | Operator | Admin |
|---|---|---|---|
FleetService.Overview, FleetService.ListEvents |
yes | yes | yes |
NodeService.ListNodes, NodeService.GetNode |
yes | yes | yes |
HealthService.ListAlerts, GetChecks, GetDoctor |
yes | yes | yes |
UserService.ListUsers, UserService.GetUser |
yes | yes | yes |
GroupService.ListGroups |
yes | yes | yes |
ProfileService.ListProfiles, ProfileService.GetProfile (secrets masked) |
yes | yes | yes |
SubscriptionService.ListClients, SubscriptionService.TestUserAgent |
yes | yes | yes |
UpdateService.GetUpdates |
yes | yes | yes |
UserService.CreateUser, UpdateUser, SetUsersEnabled, ExtendUsers, ResetUserTraffic, RevokeDevice |
no | yes | yes |
HealthService.MuteAlert, RunChecksNow, RunDoctor |
no | yes | yes |
AuthService.ListAudit |
no | no | yes |
HealthService.ApplyFix |
no | no | only through an approved MCP plan |
UpdateService.StartRollout, PauseRollout, ResumeRollout, CancelRollout, RollbackNode |
no | no | only through an approved MCP plan |
Closed to every token: deleting users, the subscription link, devices and their keys, node enrollment, settings, restarts, logs and retirement, every change to profiles, groups, DNS presets and subscription settings, WARP, brand settings, tokens, approvals, the admins' own account (passkeys, password, sessions, step-up) and the captcha settings.
Changes that need the owner#
A token never passes step-up. Procedures that need a step-up, or that change what runs on the nodes, are never callable with a token directly; over the plain API they answer 403 "this needs the owner's approval; it is not available over the API".
They are reachable only through the MCP server with an Admin token: the agent plans the change, the owner reads the plan in Integrations → Waiting for you and approves it (with a step-up of their own), and only then does the agent's apply make that one call. The approval is checked in the database on every use and opens only the procedure of that plan. A plan expires 10 minutes after it was made.
Limits#
- Requests per minute per token: as set on the token (1 to 600, default 120), with a burst of up to 30. Over the limit: 429 with
Retry-After. - Per client network on the admin surface: 30 requests per second with a burst of 200, for cookies and tokens alike.
- Request size: 64 KiB to 1 MiB depending on the service.
- Lifetime: every token expires (at most 365 days).
Audit#
Every token call writes a row to the audit log: "called <procedure>", the HTTP status, the client address, and the actor "API token <name>" (or "MCP token <name>" through MCP). Successful reads are written at most once a minute per procedure; changes and refusals every time. The rows of the procedure itself (for example "created the user …") are written as for an admin.
What responses never contain#
CreateUsercalled with a token returns the user without the subscription link and the page password.GetSubscriptionLinkis closed to tokens.- Profile secrets come back masked (
••••), for every caller. - Device keys and configurations (
DeviceService) and WARP accounts are closed to tokens. - Token secrets are never returned after creation, not even to the owner.
- Audit parameters never contain secrets.