Repository navigation
API Reference
TL;DR
- What: 50+ REST API endpoints covering auth, user management, server operations, terminal sessions, audit logs, notifications, LDAP config, SMTP config, hub settings, and agent/CLI installation
- Who: Frontend developers, integration builders, and anyone automating Veyport
- Why: Complete reference for every HTTP endpoint with request/response schemas
- Where: All endpoints served by the Hub on the HTTP port (default :8081)
- When: After authentication - most endpoints require a valid JWT access token or CLI-created API token
- How: JSON request/response bodies; secure cookie auth for browsers with Bearer fallback for scripts/API tokens; SSE for streaming
- Overview
- Authentication Flow
- Error Format
- Auth Endpoints
- User Management Endpoints
- Audit Log Endpoints
- Server Endpoints
- Path Permission Endpoints
- Agent Operation Endpoints
- Installation Endpoints
- Server Removal Endpoints
- Hub Settings Endpoints
- LDAP Directory Endpoints
- SMTP and Notification Endpoints
- Re-Enrollment Endpoints
- Terminal Endpoints
- SSH Gateway Endpoints
| Property | Value |
|---|---|
| Base URL |
http://<hub-host>:8081 (dev) or https://<hub-domain> (prod) |
| Content Type |
application/json (request and response) |
| Browser Auth |
veyport_access and veyport_refresh secure cookies, plus veyport_csrf for mutating requests |
| API Client Auth | Authorization: Bearer <JWT access token or CLI-created API token> |
| Pagination |
?limit=N&offset=N (default limit varies, max 100) |
| CORS | Enabled in dev mode for http://localhost:5173
|
| Caching |
Cache-Control: no-store on all API responses |
All request and response bodies are JSON unless otherwise noted (file uploads use multipart/form-data, SSE streams use text/event-stream, binary downloads use application/octet-stream).
For browser sessions, successful login and refresh flows set auth cookies and may omit token strings from the JSON response body. The cURL examples below use Bearer headers because they are intended for scripts and API clients.
Veyport uses four JWT token types. In browser-driven flows, the resulting access and refresh tokens are set as secure cookies.
| Token Type | Purpose | Lifetime |
|---|---|---|
| Setup | Issued after first login when TOTP is not yet configured. Authorizes /api/auth/totp/setup and /api/auth/totp/enable. |
Short-lived |
| TOTP | Issued after password verification when TOTP is enabled. Authorizes /api/auth/login/totp. |
Short-lived |
| Access | General-purpose API token. Required by most endpoints. | Short-lived |
| Refresh | Long-lived token used to obtain a new access/refresh pair via /api/auth/refresh. |
Long-lived |
For non-browser automation, Veyport also supports opaque API tokens created with the Hub CLI. These are not JWTs, are stored as SHA-256 hashes at rest, and are recommended over automating a human password + TOTP secret.
Create a dedicated low-privilege user for automation, then mint an API token on the Hub host:
# Docker deployment
docker exec veyport /app/veyport admin create-api-token \
--username scanner \
--name nightly-scan \
--expires-in 720h \
--db /data/veyport.db
# Bare-metal deployment
./bin/veyport admin create-api-token \
--username scanner \
--name nightly-scan \
--expires-in 720h \
--db /var/lib/veyport/veyport.dbUse the returned token with the standard Bearer header:
curl -H "Authorization: Bearer adt_..." https://hub.example.com/api/auth/meAPI tokens can call normal access-token-protected API endpoints, but they are intentionally rejected on interactive account-management endpoints such as password changes, avatar updates, and TOTP disable.
sequenceDiagram
participant Client
participant Hub
Client->>Hub: POST /api/auth/login {username, password}
alt TOTP not set up
Hub-->>Client: 200 {setup_token, requires_totp_setup: true}
Client->>Hub: POST /api/auth/totp/setup (Bearer setup_token)
Hub-->>Client: 200 {secret, qr_url}
Client->>Hub: POST /api/auth/totp/enable {code} (Bearer setup_token)
Hub-->>Client: 200 Set-Cookie(access, refresh, csrf) + {user}
else TOTP enabled
Hub-->>Client: 202 {totp_token}
Client->>Hub: POST /api/auth/login/totp {totp_token, code}
Hub-->>Client: 200 Set-Cookie(access, refresh, csrf) + {user}
end
All errors return a JSON object with a single error key:
{
"error": "human-readable error message"
}The HTTP status code indicates the error category:
| Status | Meaning |
|---|---|
| 400 | Bad Request - invalid input, missing required fields |
| 401 | Unauthorized - invalid credentials or expired token |
| 403 | Forbidden - insufficient permissions |
| 404 | Not Found - resource does not exist |
| 409 | Conflict - resource already exists (e.g., duplicate user) |
| 413 | Request Entity Too Large - file exceeds size limit |
| 429 | Too Many Requests - rate limit exceeded |
| 500 | Internal Server Error |
| 502 | Bad Gateway - agent communication failure |
| 504 | Gateway Timeout - agent did not respond in time |
Source: hub/internal/server/respond.go -- respondError(w, status, message) produces {"error": message}.
Check whether the Hub has been initialized (at least one user exists).
| Property | Value |
|---|---|
| Auth | None |
| Rate Limited | No |
Response (200) - unauthenticated:
{
"initialized": true
}Response (200) - authenticated (with valid access token):
{
"initialized": true,
"version": "vX.Y.Z"
}Note: The
versionfield is only included when the request carries a valid access token. Unauthenticated requests receive only theinitializedfield.
cURL:
curl https://hub.example.com/api/auth/statusRegister the first admin user. Only works when no users exist (initialized: false).
| Property | Value |
|---|---|
| Auth | None |
| Rate Limited | Yes (10 requests / 60s) |
Request Body:
{
"username": "admin",
"email": "admin@example.com",
"password": "S3cur3P@ssw0rd!"
}Validation rules:
- Username: 3-32 characters, alphanumeric and underscores only
- Password: must satisfy
auth.ValidatePasswordPolicy(minimum length, complexity)
Response (200):
{
"setup_token": "eyJhbGci...",
"user": {
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"totp_enabled": false,
"avatar": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
}Error Cases:
-
403- Registration disabled (users already exist) -
400- Invalid username or password policy violation -
409- User already exists
cURL:
curl - X POST https://hub.example.com/api/auth/register \
- H 'Content-Type: application/json' \
- d '{"username":"admin","email":"admin@example.com","password":"S3cur3P@ssw0rd!"}'Authenticate with username and password.
| Property | Value |
|---|---|
| Auth | None |
| Rate Limited | Yes (10 requests / 60s) |
Request Body:
{
"username": "admin",
"password": "S3cur3P@ssw0rd!"
}Response when TOTP is not set up (200):
{
"setup_token": "eyJhbGci...",
"requires_totp_setup": true
}Response when TOTP is enabled (202):
{
"totp_token": "eyJhbGci..."
}Error Cases:
-
401- Invalid credentials
cURL:
curl - X POST https://hub.example.com/api/auth/login \
- H 'Content-Type: application/json' \
- d '{"username":"admin","password":"S3cur3P@ssw0rd!"}'Complete login by providing a TOTP code (second factor).
| Property | Value |
|---|---|
| Auth | None (TOTP token in body) |
| Rate Limited | Yes (3 requests / 60s) |
Request Body:
{
"totp_token": "eyJhbGci...",
"code": "123456"
}Response (200):
{
"user": {
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"totp_enabled": true,
"avatar": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
}On success, the Hub also sets veyport_access, veyport_refresh, and veyport_csrf cookies for browser clients.
Error Cases:
-
401- Invalid or expired TOTP token, invalid TOTP code, user not found
cURL:
curl - X POST https://hub.example.com/api/auth/login/totp \
- H 'Content-Type: application/json' \
- d '{"totp_token":"eyJhbGci...","code":"123456"}'Exchange a refresh token for a new access/refresh token pair.
| Property | Value |
|---|---|
| Auth | None. Browser clients usually send the veyport_refresh cookie; non-browser clients may send refresh_token in the JSON body |
| Rate Limited | Yes (30 requests / 60s) |
Request Body:
{
"refresh_token": "eyJhbGci..."
}Response (200):
{}On success, the Hub rotates the refresh token and sets new veyport_access, veyport_refresh, and veyport_csrf cookies.
Error Cases:
-
401- Invalid or expired refresh token
cURL:
curl - X POST https://hub.example.com/api/auth/refresh \
- H 'Content-Type: application/json' \
- d '{"refresh_token":"eyJhbGci..."}'Generate a new TOTP secret and QR URL. The secret is stored but not yet enabled.
| Property | Value |
|---|---|
| Auth | Setup token (Bearer) |
| Rate Limited | No |
Request Body: None
Response (200):
{
"secret": "JBSWY3DPEHPK3PXP",
"qr_url": "otpauth://totp/Veyport:admin?secret=JBSWY3DPEHPK3PXP&issuer=Veyport"
}Error Cases:
-
401- Invalid or expired setup token -
404- User not found
cURL:
curl - X POST https://hub.example.com/api/auth/totp/setup \
- H 'Authorization: Bearer <setup_token>'Verify and enable TOTP by providing a valid code. For users created with a temporary password, this is also where the permanent password is set.
| Property | Value |
|---|---|
| Auth | Setup token (Bearer) |
| Rate Limited | No |
Request Body:
{
"code": "123456",
"new_password": "OptionalUnlessTemporaryPassword"
}Response (200):
{
"user": {
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"totp_enabled": true,
"avatar": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
}On success, the Hub also sets veyport_access, veyport_refresh, and veyport_csrf cookies for browser clients.
Error Cases:
-
401- Invalid TOTP code -
400- TOTP not set up (call/api/auth/totp/setupfirst)
cURL:
curl - X POST https://hub.example.com/api/auth/totp/enable \
- H 'Authorization: Bearer <setup_token>' \
- H 'Content-Type: application/json' \
- d '{"code":"123456"}'Get the current authenticated user's profile.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
| Rate Limited | No |
Response (200):
{
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"totp_enabled": true,
"avatar": "data:image/png;base64,...",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}Error Cases:
-
404- User not found
cURL:
curl https://hub.example.com/api/auth/me \
- H 'Authorization: Bearer <access_token>'Change the authenticated user's password.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
| Rate Limited | No |
Request Body:
{
"current_password": "OldP@ssw0rd!",
"new_password": "N3wP@ssw0rd!"
}Response (200):
{
"status": "password updated"
}Note: A successful password change invalidates all of the user's existing sessions (access and refresh tokens) except the one used to make the request. Other devices will need to log in again.
Error Cases:
-
401- Invalid current password -
400- New password does not meet policy requirements -
404- User not found
cURL:
curl - X PUT https://hub.example.com/api/auth/password \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"current_password":"OldP@ssw0rd!","new_password":"N3wP@ssw0rd!"}'Update the authenticated user's avatar image.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
| Rate Limited | No |
Request Body:
{
"avatar": "data:image/png;base64,iVBOR..."
}Send an empty string to remove the avatar.
Limits: Maximum ~500KB base64-encoded data URL (700,000 characters).
Response (200):
{
"status": "avatar updated"
}Error Cases:
-
400- Avatar image too large (max 500KB)
cURL:
curl - X PUT https://hub.example.com/api/auth/avatar \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"avatar":"data:image/png;base64,iVBOR..."}'Admin-only: disable TOTP for another user. Requires the admin's own TOTP code for verification.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
| Rate Limited | No |
Request Body:
{
"user_id": "target-user-uuid",
"admin_totp_code": "123456"
}Response (200):
{
"status": "totp disabled"
}Error Cases:
-
401- Invalid admin TOTP code -
404- Admin user not found -
403- Not an admin
cURL:
curl - X POST https://hub.example.com/api/auth/totp/disable \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"user_id":"target-user-uuid","admin_totp_code":"123456"}'All user management endpoints require admin role.
List all users.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Response (200):
{
"users": [
{
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"auth_provider": "local",
"terminal_access": false,
"totp_enabled": true,
"avatar": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}cURL:
curl https://hub.example.com/api/users \
- H 'Authorization: Bearer <access_token>'Create a new user. A temporary password is auto-generated.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"username": "newuser",
"email": "newuser@example.com",
"role": "viewer"
}Validation:
-
rolemust be"admin","auditor", or"viewer" - Username: 3-32 characters, alphanumeric and underscores only
Response (201):
{
"user": {
"id": "uuid",
"username": "newuser",
"email": "newuser@example.com",
"role": "viewer",
"auth_provider": "local",
"terminal_access": false,
"totp_enabled": false,
"avatar": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
"temporary_password": "auto-generated-password"
}Error Cases:
-
400- Invalid username or role -
409- User already exists
cURL:
curl - X POST https://hub.example.com/api/users \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"username":"newuser","email":"newuser@example.com","role":"viewer"}'Update a user's role.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Target user UUID
Request Body:
{
"role": "auditor"
}Response (200):
{
"user": {
"id": "uuid",
"username": "newuser",
"email": "newuser@example.com",
"role": "admin",
"auth_provider": "local",
"terminal_access": false,
"totp_enabled": false,
"avatar": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
}Error Cases:
-
400- Invalid role or attempting to change own role -
404- User not found
cURL:
curl - X PUT https://hub.example.com/api/users/USER_UUID/role \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"role":"admin"}'Delete a user.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Target user UUID
Response (200):
{
"status": "user deleted"
}Error Cases:
-
400- Cannot delete your own account -
404- User not found
cURL:
curl - X DELETE https://hub.example.com/api/users/USER_UUID \
- H 'Authorization: Bearer <access_token>'Audit viewing and workflow endpoints are available to admins and auditors. Admins can additionally update audit settings and run retention. The current audit API surface includes:
GET /api/audit-logsGET /api/audit-logs/catalogGET /api/audit-logs/healthGET /api/audit-logs/settings-
PUT /api/audit-logs/settings(admin only) GET /api/audit-logs/exportGET /api/audit-logs/exports-
POST /api/audit-logs/retention/run(admin only) GET /api/audit-logs/reviewsPOST /api/audit-logs/reviewsGET /api/audit-logs/filtersPOST /api/audit-logs/filtersDELETE /api/audit-logs/filters/{id}GET /api/audit-logs/flagsPOST /api/audit-logs/flagsGET /api/audit-logs/detectionsGET /api/audit-users
List audit log entries with optional filters.
| Property | Value |
|---|---|
| Auth | Access token (cookie or Bearer), admin or auditor |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
int | Max entries to return (default 50, max 100) |
offset |
int | Number of entries to skip (default 0) |
action |
string | Filter by action type (e.g., user.login, server.created) |
user_id |
string | Filter by user UUID |
from |
string | Start datetime (ISO 8601) |
to |
string | End datetime (ISO 8601) |
Available action types:
User actions: user.login, user.login_failed, user.login_totp_failed, user.registered, user.totp_setup, user.totp_enabled, user.totp_disabled, user.created, user.totp_reset, user.password_changed, user.password_reuse_rejected, user.role_updated, user.deleted
Server actions: server.created, server.updated, server.deleted, server.batch_deleted, server.registered, server.registration_failed, server.connected, server.disconnected, server.unregistered
File/path actions: file.read, file.uploaded, path.granted, path.revoked
Log actions: log.tail_started
Terminal actions: terminal.opened, terminal.closed
API token actions: api_token.created, api_token.revoked
Audit governance actions: audit.exported, audit.review_completed, audit.filter_saved, audit.filter_deleted, audit.retention_updated, audit.retention_executed, audit.flag_created
Response (200):
{
"entries": [
{
"id": "uuid",
"user_id": "uuid",
"action": "user.login",
"target": null,
"detail": null,
"ip_address": "192.168.1.1",
"created_at": "2025-01-01T00:00:00Z"
}
],
"total": 150,
"limit": 50,
"offset": 0
}cURL:
curl 'https://hub.example.com/api/audit-logs?limit=20&action=user.login' \
- H 'Authorization: Bearer <access_token>'List servers. Admins see all servers; viewers see only servers they have path permissions on.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
int | Max servers to return (default 50, max 100) |
offset |
int | Number of servers to skip (default 0) |
status |
string | Filter by status (pending, online, offline) |
search |
string | Search by server name |
Response (200):
{
"servers": [
{
"id": "uuid",
"name": "web-prod-1",
"hostname": "web-prod-1.example.com",
"ip_address": "10.0.1.5",
"os": "linux",
"status": "online",
"agent_version": "1.0.0",
"labels": "{\"env\":\"production\"}",
"last_seen_at": "2025-01-01T12:00:00Z",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
],
"total": 10,
"limit": 50,
"offset": 0
}cURL:
curl 'https://hub.example.com/api/servers?status=online&search=prod' \
- H 'Authorization: Bearer <access_token>'Create a new server entry and get back a registration token and install command.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"name": "web-prod-1",
"labels": "{\"env\":\"production\"}"
}The labels field is optional and defaults to "{}".
Response (201):
{
"server": {
"id": "uuid",
"name": "web-prod-1",
"hostname": null,
"ip_address": null,
"os": null,
"status": "pending",
"agent_version": null,
"labels": "{\"env\":\"production\"}",
"last_seen_at": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
"registration_token": "raw-uuid-token",
"install_command": "curl - sSL https://hub.example.com/install.sh | sudo bash - s -- --token <token> --hub hub.example.com:443 --url https://hub.example.com"
}Note: registration_token is the raw token. The Hub stores only a SHA-256 hash. The token expires after 1 hour.
Error Cases:
-
400- Server name is required
cURL:
curl - X POST https://hub.example.com/api/servers \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"name":"web-prod-1","labels":"{\"env\":\"production\"}"}'Get a single server by ID. Viewers must have path permissions on the server.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
Path Parameters:
-
id- Server UUID
Response (200):
{
"id": "uuid",
"name": "web-prod-1",
"hostname": "web-prod-1.example.com",
"ip_address": "10.0.1.5",
"os": "linux",
"status": "online",
"agent_version": "1.0.0",
"labels": "{\"env\":\"production\"}",
"last_seen_at": "2025-01-01T12:00:00Z",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}Error Cases:
-
403- Access denied (viewer without permissions) -
404- Server not found
cURL:
curl https://hub.example.com/api/servers/SERVER_UUID \
- H 'Authorization: Bearer <access_token>'Update a server's name and labels.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Request Body:
{
"name": "web-prod-1-updated",
"labels": "{\"env\":\"staging\"}"
}Response (200): Returns the updated server object (same shape as GET /api/servers/{id}).
Error Cases:
-
400- Server name is required -
404- Server not found
cURL:
curl - X PUT https://hub.example.com/api/servers/SERVER_UUID \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"name":"web-prod-1-updated","labels":"{\"env\":\"staging\"}"}'Delete a server from the database (does not uninstall the agent).
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Response (200):
{
"status": "deleted"
}Error Cases:
-
404- Server not found
cURL:
curl - X DELETE https://hub.example.com/api/servers/SERVER_UUID \
- H 'Authorization: Bearer <access_token>'Delete multiple servers at once.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"ids": ["uuid-1", "uuid-2", "uuid-3"]
}Response (200):
{
"status": "deleted",
"deleted": 3
}Error Cases:
-
400- IDs list cannot be empty -
500- Failed to delete servers
cURL:
curl - X POST https://hub.example.com/api/servers/batch-delete \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"ids":["uuid-1","uuid-2"]}'Path permissions control which filesystem paths a viewer-role user can access on a given server. Admin users have unrestricted access to all paths.
List all path permissions for a server.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Response (200):
{
"paths": [
{
"id": "perm-uuid",
"user_id": "user-uuid",
"username": "viewer1",
"server_id": "server-uuid",
"path": "/var/log/nginx",
"created_at": "2025-01-01T00:00:00Z"
}
]
}cURL:
curl https://hub.example.com/api/servers/SERVER_UUID/paths \
- H 'Authorization: Bearer <access_token>'Grant a user access to a filesystem path on a server.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Request Body:
{
"user_id": "user-uuid",
"path": "/var/log/nginx"
}Response (201):
{
"id": "perm-uuid",
"user_id": "user-uuid",
"server_id": "server-uuid",
"path": "/var/log/nginx",
"created_at": "2025-01-01T00:00:00Z"
}Error Cases:
-
400-user_idandpathare required -
409- Permission already exists or invalid reference
cURL:
curl - X POST https://hub.example.com/api/servers/SERVER_UUID/paths \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"user_id":"USER_UUID","path":"/var/log/nginx"}'Revoke a path permission.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID -
pathId- Permission UUID
Response: 204 No Content
Error Cases:
-
404- Permission not found (or does not belong to this server)
cURL:
curl - X DELETE https://hub.example.com/api/servers/SERVER_UUID/paths/PERM_UUID \
- H 'Authorization: Bearer <access_token>'Get the current user's allowed paths for a server. Admins always receive ["/"] (full access).
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
Path Parameters:
-
id- Server UUID
Response (200):
{
"paths": ["/var/log/nginx", "/var/log/syslog"]
}For admins:
{
"paths": ["/"]
}cURL:
curl https://hub.example.com/api/servers/SERVER_UUID/my-paths \
- H 'Authorization: Bearer <access_token>'These endpoints proxy requests to a connected agent via gRPC. The agent must be online and connected.
Browse files on a remote server.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), permission-checked |
Path Parameters:
-
id- Server UUID
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
path |
string | Absolute directory path to list (default: /) |
Path validation:
- Must be an absolute path (starts with
/) - Path traversal (
..) is not allowed - Viewers must have a permission covering the requested path
Response (200):
{
"files": [
{
"name": "access.log",
"path": "/var/log/nginx/access.log",
"is_dir": false,
"size": 1048576,
"mod_time": "2025-01-01T12:00:00Z",
"mode": "0644"
}
]
}Error Cases:
-
400- Invalid path (not absolute, contains..) -
403- Access denied (viewer without matching permission) -
404- Path not found on agent -
502- Agent communication failure
cURL:
curl 'https://hub.example.com/api/servers/SERVER_UUID/files?path=/var/log' \
- H 'Authorization: Bearer <access_token>'Read the contents of a file from a remote server. Returns base64-encoded data.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), permission-checked |
Path Parameters:
-
id- Server UUID
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
path |
string | Absolute file path to read (required) |
Limits:
- Reads up to 1MB of file content
- Files larger than 10MB cannot be viewed (returns 413)
Response (200):
{
"data": "base64-encoded-file-content",
"total_size": 1048576,
"mime_type": "text/plain"
}Error Cases:
-
400- Path is required, path must be absolute, path traversal not allowed -
403- Access denied -
404- File not found on agent -
413- File too large for viewing (> 10MB) -
502- Agent communication failure
cURL:
curl 'https://hub.example.com/api/servers/SERVER_UUID/files/read?path=/var/log/syslog' \
- H 'Authorization: Bearer <access_token>'Tail a log file in real-time via Server-Sent Events (SSE). Each event contains base64-encoded log data.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), permission-checked |
Path Parameters:
-
id- Server UUID
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
path |
string | Absolute file path to tail (required) |
grep |
string | Optional filter string - only lines matching this are streamed |
Response: 200 with Content-Type: text/event-stream
Each SSE message has the format:
data: <base64-encoded-chunk>\n\n
The connection stays open until the client disconnects. The Hub sends a stop command to the agent on disconnect.
Error Cases:
-
400- Path is required -
403- Access denied -
502- Failed to send request to agent
cURL:
curl - N 'https://hub.example.com/api/servers/SERVER_UUID/logs/tail?path=/var/log/syslog&grep=error' \
- H 'Authorization: Bearer <access_token>'Upload a file to a server's dropzone (/tmp/veyport-dropzone/ on the agent).
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
| Content-Type | multipart/form-data |
Path Parameters:
-
id- Server UUID
Form Fields:
-
file- The file to upload (max 100MB)
Response (200):
{
"filename": "config.tar.gz",
"size": 2048576
}Error Cases:
-
400- No file provided, filename is required -
413- File too large (max 100MB) -
502- Agent communication failure -
504- Upload timeout (30s)
cURL:
curl - X POST https://hub.example.com/api/servers/SERVER_UUID/upload \
- H 'Authorization: Bearer <access_token>' \
- F 'file=@/path/to/local/config.tar.gz'List files in a server's dropzone directory (/tmp/veyport-dropzone/).
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Response (200):
{
"files": [
{
"name": "config.tar.gz",
"path": "/tmp/veyport-dropzone/config.tar.gz",
"is_dir": false,
"size": 2048576,
"mod_time": "2025-01-01T12:00:00Z",
"mode": "0644"
}
]
}Returns an empty list if the dropzone directory does not exist yet.
cURL:
curl https://hub.example.com/api/servers/SERVER_UUID/dropzone \
- H 'Authorization: Bearer <access_token>'Delete a file from a server's dropzone.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
filename |
string | Name of the file to delete (required) |
Response: 204 No Content
Error Cases:
-
400- Filename is required -
500- Delete failed on agent
cURL:
curl - X DELETE 'https://hub.example.com/api/servers/SERVER_UUID/dropzone?filename=config.tar.gz' \
- H 'Authorization: Bearer <access_token>'These endpoints are public (no authentication required) and are used by the install script.
Download the agent installation shell script.
| Property | Value |
|---|---|
| Auth | None |
| Content-Type (response) |
text/x-sh (served as static file) |
cURL:
curl - sSL https://hub.example.com/install.shDownload the agent binary for a specific platform.
| Property | Value |
|---|---|
| Auth | None |
| Content-Type (response) | application/octet-stream |
Path Parameters:
-
os- Operating system (onlylinuxis supported) -
arch- Architecture (amd64orarm64)
Response: Binary file download with Content-Disposition: attachment; filename=veyport-agent-linux-amd64
Error Cases:
-
404- Unsupported platform or binary not found
cURL:
curl - O https://hub.example.com/install/linux/amd64Download the vey CLI binary for a specific platform. See CLI for the full install walkthrough and command reference.
| Property | Value |
|---|---|
| Auth | None |
| Content-Type (response) | application/octet-stream |
Path Parameters:
-
os- Operating system (linuxordarwin) -
arch- Architecture (amd64orarm64)
Response: Binary file download with Content-Disposition: attachment; filename="vey-{os}-{arch}"
Error Cases:
-
404- Unsupported platform (outside the{linux,darwin}x{amd64,arm64}allowlist) or binary not built for this Hub
cURL:
curl -o vey https://hub.example.com/install/cli/linux/amd64Get the SHA-256 checksum of the vey CLI binary for a specific platform, for verifying the download from the endpoint above.
| Property | Value |
|---|---|
| Auth | None |
| Content-Type (response) | text/plain |
Path Parameters:
-
os- Operating system (linuxordarwin) -
arch- Architecture (amd64orarm64)
Response: The hex-encoded SHA-256 checksum as plain text.
Error Cases:
-
404- Unsupported platform or checksum not found
cURL:
curl https://hub.example.com/install/cli/linux/amd64/sha256Unregister a server: sends an unregister command to the agent (if connected), then deletes the server from the database.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
The Hub attempts to notify the connected agent to clean up. If the agent does not respond within 10 seconds, the server is still deleted from the database.
Response (200):
{
"status": "unregistered"
}Error Cases:
-
500- Failed to delete server from database
cURL:
curl - X DELETE https://hub.example.com/api/servers/SERVER_UUID/unregister \
- H 'Authorization: Bearer <access_token>'Public endpoint called by the agent during reinstallation. The server UUID in the path acts as proof of installation. Disconnects the agent (if connected) and deletes the server from the database.
| Property | Value |
|---|---|
| Auth | None |
Path Parameters:
-
id- Server UUID
Response: 204 No Content
If the server has already been deleted, also returns 204 No Content.
Error Cases:
-
500- Failed to delete server from database
cURL:
curl - X DELETE https://hub.example.com/api/servers/SERVER_UUID/self-unregisterGet the Hub configuration (currently the gRPC external address).
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Response (200):
{
"grpc_external_addr": "veyport.example.com:9443",
"jwt_secret_rotated_at": "2026-06-12T04:10:00Z"
}| Field | Notes |
|---|---|
grpc_external_addr |
Configured external gRPC address |
jwt_secret_rotated_at |
RFC 3339 UTC timestamp of the last admin rotate-jwt-secret run. null until the secret has been rotated at least once. Read-only — ignored on PUT. |
cURL:
curl https://hub.example.com/api/settings/hub \
- H 'Authorization: Bearer <access_token>'Update the Hub configuration.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"grpc_external_addr": "veyport.example.com:9443"
}Validation:
-
grpc_external_addrmust match^[a-zA-Z0-9._:\-\[\]]+$(hostnames, IPs, and ports only -- no shell metacharacters)
Response (200):
{
"status": "ok"
}Error Cases:
-
400- Invalid gRPC address format
cURL:
curl - X PUT https://hub.example.com/api/settings/hub \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"grpc_external_addr":"veyport.example.com:9443"}'Get the current LDAP configuration. The bind password is write-only and is never returned; use bind_password_set to tell whether a password is already stored.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Response (200):
{
"enabled": true,
"url": "ldaps://freeipa.example.com:636",
"bind_dn": "uid=veyport,cn=sysaccounts,cn=etc,dc=example,dc=com",
"bind_password": "",
"bind_password_set": true,
"user_base_dn": "cn=users,cn=accounts,dc=example,dc=com",
"group_base_dn": "cn=groups,cn=accounts,dc=example,dc=com",
"user_search_filter": "(uid={username})",
"group_search_filter": "(|(member={dn})(memberUid={username}))",
"username_attribute": "uid",
"email_attribute": "mail",
"external_id_attribute": "entryUUID",
"group_name_attribute": "cn",
"start_tls": false,
"tls_server_name": "freeipa.example.com",
"ca_cert_pem": "",
"allow_insecure_transport": false,
"admin_groups": ["freeipa-admins"],
"auditor_groups": ["freeipa-auditors"],
"viewer_groups": ["freeipa-viewers"],
"terminal_groups": ["bastion-users"]
}Update LDAP login, search, TLS, and group mapping settings. The bind password is encrypted before storage. Omit bind_password or send an empty string to keep the existing password; send clear_bind_password: true to clear it.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"enabled": true,
"url": "ldaps://freeipa.example.com:636",
"bind_dn": "uid=veyport,cn=sysaccounts,cn=etc,dc=example,dc=com",
"bind_password": "service-account-password",
"user_base_dn": "cn=users,cn=accounts,dc=example,dc=com",
"group_base_dn": "cn=groups,cn=accounts,dc=example,dc=com",
"user_search_filter": "(uid={username})",
"group_search_filter": "(|(member={dn})(memberUid={username}))",
"username_attribute": "uid",
"email_attribute": "mail",
"external_id_attribute": "entryUUID",
"group_name_attribute": "cn",
"start_tls": false,
"tls_server_name": "freeipa.example.com",
"ca_cert_pem": "",
"allow_insecure_transport": false,
"admin_groups": ["freeipa-admins"],
"auditor_groups": ["freeipa-auditors"],
"viewer_groups": ["freeipa-viewers"],
"terminal_groups": ["bastion-users"]
}Validation:
- Enabled LDAP requires URL, user base DN, group base DN, and at least one role group
- Plain
ldap://requires StartTLS unlessallow_insecure_transportis true - Bind DN requires a stored or submitted bind password
- CA certificate PEM must parse as a valid certificate bundle when provided
Response (200):
{
"status": "ok"
}Test the submitted LDAP URL, TLS settings, and service bind credentials without requiring an end-user password.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body: Same schema as PUT /api/settings/ldap.
Response (200):
{
"status": "ok"
}Error Cases:
-
400- Invalid LDAP configuration -
502- LDAP connection or service bind failed
Get the current SMTP configuration. The password field is omitted from the response for security.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Response (200):
{
"host": "smtp.example.com",
"port": 587,
"username": "notifications@example.com",
"from": "Veyport <notifications@example.com>",
"tls": true,
"enabled": true
}cURL:
curl https://hub.example.com/api/settings/smtp \
- H 'Authorization: Bearer <access_token>'Update the SMTP configuration. Invalidates the cached SMTP config immediately.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"host": "smtp.example.com",
"port": 587,
"username": "notifications@example.com",
"password": "smtp-password",
"from": "Veyport <notifications@example.com>",
"tls": true,
"enabled": true
}Response (200):
{
"status": "ok"
}cURL:
curl - X PUT https://hub.example.com/api/settings/smtp \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"host":"smtp.example.com","port":587,"username":"user","password":"pass","from":"Veyport <user@example.com>","tls":true,"enabled":true}'Send a test email to verify SMTP configuration. Requires SMTP settings to be saved first.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"recipient": "admin@example.com"
}Response (200):
{
"status": "ok"
}Error Cases:
-
400- Invalid request body, missing recipient, or SMTP not configured -
500- SMTP send failure (error details logged server-side)
cURL:
curl - X POST https://hub.example.com/api/settings/smtp/test \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"recipient":"admin@example.com"}'Get the current user's notification preferences. Returns all 8 event types with their enabled/disabled state.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
Response (200):
{
"preferences": [
{
"event_type": "agent.offline",
"label": "Agent went offline",
"category": "Agent",
"enabled": true
},
{
"event_type": "security.login_failed",
"label": "Failed login attempt",
"category": "Security",
"enabled": true
}
]
}cURL:
curl https://hub.example.com/api/notifications/preferences \
- H 'Authorization: Bearer <access_token>'Update the current user's notification preferences.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
Request Body:
{
"preferences": [
{ "event_type": "agent.offline", "enabled": true },
{ "event_type": "security.login_failed", "enabled": false }
]
}Validation:
- Each
event_typemust be one of the 8 known event types. Unknown types return400.
Response (200):
{
"status": "ok"
}Error Cases:
-
400- Invalid request body or unknown event type
cURL:
curl - X PUT https://hub.example.com/api/notifications/preferences \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"preferences":[{"event_type":"agent.offline","enabled":true}]}'List notification delivery log entries. Supports pagination.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
int | Max entries to return (default 50) |
offset |
int | Number of entries to skip (default 0) |
Response (200):
{
"entries": [
{
"id": "uuid",
"user_id": "user-uuid",
"username": "admin",
"event_type": "agent.offline",
"subject": "Agent web-prod-1 went offline",
"status": "sent",
"error": null,
"created_at": "2025-06-15T10:30:00Z"
}
],
"total": 42,
"limit": 50,
"offset": 0
}cURL:
curl 'https://hub.example.com/api/notifications/log?limit=20' \
- H 'Authorization: Bearer <access_token>'Re-enrollment endpoints are admin-only and require an access token. They manage the workflow for nodes whose mTLS client certificates have expired while offline.
List all servers with a pending re-enrollment request.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Response (200):
{
"requests": [
{
"request_id": "uuid",
"server_id": "uuid",
"server_name": "web-prod-1",
"hostname": "web-prod-1.example.com",
"ip_address": "10.0.1.5",
"requested_at": "2026-07-01T03:00:00Z",
"clone_suspected": false
}
]
}cURL:
curl https://hub.example.com/api/servers/reenroll/pending \
-H 'Authorization: Bearer <access_token>'Approve a pending re-enrollment request. Requires the approving admin's current TOTP code (step-up authentication). On success, the hub encrypts the KEK to the node's X25519 transport public key, issues a new client certificate for the same serverID, and the node reconnects automatically.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Request Body:
{
"request_id": "<uuid>",
"totp_code": "123456"
}Response (200):
{
"status": "approved"
}Error Cases:
| Status | Meaning |
|---|---|
401 |
Missing or invalid access token |
403 |
Bad or missing TOTP code (step-up failed) |
404 |
Server or request_id not found |
409 |
re-register required — node has no transport key; it was enrolled before transport-key support and must be re-registered once |
cURL:
curl -X POST https://hub.example.com/api/servers/SERVER_UUID/reenroll/approve \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{"request_id":"REQUEST_UUID","totp_code":"123456"}'Deny a pending re-enrollment request. The node will not receive a new certificate.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Request Body:
{
"request_id": "<uuid>"
}Response (200):
{
"status": "denied"
}Error Cases:
-
404- Server or request_id not found
cURL:
curl -X POST https://hub.example.com/api/servers/SERVER_UUID/reenroll/deny \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{"request_id":"REQUEST_UUID"}'Terminal endpoints require an interactive browser access token. CLI-created API tokens are rejected. Access is limited to admins and LDAP users who have terminal access from LDAP group mapping plus a root (/) assignment on the target server.
Create a terminal session on an online server.
| Property | Value |
|---|---|
| Auth | Interactive access token, terminal access required |
| Rate Limit | 3600 requests/minute shared across terminal endpoints |
Path Parameters:
-
id- Server UUID
Request Body:
{
"cols": 120,
"rows": 32,
"cwd": "/home/alice"
}cwd is optional. If provided, it must be a valid absolute path.
Response (201):
{
"session_id": "session-uuid"
}Error Cases:
-
400- Invalid request body or invalidcwd -
403- Terminal access required, interactive login required, or root assignment required -
502- Agent failed to open the terminal -
504- Agent did not respond in time
Attach to a terminal session event stream. Only one stream can attach to a session.
| Property | Value |
|---|---|
| Auth | Interactive access token, terminal access required |
| Response Type | text/event-stream |
SSE Data Events:
data: <base64-encoded-terminal-bytes>
SSE Exit Events:
event: exit
data: {"exit_code":0,"error":""}
The Hub closes the terminal session when the stream disconnects.
Error Cases:
-
404- Terminal session not found -
409- Stream already attached
Send terminal input to an active session.
| Property | Value |
|---|---|
| Auth | Interactive access token, terminal access required |
Request Body:
{
"data": "whoami\n"
}data is limited to 8192 bytes per request.
Response: 202 Accepted
Error Cases:
-
400- Invalid request body or input too large -
404- Terminal session not found -
409- Terminal session is closed -
502- Failed to send input to the agent
Resize an active terminal session.
| Property | Value |
|---|---|
| Auth | Interactive access token, terminal access required |
Request Body:
{
"cols": 120,
"rows": 32
}Response: 202 Accepted
Error Cases:
-
400- Invalid request body -
404- Terminal session not found -
409- Terminal session is closed -
502- Failed to send resize event to the agent
Close an active terminal session.
| Property | Value |
|---|---|
| Auth | Interactive access token, terminal access required |
Response: 204 No Content
Error Cases:
-
404- Terminal session not found
These two endpoints support the native SSH gateway (see SSH Gateway): issuing a short-lived user certificate and publishing the gateway's host identity for pinning. The gateway itself is a raw SSH listener on a separate port (default :2222), not an HTTP endpoint — these are the only two HTTP touchpoints for it.
Issue a short-lived SSH user certificate for the caller's own identity, for use against the SSH gateway.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), interactive session only — API tokens are refused (SC-004) |
| Rate Limited | Yes (10 requests / 60s per caller) |
Request Body:
{
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... client-key"
}public_key must be an authorized_keys-form ed25519, ECDSA, or RSA key (not a certificate). The principal on the issued certificate is always the caller's own username, resolved from the access token — it cannot be requested in the body.
Response (200):
{
"certificate": "ssh-ed25519-cert-v01@openssh.com AAAA...",
"principal": "alice",
"expires_at": "2026-08-06T20:14:03Z",
"host_key_fingerprint": "SHA256:AbCdEf...",
"gateway_port": 2222
}Error Cases:
-
400- Invalid or unsupported public key, or an existing certificate submitted as the subject key -
401- No or invalid access token -
403-{"error":"interactive login required"}— the credential is an API token, not an interactive session (auditedssh.cert_issue_refused) -
429- Rate limit exceeded -
503- SSH gateway disabled by configuration, or its key material is unavailable
Successful issuance is audited ssh.cert_issued with principal and TTL.
cURL:
curl -X POST https://hub.example.com/api/ssh/certificates \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{"public_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... client-key"}'Get the SSH gateway's host identity, for pinning instead of trust-on-first-use.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) — any authenticated caller, including API tokens; this is public trust material, not a capability |
| Rate Limited | No |
Response (200):
{
"fingerprint": "SHA256:AbCdEf...",
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... host-key",
"port": 2222
}Stable across Hub restarts and upgrades — the host key is never silently regenerated (FR-006).
Error Cases:
-
503- SSH gateway key material is unavailable
cURL:
curl https://hub.example.com/api/ssh/host-key \
-H 'Authorization: Bearer <access_token>'Veyport User Guide
Getting Started
Using Veyport
Administration
Setup & Deployment
- Architecture
- Deployment
- Deployment On-Prem
- Deployment AWS
- Deployment Azure
- Deployment GCP
- Proxy Configuration
- Development
- Dependency Track
- API Reference
Help