API Reference
The Meshploy REST API. Built with Go, Chi router, and Huma for automatic OpenAPI 3.1 spec generation.
| Language | Go 1.25+ |
| Router | Chi |
| OpenAPI | Huma v2 (OpenAPI 3.1, automatic schema + docs) |
| Database | GORM + PostgreSQL (via packages/db) |
| Auth | JWT (HS256, 24h expiry) + TOTP 2FA; magt- agent tokens for automation |
Directory structure
Section titled “Directory structure”apps/api/ is a thin entrypoint — its main.go just calls server.Main(). The actual API core (config, HTTP handlers, business logic) lives in packages/server, which apps/api imports via a go.work replace directive. This split lets the API core be reused (e.g. embedded as the in-gateway /mcp server) without depending on the apps/api binary.
apps/api/├── main.go # Calls server.Main() — no business logic here├── Dockerfile├── go.mod└── tools/
packages/server/├── server.go # HTTP server setup, route registration├── entrypoint.go # Main() — config load, DB connect, server start├── config/ # Typed env config — Load() from environment├── middleware/ # Auth() resolves a JWT or magt- agent token; RequireAuth() 401s anything off the public allowlist├── handler/ # HTTP layer only — thin, delegates to service│ ├── handler.go # Handler struct, Register(), RegisterRaw()│ ├── access.go # checkAccess(), checkOrgAdminAccess(), checkOrgMemberAccess() helpers│ ├── auth.go # /auth/register, /auth/login, /me, TOTP, 2FA│ ├── agent.go # Agent principals: create, list, token mint/rotate/revoke, delete│ ├── mcp.go # Remote MCP (Streamable HTTP) at /mcp — agent-token authed, permission-scoped│ ├── org.go # Org CRUD, member management, invitations│ ├── project.go # Project CRUD│ ├── environment.go # Environment levels: order, create, rename (hostnames re-derived)│ ├── promotion.go # Groups, lineages across levels, promotion, board, own databases│ ├── environment.go # Environment levels: list, create above/below, rename│ ├── promotion.go # Promotion groups, promote, board, own database, copy down, bring down│ ├── permission.go # Per-resource permission grants│ ├── node.go # Node CRUD, self-register, self-deregister, metrics│ ├── workload.go # Service CRUD, env vars, build/db config, pods│ ├── stack.go # Stack CRUD, apply, sync│ ├── job.go # Job CRUD, trigger, run history│ ├── volume.go # Volume CRUD, mounts, backup config│ ├── route.go # Route CRUD, targets, hostname verify│ ├── domain.go # Domain CRUD + DNS verification│ ├── deployment.go # Deployment list, trigger, rollback, SSE logs│ ├── backup.go # Service backups + system backup│ ├── notification.go # Notification channels│ ├── email_config.go # Org SMTP config│ ├── variable_group.go # Variable group CRUD + service attach/detach│ ├── git_integration.go # Git provider integrations + OAuth callbacks│ ├── registry.go # Registry integration CRUD│ ├── storage.go # Storage integration CRUD│ ├── terminal.go # WebSocket: node terminal + pod terminal│ ├── webhook.go # Inbound webhooks (GitHub push, deploy token)│ ├── webhook_git.go # Inbound push webhooks for GitLab, Gitea/Forgejo, Bitbucket│ ├── template.go # One-click template catalog│ ├── config_file.go # Config file CRUD + attach/detach│ ├── entitlement.go # Licence status + activation│ ├── brand.go # An org's brand: by host (public, the sign-in page) or by org│ ├── ondemand_tls.go # Caddy ask endpoint for on-demand TLS│ ├── extension.go # Extension point: extra routes (EE)│ ├── system.go # Version, exposure notice, install/uninstall scripts│ └── health.go # GET /health├── service/ # Business logic — one file per domain│ ├── service.go # Services aggregate struct + New()│ ├── auth.go # Register (user + default org in tx), Login, TOTP│ ├── agent.go # Agent principals + agent_tokens; ResolveToken() for the auth middleware│ ├── org.go # Org CRUD, member management, invitations│ ├── project.go # Project CRUD│ ├── permission.go # Resource permission grants│ ├── node.go # Node CRUD, registration/provisioning tokens, monitor│ ├── node_exporter.go # Live metrics scraping from node_exporter│ ├── workload.go # Service CRUD, env vars, build/db config│ ├── stack.go # Stack parse, apply, sync│ ├── job.go # Job CRUD, trigger, reconciler goroutine│ ├── volume.go # Volume CRUD, mounts, K8s PVC lifecycle│ ├── route.go # Route + target CRUD│ ├── domain.go # Domain CRUD + DNS verification│ ├── deployment.go # Deployment trigger, rollback, K8s Job lifecycle│ ├── backup.go # Backup schedule, trigger, restore, retention│ ├── backup_executor.go # Backup/restore K8s Job execution│ ├── notification.go # Notification dispatch (Slack, Discord, email, webhook)│ ├── email_config.go # Org SMTP config│ ├── variable_group.go # Variable group CRUD + service attachment│ ├── git_integration.go # Git provider connections + OAuth flows│ ├── registry.go # Registry integration CRUD│ ├── storage.go # Storage integration CRUD│ ├── db_explorer.go # Live DB query + schema via K8s exec│ ├── system.go # Version info, install/uninstall script serving│ ├── template.go # Template catalog fetch/cache + deploy│ ├── config_file.go # Config files projected into workloads via Secrets│ ├── exposure.go # Host-firewall exposure notice + dismissed notices│ ├── entitlement.go # Licence verification + entitlements│ ├── brand.go # An org's brand; Meshploy's until an edition registers a source│ ├── extension.go # Extension point: per-org quotas (EE)│ ├── orphans.go # Cluster workloads that no service owns│ ├── workload_status.go # Reconciles stored service status with the cluster│ ├── volume_status.go # Reconciles stored volume status with its claim│ ├── wsticket.go # Single-use tickets for WebSocket auth│ └── headscale.go # Headscale API client (list, get, delete, rename nodes)├── k8s/ # Kubernetes client helpers├── templates/ # Built-in template assets└── version/ # Build/version metadataAPI routes
Section titled “API routes”Routes are under /api/v1 unless listed under Outside /api/v1. Most need Authorization: Bearer <token>, where the token is a user’s JWT (24h) or an agent’s magt- token. The server is fail-closed: a route that is not on its public allowlist returns 401 without a token.
This lists every route registered in packages/server/handler. Auth: ✓ needs a bearer token, public needs none, otherwise the credential the route checks instead.
The OpenAPI spec is at /openapi.json and Huma’s docs page at /docs. Both need the same Authorization header, so fetch the spec with a token:
curl -H "Authorization: Bearer <token>" https://api.<your-domain>/openapi.jsonAuth & identity
Section titled “Auth & identity”| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/login | public | Login and receive a JWT naming a console session (sid), good while that session is live |
| POST | /auth/logout | ✓ | End the console session making the request |
| POST | /auth/recovery | public | Complete login with a one-time recovery code |
| POST | /auth/register | public | Register a new user |
| GET | /auth/status | public | Check whether registration is open (no users exist yet) |
| POST | /auth/totp | public | Complete login with TOTP code |
| GET | /me | ✓ | Get current user |
| PATCH | /me/password | ✓ | Change current user password |
| POST | /me/recovery-codes/regenerate | ✓ | Regenerate 2FA recovery codes (requires current TOTP code) |
| DELETE | /me/totp | ✓ | Disable 2FA (requires current TOTP code) |
| POST | /me/totp/enable | ✓ | Verify TOTP code and enable 2FA |
| POST | /me/totp/setup | ✓ | Generate a new TOTP secret (not yet enabled) |
| GET | /me/cli-sessions | ✓ | Your logged-in CLIs, the most recently used first; current marks the one asking |
| DELETE | /me/cli-sessions/{id} | ✓ | Log one of your CLIs out |
| GET | /me/sessions | ✓ | Where you are signed in to the console: browser, address, last seen; current marks the one asking |
| DELETE | /me/sessions/{id} | ✓ | Sign one of your console sessions out |
| DELETE | /me/sessions | ✓ | Sign out everywhere else: every console session but the one asking |
CLI login
Section titled “CLI login”A CLI logs in through a browser, as OAuth’s device flow does: it starts a login and polls; a person signed in to the console sees the same code the terminal shows and approves; the CLI collects an mcli- token that acts as them, once. The token lapses after 90 days unused.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /cli/logins | public | Start a login (host, optional door: the console to approve at). Returns the device code (the CLI’s secret), the user code, the approval link, the API address to save, the polling interval and expiry |
| POST | /cli/logins/token | public | Poll with the device code: pending, denied, expired, approved with the token once, then collected |
| GET | /cli/logins/{code} | ✓ | A login waiting for approval, by its user code (any case, dash optional) |
| POST | /cli/logins/{code}/approve | ✓ | Approve it. A browser session only: an agent or CLI token is refused |
| POST | /cli/logins/{code}/deny | ✓ | Deny it. A browser session only |
| DELETE | /cli/session | ✓ | Log out the CLI making the request (an mcli- token only) |
Sign-in links
Section titled “Sign-in links”An installation that sets SIGN_IN_LINK_KEY lets whoever holds the key make a one-time link that signs a person in to the console, skipping their password and second factor: an operator opening a console for someone, as Cloud’s Open button does for a workspace’s owner. The key is as strong as the database, so it lives beside the database’s password. The link is the console’s /link, its mlnk- token in the fragment so no server on the way logs it; it is spent once, within five minutes. Unset, making one answers 404.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /system/sign-in-links | key | Make a link for the person signing in with email (an agent has none), landing on next, a path on the console. Takes the key in X-Sign-In-Link-Key. Returns the url and its expiry |
| POST | /auth/sign-in-link | public | Spend a link’s token, starting a console session: returns the session’s token |
OAuth for MCP clients
Section titled “OAuth for MCP clients”Meshploy is the OAuth authorization server for its own /mcp, so a client that can only connect by signing in (Claude on the web and the desktop) can connect. /mcp answers a request without a working credential with 401 and WWW-Authenticate: Bearer resource_metadata=...; the client reads the metadata, registers itself, sends a person to the console’s /oauth/authorize page to approve, and trades the code (PKCE S256) for an moat- access token (an hour) and an mort- refresh token (rotated on every use; a copied one presented again ends the connection). A connection acts as the person who approved it or, chosen by an owner or admin, as one of the organisation’s agents. Its access token opens /mcp and nothing else: the API accepts it only on /mcp’s own calls back to it.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /.well-known/oauth-protected-resource | public | RFC 9728 metadata for /mcp (also at .../oauth-protected-resource/mcp) |
| GET | /.well-known/oauth-authorization-server | public | RFC 8414 metadata: endpoints, S256, grant types |
| POST | /oauth/register | public | RFC 7591 dynamic client registration. Redirect URIs must be https, or http to loopback |
| POST | /oauth/token | public | authorization_code with code_verifier, or refresh_token (form-encoded) |
| GET | /oauth/authorize | ✓ | What a client asking to connect is, for the approval page. A browser session only |
| POST | /oauth/authorize | ✓ | Approve (in org_id, as yourself or as agent_id) or deny; answers where to send the browser. A browser session only |
| GET | /orgs/{orgId}/oauth/connections | ✓ | Your connections; ?user_id=, ?agent_id= or ?all=true for an owner or admin. Disconnected ones stay listed |
| GET | /orgs/{orgId}/cli-sessions | ✓ admin | CLIs signed in as the organisation’s members (?user_id= for one); elsewhere marks one whose person also belongs to another organisation |
| DELETE | /orgs/{orgId}/cli-sessions/{id} | ✓ admin | Sign out a member’s CLI. Refused (409) while they also belong to another organisation, where it acts as them too |
| GET | /orgs/{orgId}/console-sessions | ✓ admin | Console sign-ins of the organisation’s members (?user_id= for one), with elsewhere as for CLIs |
| DELETE | /orgs/{orgId}/console-sessions/{id} | ✓ admin | Sign a member out of the console. Refused (409) while they also belong to another organisation |
| GET | /orgs/{orgId}/session-counts | ✓ admin | Connected sessions per member: assistants connected here, and signed-in CLIs |
| DELETE | /orgs/{orgId}/oauth/connections/{id} | ✓ | Disconnect: your own, or any for an owner or admin |
Orgs, members & invitations
Section titled “Orgs, members & invitations”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /invitations/{token} | invite token | Get invitation info by token (public); account_exists says the address already has an account |
| POST | /invitations/{token}/accept | invite token | Accept an invitation and create an account (public). 409 when the address already has one |
| POST | /invitations/{token}/join | ✓ | Accept an invitation with the account you are signed in as; refused (403) for another address |
| GET | /orgs | ✓ | List organizations for the authenticated user |
| POST | /orgs | ✓ | Create an organization, only on a server that has none: Community runs one per server (409 otherwise) |
| GET | /orgs/{orgId} | ✓ | Get an organization |
| PATCH | /orgs/{orgId} | ✓ | Update an organization |
| PUT | /orgs/{orgId}/network-isolation | ✓ admin | {"enabled": true} closes each of the organization’s projects to the rest of the cluster: its pods take traffic only from the project’s own pods and levels, from projects that use one of its services’ published variables, and from the nodes (the proxy, published ports). Outgoing traffic is unchanged. Off (the default) removes every policy it made. Enforced by k3s’s own NetworkPolicy controller |
| GET | /orgs/{orgId}/sandbox | ✓ | The nodes prepared to run sandboxed pods (labelled meshploy.com/sandbox=true by meshploy install sandbox), the sandboxed projects and levels the caller can see, with required where a rule makes it so, and new_projects: whether a project created now starts sandboxed (new_projects_required and new_projects_why when a rule makes it so) |
| PUT | /orgs/{orgId}/sandbox | ✓ admin | {"new_projects": true} starts the organization’s new projects sandboxed when their creator does not say; existing projects keep their own. Refused (409) while no node can run sandboxed pods |
| DELETE | /orgs/{orgId} | ✓ | Delete an organization (owner only) |
| GET | /orgs/{orgId}/invitations | ✓ | List pending invitations |
| POST | /orgs/{orgId}/invitations | ✓ | Create an invite link for a new member |
| GET | /orgs/{orgId}/members | ✓ | List organization members |
| POST | /orgs/{orgId}/members | ✓ | Add a member to an organization |
| PATCH | /orgs/{orgId}/members/{userId} | ✓ | Update a member’s role |
| DELETE | /orgs/{orgId}/members/{userId} | ✓ | Remove a member from an organization |
Permissions
Section titled “Permissions”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/jobs/{resourceId}/permissions | ✓ | List permissions on a job |
| GET | /orgs/{orgId}/members/{userId}/permissions | ✓ | List all permission grants for a member |
| POST | /orgs/{orgId}/members/{userId}/permissions | ✓ | Grant a permission to a member |
| DELETE | /orgs/{orgId}/members/{userId}/permissions | ✓ | Revoke a permission from a member |
| GET | /orgs/{orgId}/projects/{resourceId}/permissions | ✓ | List permissions on a project |
| GET | /orgs/{orgId}/services/{resourceId}/permissions | ✓ | List permissions on a service |
| GET | /orgs/{orgId}/stacks/{resourceId}/permissions | ✓ | List permissions on a stack |
Agents
Section titled “Agents”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/agents | ✓ | List agent principals in an org |
| POST | /orgs/{orgId}/agents | ✓ | Create an agent principal and mint its first token |
| DELETE | /orgs/{orgId}/agents/{agentId} | ✓ | Delete an agent principal and all its tokens/grants |
| POST | /orgs/{orgId}/agents/{agentId}/tokens | ✓ | Mint an additional token for an agent (rotation) |
| DELETE | /orgs/{orgId}/agents/{agentId}/tokens/{tokenId} | ✓ | Revoke an agent token |
Licence & entitlements
Section titled “Licence & entitlements”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /entitlements | ✓ | Current license entitlements for this install |
| POST | /entitlements/license | ✓ | Install a license token |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /brand | public | The brand of the organisation the request’s host belongs to, for the sign-in page; Meshploy’s on any other host |
| GET | /orgs/{orgId}/brand | ✓ | An organisation’s brand (members) |
Projects
Section titled “Projects”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/projects | ✓ | List projects in an organization |
| POST | /orgs/{orgId}/projects | ✓ | Create a project; sandboxed (optional) runs its apps and jobs on gVisor, and left out follows the organization’s new_projects. Refused (409) asking for no sandbox where a rule requires one, or for one no node can run |
| GET | /orgs/{orgId}/projects/{projectId} | ✓ | Get a project |
| PATCH | /orgs/{orgId}/projects/{projectId} | ✓ | Update a project |
| PUT | /orgs/{orgId}/projects/{projectId}/network-open | ✓ admin | {"open": true} exempts the project, or the level, from the organization’s network isolation: any pod in the cluster may reach it again |
| GET | /orgs/{orgId}/projects/{projectId}/sandbox | ✓ | Whether the project’s, or the level’s, apps and jobs run on gVisor: on, and required with why when a rule makes it so |
| PUT | /orgs/{orgId}/projects/{projectId}/sandbox | ✓ admin | {"on": true} runs its apps and jobs on gVisor instead of the host’s kernel and moves the running ones at once (services roll); its managed databases stay on the host’s kernel. Refused (409) while no node can run sandboxed pods, while an app or job is pinned to one that cannot, or, switching off, while it is required. Builds are never sandboxed |
| DELETE | /orgs/{orgId}/projects/{projectId} | ✓ | Delete a project |
| DELETE | /orgs/{orgId}/projects/{projectId}/build-cache | ✓ | Clear a project’s build cache: its claim, every builder’s folder in it |
Environments
Section titled “Environments”A project is its own production level; each level below it is a project of its own (parent_project_id), with its own namespace, so every route above works inside a level. {projectId} may be any level.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/projects/{projectId}/environments | ✓ | A project’s levels, production first |
| POST | /orgs/{orgId}/projects/{projectId}/environments | ✓ | Add a level above or below an existing one; starts empty |
| PATCH | /orgs/{orgId}/projects/{projectId}/environment | ✓ | Rename a level: its hostnames are re-derived, its namespace stays |
| GET | /orgs/{orgId}/projects/{projectId}/board | ✓ | Levels, groups, and what each service runs at each level |
| GET | /orgs/{orgId}/projects/{projectId}/promotion-groups | ✓ | List promotion groups |
| POST | /orgs/{orgId}/projects/{projectId}/promotion-groups | ✓ | Create a group; its services are copied into the level it enters at, the only one that builds |
| PATCH | /orgs/{orgId}/projects/{projectId}/promotion-groups/{groupId} | ✓ | Rename a group |
| DELETE | /orgs/{orgId}/projects/{projectId}/promotion-groups/{groupId} | ✓ | Delete a group; its services stay where they are |
| POST | /orgs/{orgId}/projects/{projectId}/promotion-groups/{groupId}/services | ✓ | Add services, merging away any single-service group they were in |
| DELETE | /orgs/{orgId}/projects/{projectId}/promotion-groups/{groupId}/services/{lineageId} | ✓ | Take a service out of a group; its copies keep running |
| POST | /orgs/{orgId}/projects/{projectId}/promotion-groups/{groupId}/promote | ✓ | Promote from this level to the next on the group’s path, as the same images; needs update on the target level. ?overwrite=true also replaces a newer image built there (a hotfix) |
| GET | /orgs/{orgId}/projects/{projectId}/promotion-groups/{groupId}/preflight | ✓ | Per service: new to the target, own variables it brings, and what would block it (variables from below) |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/copy-to-level | ✓ | Copy a service into a lower level as a group of its own |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/bring-down | ✓ | Run a service’s current image in a lower level |
| DELETE | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/level-copy | ✓ | Delete this level’s copy of a service and its routes; the level uses the one above, and the service leaves a group that builds here |
| POST | /orgs/{orgId}/projects/{projectId}/own-database | ✓ | Give this level its own copy of a database it uses from above, empty or clone (restores the source’s latest backup) |
Nodes & cluster
Section titled “Nodes & cluster”| Method | Path | Auth | Description |
|---|---|---|---|
| DELETE | /nodes/self-deregister | node token | Self-deregister a node using its registration token and node ID |
| GET | /edge/snapshot | edge node secret | The edge’s snapshot for a proxy with no database: routes, TCP ports, mesh gate state, page brands, stopped services. Only the node holding the cluster agent, with its mnode- secret and X-Meshploy-Node, on a CLUSTER_AGENT API; 401 otherwise. With routes and mesh (the versions held) it waits up to wait seconds (25 at most) for them to move, and answers 204 when they did not |
| POST | /edge/tcp-status | edge node secret | What became of TCP routes on the edge: {"routes":[{"id","status","last_error"}]}, status open, paused or failed |
| POST | /nodes/self-register | registration token | Self-register a node using a registration token. The node holding the cluster’s server on a CLUSTER_AGENT API also sends cluster_agent (certificate, token, port), only with a provisioning token minted for it: 422 for any other token, 409 when an agent is already registered. The same machine registering again after a revocation keeps its node |
| GET | /orgs/{orgId}/cluster/headscale-preauth-key | ✓ | Get the most recent active Headscale preauth key |
| POST | /orgs/{orgId}/cluster/headscale-preauth-key | ✓ | Generate a new Headscale preauth key for joining the WireGuard mesh |
| GET | /orgs/{orgId}/cluster/join-token | ✓ | Get a token for joining the cluster: the server’s own on a gateway; through a cluster agent, one it mints that lasts an hour (expires_at) |
| GET | /orgs/{orgId}/projects/{projectId}/map | ✓ | Everything a level’s map is drawn from, in one read: services, stacks, routes, volumes, troubles, build hints, and which services read another’s published variables |
| GET | /orgs/{orgId}/placement | ✓ | Org admins. Where every service runs: each node’s allocatable room and what its pods request, each service’s pods, requests, pinned node and the nodes its volumes’ data is bound to |
| GET | /orgs/{orgId}/placement/nodes/{node}/what-if | ✓ | Org admins. What would happen if the cluster node went down: per service, keeps running, moves (and where), or stays down and why. Nothing is stopped |
| GET | /orgs/{orgId}/cluster/mesh-health | ✓ | Report whether the control plane can reach Headscale |
| GET | /orgs/{orgId}/cluster/orphans | ✓ | List cluster workloads that no service owns |
| DELETE | /orgs/{orgId}/cluster/orphans/{namespace}/{name} | ✓ | Remove a cluster workload that no service owns |
| POST | /orgs/{orgId}/node-provisioning-tokens | ✓ | Create a single-use node provisioning token. cluster_agent: true mints the one for the node holding the cluster’s server, with its cluster agent; only on a CLUSTER_AGENT API (422 otherwise), and only while none is registered (409) |
| GET | /orgs/{orgId}/node-registration-token | ✓ | Get the node registration token |
| POST | /orgs/{orgId}/node-registration-token | ✓ | Generate (or rotate) the node registration token |
| GET | /orgs/{orgId}/nodes | ✓ | List nodes in an organization |
| POST | /orgs/{orgId}/nodes | ✓ | Register a new node |
| GET | /orgs/{orgId}/nodes/{nodeId} | ✓ | Get a node |
| PATCH | /orgs/{orgId}/nodes/{nodeId} | ✓ | Update a node |
| DELETE | /orgs/{orgId}/nodes/{nodeId} | ✓ | Remove a node from Headscale, the cluster and Meshploy, in that order. 200 {"removed": true} when it is gone; 202 {"removed": false, "error": ...} while Headscale has not confirmed the peer is removed, which is retried every minute. Calling it again retries at once |
| POST | /orgs/{orgId}/nodes/{nodeId}/cancel-removal | ✓ | Stop a node removal that is waiting for Headscale; 409 if none is |
| DELETE | /orgs/{orgId}/nodes/{nodeId}/cluster-agent | ✓ admin | Revoke the node’s cluster agent: the cluster is no longer reached through it, open requests to it are cut, and a new agent may register. 204; 404 if the node carries none |
| GET | /orgs/{orgId}/nodes/{nodeId}/metrics | ✓ | Get live resource metrics for a node (requires node_exporter) |
| GET | /orgs/{orgId}/nodes/{nodeId}/containers | ✓ | List containers running on a node that Meshploy does not manage |
| GET | /orgs/{orgId}/discovery | ✓ | List endpoints and containers on this org’s nodes that Meshploy does not route |
| POST | /orgs/{orgId}/discovery/ignores | ✓ | Record that a discovered endpoint is known and correct as it is. Not called by the console: Discovery is a view of a machine, not a list to clear. Kept for a future “new endpoint appeared” notification, which needs a baseline |
| DELETE | /orgs/{orgId}/discovery/ignores/{ignoreId} | ✓ | Remove that record |
Services
Section titled “Services”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/projects/{projectId}/services | ✓ | List services in a project |
| POST | /orgs/{orgId}/projects/{projectId}/services | ✓ | Create a service |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId} | ✓ | Get a service |
| PATCH | /orgs/{orgId}/projects/{projectId}/services/{serviceId} | ✓ | Update a service |
| DELETE | /orgs/{orgId}/projects/{projectId}/services/{serviceId} | ✓ | Delete a service |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/build-config | ✓ | Get build config for a service |
| PATCH | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/build-config | ✓ | Create or update build config for a service |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/build-config/deploy-token | ✓ | Regenerate the per-service webhook deploy token |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/build-config/env-vars | ✓ | Get build-time environment variables for a service |
| PUT | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/build-config/env-vars | ✓ | Set build-time environment variables for a service |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/database-config | ✓ | Get database config for a database service |
| PATCH | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/database-config | ✓ | Change a database’s network access |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/db/query | ✓ | Execute a database query |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/db/schema | ✓ | Introspect database schema |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/env-vars | ✓ | Get decrypted env vars for a service |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/pods | ✓ | List running pods for a service; runtime_class is gvisor for a sandboxed pod |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/pods/metrics | ✓ | Live CPU and memory usage per pod (requires metrics-server) |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/reset | ✓ | Wipe and re-provision a database (destructive) |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/start | ✓ | Start a service |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/stop | ✓ | Stop a service |
Deployments & logs
Section titled “Deployments & logs”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments | ✓ | List deployments for a service |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments | ✓ | Trigger a new deployment |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments/{deploymentId} | ✓ | Get a deployment |
| DELETE | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments/{deploymentId} | ✓ | Cancel an active deployment |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments/{deploymentId}/logs/stream | ✓ | Stream a deployment’s build log (SSE) |
| DELETE | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments/{deploymentId}/record | ✓ | Delete a deployment record |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments/{deploymentId}/rollback | ✓ | Roll back to a previous successful deployment; 409 when its image was removed from the registry |
| GET | /orgs/{orgId}/projects/{projectId}/image-retention | ✓ | A level’s services made on their own that keep every image they build |
| POST | /orgs/{orgId}/projects/{projectId}/image-retention | ✓ | Make them keep their last 3 images, removing the rest now |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/redeploy | ✓ | Run the current image again, without building; keeps where the image came from |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/source | ✓ | Upload a folder, a tar.gz as the body, as the service’s source (?name= its folder name, ?deploy=true to deploy it). Needs the deploy permission; up to 100 MB; stored in the service’s registry, only the latest kept |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/dependents | ✓ | Services and jobs, at any level, whose variables come from this service today |
| GET | /orgs/{orgId}/projects/{projectId}/health | ✓ | Services in a project that are not staying up, and why: out of memory (with the limit), crashing (with the exit code), image pull, cannot start; and each service’s suggestions from its last build (hints), with their fixes |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/hints/{kind}/dismiss | ✓ | Set aside one kind of suggestion (start_command, memory, port) for a service, for everyone |
| GET | /orgs/{orgId}/activity | ✓ | Recent deployments and job runs across the projects the caller can see, with each deployment’s level and where its image came from |
| GET | /orgs/{orgId}/overview | ✓ | The workspace overview: what needs attention (failures, offline nodes, missing backups, waiting promotions, hotfixes; domains to admins), each count broken down across every level, and delivery over 14 days (runs per day; deploys a week, change failure rate, time to recover and staging-to-production time, all for production; a deploy sparkline per project) |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/logs | ✓ | Snapshot of a service’s container logs |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/logs/stream | ✓ | Stream a service’s container logs (SSE) |
Stacks
Section titled “Stacks”| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /orgs/{orgId}/projects/{projectId}/apply | ✓ | Upsert a stack from an inline compose manifest and reconcile it |
| GET | /orgs/{orgId}/projects/{projectId}/stacks | ✓ | List stacks for a project |
| POST | /orgs/{orgId}/projects/{projectId}/stacks | ✓ | Create a new stack |
| POST | /orgs/{orgId}/projects/{projectId}/stacks/meshploy-config | ✓ | Write out every x-meshploy setting an apply would default |
| GET | /orgs/{orgId}/projects/{projectId}/stacks/{stackId} | ✓ | Get a stack |
| PUT | /orgs/{orgId}/projects/{projectId}/stacks/{stackId} | ✓ | Update a stack’s spec and variables, and the images its built services keep (rollback_enabled, image_retention), set on them at once |
| DELETE | /orgs/{orgId}/projects/{projectId}/stacks/{stackId} | ✓ | Delete a stack |
| POST | /orgs/{orgId}/projects/{projectId}/stacks/{stackId}/apply | ✓ | Apply the stack spec - reconcile services. deploy: false writes the records and rolls nothing out; files carries what the spec names by path when the server cannot read it (a migration sending a git stack’s repository files from the old checkout) |
| GET | /orgs/{orgId}/projects/{projectId}/stacks/{stackId}/runs | ✓ | The stack’s Syncs and Applies, newest first, each with its rollout’s state |
| GET | /orgs/{orgId}/projects/{projectId}/stacks/{stackId}/runs/{runId} | ✓ | One Sync or Apply: its services by depends_on layer, each with its deployment, and what the apply said |
| POST | /orgs/{orgId}/projects/{projectId}/stacks/{stackId}/destroy | ✓ | Destroy the services this stack created, keeping the stack |
| GET | /orgs/{orgId}/projects/{projectId}/stacks/{stackId}/services | ✓ | List services belonging to a stack |
| POST | /orgs/{orgId}/projects/{projectId}/stacks/{stackId}/sync | ✓ | Fetch spec from git source and re-apply |
Templates
Section titled “Templates”| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /orgs/{orgId}/projects/{projectId}/templates/{templateId}/deploy | ✓ | Deploy a template into a project as a stack |
| GET | /templates | ✓ | List one-click templates |
| POST | /templates/refresh | ✓ | Re-read the template catalog from its source |
| GET | /templates/{templateId} | ✓ | Get a template (manifest + compose) |
| GET | /templates/{templateId}/icon | public | Template icon image |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/projects/{projectId}/jobs | ✓ | List jobs in a project |
| POST | /orgs/{orgId}/projects/{projectId}/jobs | ✓ | Create a job or cron job |
| GET | /orgs/{orgId}/projects/{projectId}/jobs/{jobId} | ✓ | Get a job |
| PATCH | /orgs/{orgId}/projects/{projectId}/jobs/{jobId} | ✓ | Update a job |
| DELETE | /orgs/{orgId}/projects/{projectId}/jobs/{jobId} | ✓ | Delete a job |
| GET | /orgs/{orgId}/projects/{projectId}/jobs/{jobId}/runs | ✓ | List run history for a job |
| DELETE | /orgs/{orgId}/projects/{projectId}/jobs/{jobId}/runs/{runId} | ✓ | Delete a job run record |
| POST | /orgs/{orgId}/projects/{projectId}/jobs/{jobId}/trigger | ✓ | Manually trigger a job run |
Volumes
Section titled “Volumes”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/mounts | ✓ | List a service’s volume mounts |
| GET | /orgs/{orgId}/projects/{projectId}/volumes | ✓ | List volumes |
| POST | /orgs/{orgId}/projects/{projectId}/volumes | ✓ | Create a volume |
| GET | /orgs/{orgId}/projects/{projectId}/volumes/{volumeId} | ✓ | Get a volume |
| DELETE | /orgs/{orgId}/projects/{projectId}/volumes/{volumeId} | ✓ | Delete a volume (must be unattached) |
| GET | /orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/backup | ✓ | Get a volume’s backup config |
| PUT | /orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/backup | ✓ | Set a volume’s backup config |
| DELETE | /orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/backup | ✓ | Remove a volume’s backup config |
| POST | /orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/mounts | ✓ | Attach a volume to a service |
| DELETE | /orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/mounts/{mountId} | ✓ | Detach a volume mount |
| PUT | /orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/node | ✓ | Pin a volume to a node, or clear the pin to auto-schedule |
| GET | /orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/placement | ✓ | Where the volume’s claim is actually bound or pinned |
Variable groups
Section titled “Variable groups”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/variable-groups | ✓ | List variable groups attached to service |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/variable-groups | ✓ | Attach variable group to service |
| DELETE | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/variable-groups/{groupId} | ✓ | Detach variable group from service |
| GET | /orgs/{orgId}/projects/{projectId}/jobs/{jobId}/variable-groups | ✓ | List variable groups attached to a job |
| POST | /orgs/{orgId}/projects/{projectId}/jobs/{jobId}/variable-groups | ✓ | Attach a variable group to a job |
| DELETE | /orgs/{orgId}/projects/{projectId}/jobs/{jobId}/variable-groups/{groupId} | ✓ | Detach a variable group from a job |
| GET | /orgs/{orgId}/projects/{projectId}/variable-groups | ✓ | List variable groups |
| POST | /orgs/{orgId}/projects/{projectId}/variable-groups | ✓ | Create variable group |
| GET | /orgs/{orgId}/projects/{projectId}/variable-groups/{groupId} | ✓ | Get variable group |
| PATCH | /orgs/{orgId}/projects/{projectId}/variable-groups/{groupId} | ✓ | Update variable group |
| DELETE | /orgs/{orgId}/projects/{projectId}/variable-groups/{groupId} | ✓ | Delete variable group |
| PUT | /orgs/{orgId}/projects/{projectId}/variable-groups/{groupId}/items | ✓ | Upsert variable group item |
| GET | /orgs/{orgId}/projects/{projectId}/variable-groups/{groupId}/items/{itemId}/value | ✓ | Reveal one item’s value. A list never carries a secret’s value, so reading one is its own request, gated on project update rather than view |
| DELETE | /orgs/{orgId}/projects/{projectId}/variable-groups/{groupId}/items/{itemId} | ✓ | Delete variable group item |
Config files
Section titled “Config files”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/projects/{projectId}/config-files | ✓ | List the project’s config files |
| POST | /orgs/{orgId}/projects/{projectId}/config-files | ✓ | Create a config file |
| GET | /orgs/{orgId}/projects/{projectId}/config-files/{fileId} | ✓ | Get a config file and the services mounting it |
| PATCH | /orgs/{orgId}/projects/{projectId}/config-files/{fileId} | ✓ | Replace a config file’s content, re-applying every service using it |
| DELETE | /orgs/{orgId}/projects/{projectId}/config-files/{fileId} | ✓ | Delete a config file that no service mounts |
| POST | /orgs/{orgId}/projects/{projectId}/config-files/{fileId}/attach/{serviceId} | ✓ | Mount a config file into a service |
| DELETE | /orgs/{orgId}/projects/{projectId}/config-files/{fileId}/attach/{serviceId} | ✓ | Unmount a config file from a service |
Routes
Section titled “Routes”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/projects/{projectId}/routes | ✓ | List routes in a project |
| POST | /orgs/{orgId}/projects/{projectId}/routes | ✓ | Create a route |
| GET | /orgs/{orgId}/projects/{projectId}/routes/{routeId} | ✓ | Get a route |
| DELETE | /orgs/{orgId}/projects/{projectId}/routes/{routeId} | ✓ | Delete a route |
| POST | /orgs/{orgId}/projects/{projectId}/routes/{routeId}/targets | ✓ | Add a path target to a route |
| PATCH | /orgs/{orgId}/projects/{projectId}/routes/{routeId}/targets/{targetId} | ✓ | Update a route target |
| DELETE | /orgs/{orgId}/projects/{projectId}/routes/{routeId}/targets/{targetId} | ✓ | Delete a route target |
| POST | /orgs/{orgId}/projects/{projectId}/routes/{routeId}/move | ✓ | Move a route to another base domain, keeping its subdomain, zone and targets; keep_redirect leaves the old hostname answering with a 301 |
| POST | /orgs/{orgId}/projects/{projectId}/routes/{routeId}/verify-hostname | ✓ | Verify DNS ownership of a custom-domain route via TXT record |
| POST | /orgs/{orgId}/projects/{projectId}/routes/{routeId}/publish | ✓ | Serve a paused route again |
| POST | /orgs/{orgId}/projects/{projectId}/routes/{routeId}/pause | ✓ | Keep a route, answer 404 and issue no certificate. Create one paused with published: false |
| GET | /orgs/{orgId}/routes | ✓ | List all routes in an organization |
| GET | /orgs/{orgId}/tcp-routes | ✓ | List every published TCP port in an organization, with the gateway’s own |
| GET | /orgs/{orgId}/projects/{projectId}/tcp-routes | ✓ | List TCP routes in a project |
| POST | /orgs/{orgId}/projects/{projectId}/tcp-routes | ✓ | Publish a TCP port on the gateway |
| GET | /orgs/{orgId}/projects/{projectId}/tcp-routes/{routeId} | ✓ | Get a TCP route |
| POST | /orgs/{orgId}/projects/{projectId}/tcp-routes/{routeId}/publish | ✓ | Open a paused TCP route’s port again |
| POST | /orgs/{orgId}/projects/{projectId}/tcp-routes/{routeId}/pause | ✓ | Close a TCP route’s port and keep the route |
| PATCH | /orgs/{orgId}/projects/{projectId}/tcp-routes/{routeId} | ✓ | Change a TCP route’s port or who may connect |
| DELETE | /orgs/{orgId}/projects/{projectId}/tcp-routes/{routeId} | ✓ | Stop publishing a TCP port |
| GET | /orgs/{orgId}/edge-fallback | ✓ | Where the proxy sends hostnames it has no route for during a migration whose edge was taken first; null when nothing |
| PUT | /orgs/{orgId}/edge-fallback | ✓ | Org admin: send the listed hostnames to the old platform’s edge on its side port while no route serves them |
| DELETE | /orgs/{orgId}/edge-fallback | ✓ | Org admin: stop sending anything to the old platform’s edge |
Domains & TLS
Section titled “Domains & TLS”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /internal/domain-check | public (Caddy) | Caddy ask endpoint for On-Demand TLS |
| GET | /internal/ondemand-tls-check | public (Caddy) | Caddy ask endpoint for On-Demand TLS (self-managed DNS mode) |
| GET | /orgs/{orgId}/domains | ✓ | List domains for an organization (primary first) |
| POST | /orgs/{orgId}/domains | ✓ | Add a base domain; with dns_provider_id, an on-demand one whose records the provider writes, verified at once; with project_id, reserved for that project |
| PUT | /orgs/{orgId}/domains/{domainId}/project | ✓ | Reserve a domain for one project and its levels, or give it back to every project (project_id empty) |
| GET | /orgs/{orgId}/domains/{domainId} | ✓ | Get a domain |
| POST | /orgs/{orgId}/domains/{domainId}/verify | ✓ | Check the ownership TXT record for a domain |
| GET | /orgs/{orgId}/domains/{domainId}/routes | ✓ | Hostnames served under a base domain, across every project |
| POST | /orgs/{orgId}/domains/{domainId}/retire | ✓ | Start retiring a base domain: no new routes, everything already on it keeps serving |
| DELETE | /orgs/{orgId}/domains/{domainId}/retire | ✓ | Stop retiring a base domain |
| POST | /orgs/{orgId}/domains/{domainId}/make-primary | ✓ | Move the primary here. The old primary keeps serving its console, api and headscale names until it is removed |
| GET | /orgs/{orgId}/domains/{domainId}/nodes | ✓ | Nodes whose control connection goes through this domain’s headscale name - what stops a former primary being removed |
| POST | /orgs/{orgId}/nodes/{nodeId}/control-moved | ✓ | Record that a node now reaches Headscale through the primary. Records what the operator did on the machine; it does not move it |
| GET | /orgs/{orgId}/domains/{domainId}/integrations | ✓ | Git provider registrations - a GitHub App’s URLs, an OAuth redirect, repository push hooks - still pointing at this domain |
| POST | /orgs/{orgId}/git-integrations/{id}/move-hooks | ✓ | Move an integration’s repository push hooks to the primary’s API address through the provider’s API, per repository |
| POST | /orgs/{orgId}/git-integrations/{id}/registration-updated | ✓ | Record that a registration was changed at the provider by hand. For an OAuth redirect, Meshploy starts sending the new URI |
| GET | /orgs/{orgId}/domains/{domainId}/deploy-hooks | ✓ | Services whose CI deploy webhook was last called through this domain, as observed from where calls arrive |
| DELETE | /orgs/{orgId}/services/{serviceId}/deploy-hook-call | ✓ | Forget a deploy webhook’s last caller, for a CI job that no longer exists. A later call records it again |
| GET | /orgs/{orgId}/custom-domains | ✓ | Hostnames that belong to a route rather than a base domain |
| PATCH | /orgs/{orgId}/domains/{domainId}/dns-mode | ✓ | Change how a domain’s DNS is arranged |
| PUT | /orgs/{orgId}/domains/{domainId}/dns-provider | ✓ | Choose who keeps an on-demand domain’s records: a connected DNS provider (written and read back, verifying the domain) or by hand (Meshploy’s records removed) |
| DELETE | /orgs/{orgId}/domains/{domainId} | ✓ | Remove a base domain. A verified one must be retiring and hold no routes; no node may still reach the mesh through it, no git provider may still call it, and no CI job may still deploy through it. An unverified one can go at once |
Verifying a domain, changing its DNS mode or removing one records the new domain set in the host agent’s inbox and asks it to regenerate Caddy and CoreDNS. The API writes no configuration itself and reloads nothing: it has no root on the host and no Docker socket.
Backups
Section titled “Backups”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups | ✓ | List backup configs for a service |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups | ✓ | Add a backup config for a service |
| PATCH | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups/{id} | ✓ | Update a backup config |
| DELETE | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups/{id} | ✓ | Delete a backup config |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups/{id}/objects | ✓ | List restore points for a backup config |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups/{id}/restore | ✓ | Restore a database from a backup object |
| POST | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups/{id}/trigger | ✓ | Manually trigger a backup |
| GET | /orgs/{orgId}/system-backup | ✓ | Get system backup config for an org |
| PUT | /orgs/{orgId}/system-backup | ✓ | Create or update system backup config |
| DELETE | /orgs/{orgId}/system-backup | ✓ | Delete system backup config |
| GET | /orgs/{orgId}/system-backup/objects | ✓ | List restore points for the system backup |
| POST | /orgs/{orgId}/system-backup/restore | ✓ | Restore system database from a backup object |
| POST | /orgs/{orgId}/system-backup/trigger | ✓ | Manually trigger the system backup |
Notifications & email
Section titled “Notifications & email”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/email-config | ✓ | Get the org SMTP configuration |
| PUT | /orgs/{orgId}/email-config | ✓ | Create or update the org SMTP configuration |
| DELETE | /orgs/{orgId}/email-config | ✓ | Remove the org SMTP configuration |
| POST | /orgs/{orgId}/email-config/test | ✓ | Send a test email to one address; a failed send returns success: false and the error |
| GET | /notification-events | ✓ | List the events a notification channel can subscribe to |
| GET | /orgs/{orgId}/notification-channels | ✓ | List notification channels |
| POST | /orgs/{orgId}/notification-channels | ✓ | Create a notification channel |
| PUT | /orgs/{orgId}/notification-channels/{id} | ✓ | Update a notification channel |
| DELETE | /orgs/{orgId}/notification-channels/{id} | ✓ | Delete a notification channel |
| POST | /orgs/{orgId}/notification-channels/{id}/test | ✓ | Send a test notification and record the attempt |
| GET | /orgs/{orgId}/notification-channels/{id}/deliveries | ✓ | A channel’s delivery attempts, newest first (?status=failed, ?limit=, ?before=<timestamp> to page back) |
| POST | /orgs/{orgId}/notification-deliveries/{id}/retry | ✓ | Send a recorded attempt again |
Integrations
Section titled “Integrations”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /gitea/callback | OAuth state | Gitea OAuth callback |
| GET | /github/app-callback | OAuth state | GitHub App installation callback |
| GET | /github/callback | OAuth state | GitHub OAuth callback |
| GET | /gitlab/callback | OAuth state | GitLab OAuth callback |
| GET | /orgs/{orgId}/git-integrations | ✓ | List git integrations |
| POST | /orgs/{orgId}/git-integrations | ✓ | Create a GitLab, Gitea or Bitbucket integration from a token |
| POST | /orgs/{orgId}/git-integrations/github | ✓ | Start a GitHub App integration (manifest flow) |
| POST | /orgs/{orgId}/git-integrations/oauth | ✓ | Start a GitLab, Gitea or Bitbucket OAuth App connection |
| DELETE | /orgs/{orgId}/git-integrations/{id} | ✓ | Delete a git integration |
| GET | /orgs/{orgId}/git-integrations/{id}/branches | ✓ | List branches for a repository |
| POST | /orgs/{orgId}/detect-stack | ✓ | Look at a repository’s branch (shallow clone of its tree and small files) and suggest a builder, port, start command and memory limit for a new service |
| GET | /orgs/{orgId}/git-integrations/{id}/install-url | ✓ | Get GitHub App install URL for a specific integration |
| GET | /orgs/{orgId}/git-integrations/{id}/oauth-reconnect | ✓ | Re-generate OAuth authorization URL for a pending integration |
| GET | /orgs/{orgId}/git-integrations/{id}/push-hook | ✓ | Where the provider should deliver pushes, and the secret to sign them with (org admin) |
| GET | /orgs/{orgId}/git-integrations/{id}/repos | ✓ | List repositories for a git integration |
| GET | /orgs/{orgId}/registry-integrations | ✓ | List container registry integrations |
| POST | /orgs/{orgId}/registry-integrations | ✓ | Add a container registry integration |
| DELETE | /orgs/{orgId}/registry-integrations/{id} | ✓ | Remove a container registry integration |
| GET | /orgs/{orgId}/storage-integrations | ✓ | List object storage integrations |
| POST | /orgs/{orgId}/storage-integrations | ✓ | Add an object storage integration |
| DELETE | /orgs/{orgId}/storage-integrations/{id} | ✓ | Remove an object storage integration |
| GET | /orgs/{orgId}/dns-providers | ✓ | List connected DNS providers |
| POST | /orgs/{orgId}/dns-providers | ✓ | Connect a DNS provider; the token must be able to write records in at least one zone |
| POST | /orgs/{orgId}/dns-providers/{id}/check | ✓ | Check its token again and keep the zones it may write in |
| DELETE | /orgs/{orgId}/dns-providers/{id} | ✓ | Disconnect a DNS provider; refused while a domain uses it |
Terminals
Section titled “Terminals”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /orgs/{orgId}/nodes/{nodeId}/terminal | ticket | WebSocket: shell on a node |
| GET | /orgs/{orgId}/projects/{projectId}/services/{serviceId}/pods/{podName}/terminal | ticket | WebSocket: shell in a pod |
| POST | /terminal/ticket | ✓ | Mint a single-use ticket for a terminal WebSocket |
Webhooks
Section titled “Webhooks”| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /webhooks/deploy/{serviceId} | deploy token | Inbound deploy webhook |
| POST | /webhooks/github/{integrationId} | HMAC | Inbound GitHub push webhook |
| POST | /webhooks/git/{provider}/{integrationId} | HMAC / token | Inbound push webhook for GitLab, Gitea/Forgejo and Bitbucket |
System
Section titled “System”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /system/login-info | public | The console and API addresses for a CLI finding this server, on the domain it was reached through when that domain serves the platform, else the primary’s; and the version |
| GET | /system/edge | ✓ | The gateway’s edge name (edge.<primary domain>), which a custom hostname points at with a CNAME, its public address, and whether the name resolves there now |
| GET | /system/exposure | ✓ | Report whether this gateway runs without a host firewall |
| POST | /system/notices/{key}/dismiss | ✓ | Dismiss a console advisory for the current user |
| GET | /system/channels | ✓ | Describe the stable and edge channels, where this server is, and whether it may switch |
| GET | /system/upgrade | ✓ | Report whether this server can be upgraded from the console, and the last upgrade |
| POST | /system/upgrade | ✓ | Queue an upgrade of this server to the latest build on its channel, a switch to the other channel ({"channel": "edge"}), or a switch to the Enterprise images the active licence grants ({"edition": "enterprise"}). Instance owner only |
| GET | /system/edge-upgrade | ✓ | Through a cluster agent, the edge’s upgrade: a request still waiting, the one named by ?request=, and the updater’s last run as the edge reports it. The server’s owner only |
| POST | /system/edge-upgrade | ✓ | Asks the edge to upgrade itself and its cluster agent (channel: stable or edge, else its own), as Meshploy does release by release; its updater runs it. 202 with the request id. The server’s owner only |
| GET | /system/version | ✓ | Get current and latest platform version |
| GET | /system/migrate/dokploy | ✓ | Instance owner: the host agent’s latest Dokploy detection and plan, and the state of each request |
| POST | /system/migrate/dokploy/{kind} | ✓ | Instance owner: run a stage of the migration. detect and plan are read-only on the host. credential mints the migration agent’s token and leaves it in the inbox for the agent to take once. prepare is stage 1 - the Meshploy side, services stopped and routes paused, Dokploy untouched. move is stage 2 for one group (body: group) and is the first kind that stops anything. cutover is stage 3, handing over ports 80 and 443; with edge_first it runs before every group has moved, and the old edge keeps serving them from a side port. rollback undoes one group, or everything when no group is given; 409 when the agent is not reporting |
| GET | /system/host-agent | ✓ | Whether the gateway’s host agent is reporting, its version and the firewall it found |
| POST | /system/check-updates | ✓ | Ask GitHub for the newest release or build now, past the caches; at most one real check every 30 seconds |
Outside /api/v1
Section titled “Outside /api/v1”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health | public | Health check |
| GET | /install.sh | public | Install script, with this gateway’s address filled in |
| GET | /join/macos.sh | public | Join a Mac as a mesh-only node |
| GET | /join/windows.ps1 | public | Join a Windows machine as a mesh-only node |
| GET/POST | /mcp | agent token | Remote MCP (Streamable HTTP), permission-scoped |
| GET | /uninstall.sh | ✓ | Uninstall script |
Node enrichment
Section titled “Node enrichment”GET /orgs/{orgId}/nodes enriches each node with live Headscale peer data (online status, last seen, FQDN). When a node has a stored headscale_id the lookup is O(1). Nodes without an ID fall back to an IP scan and store the ID as a side-effect for future calls.
Self-register / self-deregister
Section titled “Self-register / self-deregister”Worker nodes authenticate with a registration or provisioning token rather than a user JWT:
- Self-register — called by
install.sh. Accepts either amreg-<hex>registration token (reusable, org-wide) or amprov-<hex>provisioning token (single-use, with expiry). Creates the node record and returns the node ID. A cluster node also gets its k3s join token there; when none can be had the node is registered anyway, with its secret, andk3s_join_errorsays why, since the single-use token it spent cannot register it again. - Provision (
POST /nodes/provision): the public call before the mesh, made with a provisioning token. It answers the mesh credentials, the API’s own mesh address (api_mesh_url, onAPI_PORT), andcluster_agent: truefor a token minted for the cluster’s server, whichinstall.shinstalls as an edge. - Self-deregister — called by
uninstall.sh. Removes the node from Headscale, the k3s cluster, and the database.
Environment variables
Section titled “Environment variables”| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes | PostgreSQL DSN |
JWT_SECRET | Yes | Secret for signing JWTs |
ENCRYPTION_KEY | Yes | Exactly 32 characters: AES-256-GCM field encryption |
API_PORT | No | Listen port (default: 4000) |
API_BASE_URL | No | Public base URL of the API (default: http://localhost:4000) |
FRONTEND_URL | No | Console URL (default: http://localhost:5173) |
SETUP_TOKEN | No | Gates the first registration; set by install.sh. Empty disables the check |
SIGN_IN_LINK_KEY | No | Lets whoever holds it make one-time console sign-in links (POST /system/sign-in-links). Empty (the default) turns them off |
HEADSCALE_URL | No | Headscale API URL |
HEADSCALE_API_KEY | No | Headscale API key |
HEADSCALE_USER | No | Headscale user pre-auth keys are created under (default: meshploy) |
KUBECONFIG | No | Path to kubeconfig; empty = in-cluster |
K3S_SERVER_URL | No | Override the k3s API URL (needed when the API runs in Docker) |
K3S_TLS_SERVER_NAME | No | Name the cluster certificate is verified against when K3S_SERVER_URL rewrites the address |
K3S_SKIP_TLS_VERIFY | No | Escape hatch: disables authentication of the cluster connection. Leave unset |
K3S_TOKEN | No | Node token for workers joining the cluster |
BUILDER_IMAGE | No | Override the builder container image |
DOMAIN | No | Base domain; seeds the org domain record |
MESH_IP | No | Gateway’s WireGuard mesh IP; seeds the gateway node |
GATEWAY_HOSTNAME | No | Gateway hostname, used for gateway node seeding |
PUBLIC_IP | No | Gateway public IP, backfilled on the gateway node record |
HOST_GATEWAY_IP | No | Docker bridge IP, used when the API runs in Docker to reach the gateway’s node_exporter |
FIREWALL_STATE | No | What install.sh saw on the host: none, ufw or firewalld. Drives the console’s exposure notice |
NODEPORT_ADDRESSES | No | The CIDR kube-proxy binds published ports to, e.g. 100.64.0.0/10 for the mesh. Empty means every interface, which the console warns about when a database is published |
FIREWALL_CHECKED_AT | No | When install.sh checked the firewall (RFC3339) |
BUILTIN_REGISTRY_ENDPOINT | No | Seed a built-in registry row per org (<host>:<port>) |
TEMPLATE_DIR | No | Local template catalog directory; overrides the remote repo |
TEMPLATE_REPO | No | GitHub owner/repo the catalog is fetched from (default: meshploy/meshploy-templates) |
TEMPLATE_REPO_REF | No | Git ref for the catalog repo (default: main) |
TEMPLATE_REFRESH_INTERVAL | No | How often the catalog cache refreshes (default: 1h) |
Running locally
Section titled “Running locally”cd apps/apigo run main.goAPI at http://localhost:4000. The OpenAPI spec is at /openapi.json and needs a bearer token, like every non-public route.
Database migrations run automatically on startup via db.Migrate().