Skip to content

API Overview ​

Nearly every interaction between the Lamaste management UI and the backend happens through a JSON REST API protected by mTLS client certificates. The exceptions are the invitation acceptance flow (/api/invite/*) and the agent enrollment endpoint (/api/enroll), which are public.

In Plain English ​

When you open the Lamaste management panel in your browser, the page you see is a single-page application (SPA) built with React. Every button you click — creating a tunnel, adding a user, renewing a certificate — sends a request to the panel server's REST API running on the same machine. The server processes the request, makes changes to the system (writing config files, reloading services), and sends back a JSON response.

The API sits behind nginx, which enforces mTLS on the panel vhost — meaning your browser must present a valid client certificate before the connection is even established. Without that certificate, the TLS handshake fails and no HTTP traffic reaches the API at all.

There are two exceptions to the mTLS requirement. The /api/invite/* routes are registered in a public context without mTLS middleware, so that invited users can accept their invitation and set a password without needing a client certificate. The /api/enroll endpoint is also public, allowing agents to complete hardware-bound certificate enrollment using a token previously generated by an admin.

Base URL ​

The API is served by the Fastify panel server at 127.0.0.1:3100. In production, nginx reverse-proxies to this address. You never call port 3100 directly — all requests go through nginx on port 9292.

https://<droplet-ip>:9292/api/...

If a domain has been configured and provisioned:

https://panel.<your-domain>/api/...

Both URLs reach the same server. The IP-based URL always works, even if DNS is misconfigured.

Authentication ​

mTLS Client Certificates ​

All API endpoints except /api/invite/* and /api/enroll require a valid mTLS client certificate. nginx verifies the certificate at the TLS layer and forwards the result to the panel server via the X-SSL-Client-Verify header.

Browser ──HTTPS + client cert──▶ nginx (port 9292)
  nginx checks ssl_client_verify
    ✓ SUCCESS → proxies to 127.0.0.1:3100 with X-SSL-Client-Verify: SUCCESS
    ✗ FAILED  → TLS handshake rejected, no HTTP traffic reaches the API

In development mode (NODE_ENV=development), the mTLS check is bypassed. A warning is logged once on startup.

Rejection response (403):

json
{
  "error": "mTLS certificate required",
  "details": {
    "hint": "Access to the Lamaste panel requires a valid client certificate."
  }
}

No Session Tokens (with One Exception) ​

The client certificate is the primary authentication mechanism — there are no login endpoints and no bearer tokens. If the certificate is valid, every request is authorized. This is the same model used by LXD.

The one exception is the optional two-factor authentication (2FA) feature. When 2FA is enabled, admin requests must also carry a valid lamaste_2fa_session cookie. This cookie is issued by POST /api/settings/2fa/verify after the admin presents a correct TOTP code, and it is HMAC-SHA256 signed, HttpOnly, Secure, SameSite=Strict with a 12-hour absolute expiry and 2-hour inactivity timeout. If a request arrives with a valid mTLS certificate but without a valid 2FA session cookie, the API returns:

json
{
  "error": "2fa_required"
}

with HTTP 401. The 2FA status and verification endpoints are themselves exempt from the 2FA session requirement so that the admin can check status and submit a code without already having a session.

Content Type ​

All request and response bodies use application/json unless explicitly noted otherwise:

  • File downloads (certificates) return their native MIME type
  • File uploads use multipart/form-data
  • WebSocket connections use the standard WebSocket upgrade handshake

Requests with a JSON body must include the Content-Type: application/json header.

Error Format ​

Every error response follows a consistent contract:

json
{
  "error": "Human-readable error summary",
  "details": {}
}
FieldTypePresenceDescription
errorstringAlwaysA short, human-readable error message
detailsobjectOptionalAdditional structured information about the error

Validation Errors (400) ​

All request bodies are validated with Zod schemas at the route level. When validation fails, the error handler returns a 400 with the Zod issues:

json
{
  "error": "Validation failed",
  "details": {
    "issues": [
      {
        "path": ["subdomain"],
        "message": "Subdomain must be lowercase alphanumeric with optional hyphens, cannot start or end with a hyphen"
      },
      {
        "path": ["port"],
        "message": "Port must be at least 1024"
      }
    ]
  }
}

Operational Errors (4xx) ​

Business logic errors return appropriate HTTP status codes with descriptive messages:

json
{
  "error": "Cannot delete the last user"
}

Common status codes:

CodeMeaningExample
400Bad request / validation failedInvalid subdomain format
4012FA session requiredValid mTLS cert but no 2FA session cookie
403mTLS certificate missing or invalidNo client cert presented
404Resource not foundTunnel ID does not exist
409ConflictUsername already exists
410GoneOnboarding endpoint called after completion
503Service unavailableManagement endpoint called before onboarding

Internal Errors (500) ​

Unexpected errors return a generic message in production to avoid leaking internal details:

json
{
  "error": "Internal server error"
}

In development mode, the response includes a details object with the error message and stack trace.

Onboarding Guard ​

The API is split into two groups with mutual exclusion enforced by middleware:

Route GroupPrefixAvailable WhenOtherwise Returns
Onboarding/api/onboarding/*status != COMPLETED410 Gone
Management/api/* (except health, onboarding, invite, and enroll)status == COMPLETED503 Service Unavailable
Public/api/invite/*, /api/enrollAlways (no mTLS)N/A

The GET /api/onboarding/status endpoint is always accessible regardless of onboarding state. The GET /api/health endpoint is also always accessible — it is registered outside both guards. The /api/invite/* and /api/enroll routes are registered in a separate public context with no mTLS middleware and no onboarding guard.

410 response (onboarding complete):

json
{
  "error": "Onboarding already completed"
}

503 response (onboarding incomplete):

json
{
  "error": "Onboarding not complete",
  "onboardingStatus": "FRESH"
}

Onboarding States ​

The onboarding progresses through a linear state machine:

FRESH → DOMAIN_SET → DNS_READY → PROVISIONING → COMPLETED

Each state transition is triggered by a specific API call and validated server-side. You cannot skip states.

CORS Policy ​

Cross-Origin Resource Sharing is configured to accept requests from the panel UI origins:

  • Before domain setup: https://<droplet-ip>:9292
  • After domain setup: both https://<droplet-ip>:9292 and https://panel.<domain>

Requests from other origins are rejected by the CORS policy.

WebSocket Connections ​

Two endpoints use WebSocket for real-time streaming:

EndpointPurposeProtocol
WS /api/onboarding/provision/streamProvisioning progressJSON messages
WS /api/services/:name/logsLive service log tailingJSON messages

WebSocket connections follow the standard upgrade handshake over the same HTTPS connection. The wss:// protocol is used in production since all traffic goes through nginx with TLS.

WebSocket messages are always JSON objects. There is no binary framing.

Common Response Patterns ​

Success with Data ​

Most GET endpoints return the requested resource directly:

json
{
  "tunnels": [{ "id": "...", "subdomain": "app", "port": 8080 }]
}

Success with Confirmation ​

Mutating endpoints typically return an ok field:

json
{
  "ok": true,
  "tunnel": { "id": "...", "subdomain": "app", "port": 8080 }
}

Success with Warning ​

Some operations succeed but produce a non-fatal warning (for example, nginx reload failure after certificate renewal):

json
{
  "ok": true,
  "domain": "app.example.com",
  "newExpiry": "2026-06-11T00:00:00.000Z",
  "warning": "Certificate renewed but nginx reload failed"
}

File Upload Limits ​

The server accepts multipart file uploads up to 50 MB per file, used by the static sites file management endpoints.

Rate Limiting ​

There is no general application-level rate limiting. The mTLS requirement means only authenticated administrators can reach the API, and the expected number of concurrent users is one or two. If you are self-hosting and want rate limits, configure them in nginx.

There are two exceptions:

  • 2FA endpoints (/confirm, /verify, /disable) enforce per-IP rate limiting: 5 attempts per 2-minute window, with a 5-minute ban once the limit is exceeded. This protects against brute-force TOTP guessing even though the caller already holds a valid mTLS certificate.
  • Ticket requests (POST /api/tickets) enforce per-agent rate limiting: 10 tickets per minute per agent. This protects against resource exhaustion from automated ticket generation.

Quick Reference ​

ItemValue
Base URL (IP)https://<ip>:9292/api
Base URL (domain)https://panel.<domain>/api
AuthenticationmTLS client certificate
Content-Typeapplication/json (default)
ValidationZod schemas at route level
Error format{ "error": "...", "details": {...} }
WebSocket protocolwss:// with JSON messages
Max upload size50 MB per file
Internal listen address127.0.0.1:3100

Endpoint Summary ​

MethodPathGroupDescription
GET/api/healthAlwaysHealth check
GET/api/onboarding/statusAlwaysOnboarding state
POST/api/onboarding/domainOnboardingSet domain and email
POST/api/onboarding/verify-dnsOnboardingVerify DNS records
POST/api/onboarding/provisionOnboardingStart provisioning
WS/api/onboarding/provision/streamOnboardingProvisioning progress
GET/api/invite/:tokenPublicGet invitation details
POST/api/invite/:token/acceptPublicAccept invitation
POST/api/enrollPublicEnroll agent with token (hardware-bound)
GET/api/system/statsManagementSystem statistics
POST/api/system/updateManagementTrigger background server update
POST/api/agents/plugins/reportManagementReport agent plugin capabilities
GET/api/agents/me/chisel-credentialManagementThe calling agent's Chisel credential
POST/api/agents/me/chisel-credential/rotateManagementRotate own Chisel credential (agent)
GET/api/agents/me/registry-credentialManagementOne-hour registry token for the agent
GET/api/agents/me/relay-keysManagementRelay assertion keys, caller's relay label
GET/api/agents/me/plugin-config/:routeManagementHosting agent: its plugin's port, secret
POST/api/agents/me/plugin-config/:route/ackManagementHosting agent holds the secret issued
POST/api/agents/me/jobs/:id/resultManagementReport an agent job's result (agent)
PUT/api/agents/me/host-factsManagementReport the agent's host facts (agent)
GET/api/agent-panel/authz/:labelManagementAgent-panel vhost auth_request (nginx)
POST/api/agents/:label/chisel-credential/rotateManagementRotate an agent's Chisel credential
GET/api/tunnels/agent-configManagementOne agent's tunnel configuration
GET/api/tunnelsManagementList tunnels
POST/api/tunnelsManagementCreate tunnel
PATCH/api/tunnels/:idManagementMove, access mode, body limit, enable
DELETE/api/tunnels/:idManagementDelete tunnel
GET/api/sitesManagementList static sites
POST/api/sitesManagementCreate static site
DELETE/api/sites/:idManagementDelete static site
PATCH/api/sites/:idManagementUpdate site settings
POST/api/sites/:id/verify-dnsManagementVerify site DNS
GET/api/sites/:id/filesManagementList site files
POST/api/sites/:id/filesManagementUpload site files
DELETE/api/sites/:id/filesManagementDelete site file
GET/api/invitationsManagementList invitations
POST/api/invitationsManagementCreate invitation
DELETE/api/invitations/:idManagementRevoke invitation
GET/api/usersManagementList users
POST/api/usersManagementCreate user
PUT/api/users/:usernameManagementUpdate user
DELETE/api/users/:usernameManagementDelete user
POST/api/users/:username/reset-totpManagementReset TOTP secret
GET/api/certsManagementList certificates
GET/api/certs/auto-renew-statusManagementAuto-renew timer status
POST/api/certs/:domain/renewManagementForce-renew certificate
POST/api/certs/mtls/rotateManagementRotate mTLS cert
GET/api/certs/mtls/downloadManagementDownload client.p12
POST/api/certs/agentManagementGenerate agent certificate
GET/api/certs/agentManagementList agent certificates
GET/api/certs/agent/:label/downloadManagementDownload agent .p12
PATCH/api/certs/agent/:label/capabilitiesManagementUpdate agent capabilities
PATCH/api/certs/agent/:label/allowed-sitesManagementUpdate agent site access
DELETE/api/certs/agent/:labelManagementRevoke agent certificate
POST/api/certs/agent/enrollManagementGenerate enrollment token
DELETE/api/certs/agent/enroll/:labelManagementRevoke unused enrollment token
POST/api/certs/agent/upgrade-certManagementUpgrade agent cert to hardware-bound
POST/api/certs/admin/upgrade-to-hardware-boundManagementUpgrade admin to hardware-bound
GET/api/certs/admin/auth-modeManagementGet admin auth mode
GET/api/settings/2faManagementGet 2FA status
POST/api/settings/2fa/setupManagementGenerate TOTP secret
POST/api/settings/2fa/confirmManagementConfirm initial code, enable 2FA
POST/api/settings/2fa/verifyManagementVerify code, issue session cookie
POST/api/settings/2fa/disableManagementDisable 2FA
GET/api/servicesManagementList service statuses
POST/api/services/:name/:actionManagementControl a service (start/stop/restart)
WS/api/services/:name/logsManagementStream service logs
GET/api/pluginsManagementList installed plugins
GET/api/plugins/:nameManagementGet plugin details
POST/api/plugins/installManagementInstall a plugin
POST/api/plugins/:name/enableManagementEnable a plugin
POST/api/plugins/:name/disableManagementDisable a plugin
DELETE/api/plugins/:nameManagementUninstall a plugin
GET/api/plugins/:name/oidc-clientsManagementA plugin's OIDC clients (no secrets)
POST/api/plugins/:name/oidc-clients/:clientId/rotate-secretManagementNew secret for a confidential client
GET/api/plugins/:name/hostingManagementWhich agent hosts the plugin, its health
POST/api/plugins/:name/hostingManagementHost the plugin on an agent (operation)
DELETE/api/plugins/:name/hostingManagementRemove the hosting (operation)
POST/api/plugins/:name/hosting/rotate-secretManagementNew installation secret
GET/api/plugins/:name/hosting/eligibilityManagementWhich agents can host the plugin, and why
GET/api/plugins/:name/retained-dataManagementData agents keep of its agent half
DELETE/api/plugins/:name/retained-dataManagementPurge it on an agent, or forget the record
GET/api/plugins/:name/hooksManagementLifecycle hooks, what needs approval
POST/api/plugins/:name/hooks/approveManagementApprove with the approval digest
PUT/api/plugins/:name/hooks/auto-approveManagementAuto-approve what later versions bring
POST/api/plugins/:name/updateManagementUpdate an agent-hosted plugin in place
DELETE/api/plugins/:name/updateManagementDiscard an update waiting for approval
GET/api/plugins/:name/operationsManagementThe plugin's recent operations
GET/api/plugins/:name/operations/:idManagementOne operation and its steps
ANY/api/<route>, /api/<route>/*ManagementForwarded to the agent half's /admin/*
GET/api/auditManagementAudit log
GET/api/notificationsManagementPanel notifications
POST/api/notifications/readManagementMark notifications read
GET/api/registry-credentialManagementRegistry credential summary
PUT/api/registry-credentialManagementSet the registry credential (verified)
DELETE/api/registry-credentialManagementRemove the registry credential
GET/api/plugins/push-install/configManagementGet push install configuration
PATCH/api/plugins/push-install/configManagementUpdate push install configuration
GET/api/plugins/push-install/policiesManagementList push install policies
POST/api/plugins/push-install/policiesManagementCreate push install policy
PATCH/api/plugins/push-install/policies/:idManagementUpdate push install policy
DELETE/api/plugins/push-install/policies/:idManagementDelete push install policy
POST/api/plugins/push-install/enable/:labelManagementEnable push install for agent
DELETE/api/plugins/push-install/enable/:labelManagementDisable push install for agent
GET/api/plugins/push-install/agent-statusManagementAgent checks own push install status
POST/api/plugins/push-install/:labelManagementRecord a push-install command (legacy)
GET/api/plugins/push-install/sessionsManagementList push install audit log
POST/api/tickets/scopesManagementRegister ticket scope
GET/api/tickets/scopesManagementList scopes, instances, assignments
DELETE/api/tickets/scopes/:nameManagementDelete ticket scope
POST/api/tickets/instancesManagementRegister instance
DELETE/api/tickets/instances/:instanceIdManagementDeregister instance
POST/api/tickets/instances/:instanceId/heartbeatManagementInstance heartbeat
POST/api/tickets/assignmentsManagementAssign agent to instance
DELETE/api/tickets/assignments/:agentLabel/:instanceScopeManagementRemove assignment
GET/api/tickets/assignmentsManagementList assignments
POST/api/ticketsManagementRequest ticket
GET/api/tickets/inboxManagementCheck ticket inbox
POST/api/tickets/validateManagementValidate and consume ticket
GET/api/ticketsManagementList all tickets (admin)
DELETE/api/tickets/:ticketIdManagementRevoke ticket
POST/api/tickets/sessionsManagementCreate session from ticket
POST/api/tickets/sessions/:sessionId/heartbeatManagementSession heartbeat
PATCH/api/tickets/sessions/:sessionIdManagementUpdate session status
DELETE/api/tickets/sessions/:sessionIdManagementKill session
GET/api/tickets/sessionsManagementList sessions
POST/api/storage/serversManagementRegister a storage server
GET/api/storage/serversManagementList registered storage servers
DELETE/api/storage/servers/:idManagementRemove a storage server
POST/api/storage/bindingsManagementBind storage to plugin
GET/api/storage/bindingsManagementList all bindings
GET/api/storage/bindings/:pluginNameManagementGet binding for plugin
DELETE/api/storage/bindings/:pluginNameManagementUnbind storage from plugin

POST /api/plugins/push-install/:label checks the push-install gates and records the command in the push-install log; nothing delivers it to the agent. What Lamaste does push to an agent — installing, updating and removing an agent half — runs as plugin operations under the same gates (Agent-Hosted Plugins). The agent-hosted plugin endpoints are described in Agent-Hosted Plugins, Lifecycle Hooks and Audit Log and Notifications.

Agent Capabilities ​

Agent certificates use capability-based access control. Base capabilities are always available; plugins can declare additional capabilities in their manifest.

Base capabilities:

CapabilityDescription
tunnels:readList own tunnels, fetch own Chisel config (always-on, cannot be removed)
tunnels:writeCreate, update and delete tunnels the agent carries
services:readView service status
services:writeStart, stop, and restart services
system:readView system stats (CPU, RAM, disk)
sites:readList sites and browse files
sites:writeUpload and delete files on assigned sites
panel:exposeExpose agent management panel at agent-<label>.<domain>
identity:readParse Authelia identity headers on plugin routes
identity:queryQuery panel for Authelia user metadata

Plugin-declared capabilities: Plugins can declare additional capabilities in their lamaste-plugin.json manifest using either a flat array ("capabilities": ["scope:action"]) or a nested object ("capabilities": { "agent": ["scope:action"] }). Both formats are normalized to a flat array internally. These are merged with base capabilities and available for assignment to agent certificates. Capabilities are validated dynamically via getValidCapabilities().

Ticket scope capabilities: Ticket scopes (registered via POST /api/tickets/scopes) declare capabilities that are dynamically merged with base and plugin capabilities. For example, a scope named shell declaring scopes: [{ name: "shell:connect" }] makes shell:connect available for assignment to agent certificates. See the Tickets API for details.

Released under the PolyForm Noncommercial License 1.0.0