DOCS / API
API reference
Base URL: https://app.glassmkr.com/api/v1. All requests and responses use JSON over HTTPS.
Authenticated endpoints require a Bearer token:
Authorization: Bearer YOUR_API_TOKEN Every response with a status of 400 or above returns the same envelope:
{
"error": "stable_machine_code",
"message": "Human explanation. Do not parse this.",
"request_id": "matches the X-Request-Id header",
"documentation_url": "https://glassmkr.com/docs/api/errors#stable_machine_code",
"retryable": false,
"retry_after_seconds": null,
"details": []
} Branch on error, never on message. This holds for the whole /api/ namespace, including a 404 for an unknown path and a 405 for the wrong
verb. The full code list with retry guidance is at API errors.
For programmatic-API specifics (account keys, scopes, audit log) see the Programmatic API page. Nothing in the API is gated: every account key with the right scope reaches the full surface.
#Authentication
Register
/auth/register PublicCreate a new Glassmkr account.
Request body:
{
"email": "[email protected]",
"password": "<at least 12 characters>",
"name": "Jane Doe"
} Response (201):
{
"user": {
"id": "usr_a1b2c3d4",
"email": "[email protected]",
"name": "Jane Doe",
"verified": false,
"created_at": "2026-04-05T10:00:00Z"
},
"token": "<session JWT>"
} A verification email is sent automatically. The account is fully functional before verification, but some features (team invites) require a verified email.
Login
/auth/login PublicAuthenticate and receive a session token.
{
"email": "[email protected]",
"password": "<at least 12 characters>"
} Response (200):
{
"token": "<session JWT>",
"expires_at": "2026-04-12T10:00:00Z"
} Session tokens are valid for 7 days. Error (401) on bad credentials: { "error": "invalid_credentials", "message": "Email or password is incorrect." }.
Logout
/auth/logout AuthenticatedInvalidate the current session token. Response (204) no content.
Get current user
/auth/me AuthenticatedReturns the authenticated user's profile.
{
"id": "usr_a1b2c3d4",
"email": "[email protected]",
"name": "Jane Doe",
"verified": true,
"role": "owner",
"created_at": "2026-04-05T10:00:00Z",
"servers_count": 6,
"plan": "free"
}Verify email
/auth/verify PublicConfirm an email address using the token from the verification email.
{ "token": "<verification token>" }#Servers
Register server
/servers AuthenticatedRegister a new server. Returns a fresh collector key.
Request body: name, hostname, tags, and profile are accepted. Hardware fields (OS, architecture, core count, RAM) are reported by the agent on each ingest, never on registration.
{
"name": "web-prod-01",
"hostname": "web-prod-01.example.com",
"tags": ["production", "web"]
} name is optional (1-100 chars); when omitted, a placeholder label is assigned and superseded once the agent reports the real hostname on its first snapshot. hostname defaults to name when set and must be a valid RFC 1035 hostname. tags is optional, max 20 strings of 1-50 chars each. profile is optional: a host-type profile that suppresses the alerts expected by design for that kind of host. The field is named profile (not host_type); accepted values are null (the default "General", no suppression) or "marketplace_gpu" (a rented marketplace GPU box, which silences no_firewall, unattended_upgrades_disabled, and gpu_power_cap_throttling). An unknown value returns 400 validation_failed with profile must be null or one of: .... Other fields are silently dropped (mass-assignment defense).
Response (201):
{
"success": true,
"server": {
"id": "srv_a1b2c3d4",
"name": "web-prod-01",
"hostname": "web-prod-01.example.com",
"tags": ["production", "web"],
"api_key": "gmk_cru_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx_aBcD"
},
"ingest_url": "https://app.glassmkr.com/api/v1/ingest",
"message": "Save your collector key. It will not be shown again."
} The collector key is shown once. Configure it on the agent before the dashboard tile leaves "pending first snapshot". The Idempotency-Key header is supported (24h replay window).
List servers
/servers AuthenticatedList all servers in the account.
Query parameters:
| Param | Type | Description |
|---|---|---|
tag | string | Filter by tag. Repeat for multiple tags (AND logic). |
limit | int | Page size, 1-100 (default 100). |
cursor | string | Opaque pagination cursor returned as next_cursor on the previous page. |
Response (200):
{
"servers": [
{
"id": "srv_a1b2c3d4",
"name": "web-prod-01",
"hostname": "web-prod-01.example.com",
"ip": "10.0.1.42",
"os_type": "ubuntu",
"os_version": "24.04 LTS",
"status": "active",
"suspended_at": null,
"suspended_reason": null,
"last_seen_at": "2026-05-09T07:00:00Z",
"collector_version": "1.2.3",
"active_alerts": 0,
"disk_health_rollup": "healthy",
"created_at": "2026-04-05T10:00:00Z",
"tags": ["production", "web"],
"dmi_vendor": "GIGABYTE",
"dmi_product": "R292-4S1-00",
"ipmi_sensors_count": 106
}
],
"next_cursor": null
} Per-snapshot metrics (CPU usage, RAM usage, disk usage) are not on the list endpoint. Use GET /servers/:id/health for the latest snapshot from a specific server.
status is active for normal operation, suspended when the server is disabled (a historical billing state; see Billing). disk_health_rollup is the worst per-drive state across all SMART-monitored drives: healthy, declining, failing, or broken.
Get server
/servers/:server_id AuthenticatedGet full details for a single server. Same shape as the list endpoint plus a few read-only fields (config_overrides, free_analysis_used).
Update server
/servers/:server_id AuthenticatedUpdate name, tags, or profile (same values as on registration; send "profile": null to clear it). hostname is intentionally not updatable so ops can find a box by hostname after a rename.
{
"name": "web-prod-renamed",
"tags": ["production", "web", "fra1"]
}Delete server
/servers/:server_id?confirm=true AuthenticatedMove a server to trash. It stops collecting and drops out of your node count, and you
restore it with POST /servers/{id}/restore. ?confirm=true is required; a bare DELETE returns 400. The response states permanent: false and restorable: true so a client does not
have to infer the behaviour from the verb.
Every interface agrees. The MCP tool glassmkr.admin.delete_server and the dashboard's delete button do exactly
this. Until 2026-08-28 this endpoint destroyed the row while the MCP tool moved it to
trash, which meant an agent that learned the restorable behaviour from one lost data
using the other. Permanent removal is now a separate operation, below.
Purge a trashed server
/trashed-servers/{id} Account keyPermanent and irreversible. Destroys the server row and its stored metrics. Four conditions, all required:
- The server must already be in the trash. You cannot purge a live server in one call; a still-active server returns 409
not_trashed. ?confirm=true.- Recent re-authentication via
POST /account/verify-password, so a leaked key alone is not enough. - An account key holding the
servers:purgecapability. It is opt-in at key creation and is not granted by admin scope, so no key that existed before this shipped can purge. Without it: 403missing_capability.
Deliberately absent from MCP. An agent has no path to permanent destruction at all.
Per-endpoint sub-limit: 100 deletes/hour/account.
Rotate collector key
/servers/:server_id/rotate-key AuthenticatedIssue a fresh collector key for an existing server. The previous key stops working immediately. Update /etc/glassmkr/crucible.yaml (legacy installs: /etc/glassmkr/collector.yaml; the agent reads either) on the agent host and restart the service before the next ingest cycle to avoid a gap.
{
"success": true,
"server": { "id": "srv_a1b2c3d4" },
"collector_key": "gmk_cru_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx_aBcD",
"rotated_at": "2026-05-09T07:30:00Z",
"message": "Save this collector key. It will not be shown again."
} Rate-limited to 10/hour/account. Note: the field name on this endpoint is collector_key, not api_key as on POST /servers.
Restore server
/servers/:server_id/restore AuthenticatedRestore a single suspended server. Restoring requires nothing beyond the call itself.
Restore all suspended servers
/servers/restore-all AuthenticatedBulk-restore every server suspended for no_card_on_file (a historical state). Used by the dashboard's Settings → Disabled servers → Restore all button.
#Ingest
Push snapshot
/ingest Collector keySubmit a Crucible snapshot. Called by the agent every collection interval (default 300 seconds). Authenticated by the collector key in the Authorization: Bearer gmk_cru_live_... header. Rate-limited to one ingest per server per 55 seconds; subsequent calls return 429.
Request body (abbreviated; the agent emits the full Snapshot type):
{
"system": { "hostname": "web-prod-01", "ip": "10.0.1.42",
"os": "Ubuntu 24.04 LTS", "os_id": "ubuntu",
"kernel": "6.8.0-31-generic", "uptime_seconds": 86400 },
"cpu": { "user_percent": 15.2, "system_percent": 5.3,
"iowait_percent": 1.1, "idle_percent": 78.4,
"load_1m": 0.4, "load_5m": 0.6, "load_15m": 0.5,
"cores": [{ "core": 0, "user_percent": 20.1, "system_percent": 4.2,
"iowait_percent": 0.5, "idle_percent": 75.2 }] },
"memory": { "total_mb": 65536, "used_mb": 44032,
"available_mb": 21504, "swap_total_mb": 8192,
"swap_used_mb": 0 },
"disks": [{ "device": "/dev/nvme0n1p2", "mount": "/",
"total_gb": 500, "used_gb": 225, "available_gb": 250,
"percent_used": 47, "fstype": "ext4",
"io_read_mb_s": 15.2, "io_write_mb_s": 3.8,
"latency_p99_ms": 0.4,
"inodes_total": 32768000, "inodes_used": 1245000 }],
"smart": [{ "device": "/dev/nvme0n1", "model": "Samsung 990 Pro 2TB",
"health": "PASSED", "temperature_c": 38,
"percentage_used": 12, "power_on_hours": 8760 }],
"network": [{ "interface": "eth0", "speed_mbps": 10000,
"rx_bytes_sec": 125000, "tx_bytes_sec": 42000,
"rx_errors": 0, "tx_errors": 0,
"rx_drops": 0, "tx_drops": 0 }],
"raid": [],
"ipmi": { "available": true, "sel_entries_count": 12,
"ecc_errors": { "correctable": 0, "uncorrectable": 0 },
"sensors": [{ "name": "CPU1_TEMP", "value": 52, "unit": "C",
"status": "ok", "type": "temperature",
"upper_critical": 90 }] },
"os_alerts": { "oom_kills_recent": 0, "zombie_processes": 0,
"time_drift_ms": 0 },
"thermal": { "available": true, "source": "hwmon coretemp Package id 0",
"max_cpu_celsius": 52,
"cpu_readings": [{ "chip": "coretemp-isa-0000",
"label": "Package id 0", "celsius": 52 }] },
"dmi": { "available": true, "vendor": "supermicro",
"raw_vendor": "Supermicro Inc.",
"product_name": "SYS-1029P-WTR",
"bios_version": "3.4", "bios_date": "2023-01-12",
"is_virtual": false },
"gpu": { "available": true, "tier": "nvidia-smi",
"devices": [{ "index": 0, "name": "NVIDIA L4",
"temperature_c": 48, "utilization_percent": 12,
"memory_used_mb": 4096, "memory_total_mb": 24564 }] },
"collector_version": "1.2.3",
"timestamp": "2026-05-22T07:00:00Z"
} Optional top-level blocks: security, zfs, io_errors, io_latency, conntrack, systemd, ntp, file_descriptors, thermal, dmi, gpu, expected_reboot. Unknown fields are accepted via passthrough; new collector versions can extend the schema without a coupled Dashboard release.
Response (200):
{
"success": true,
"received_at": "2026-05-22T07:00:00.123Z",
"new_alerts": 0,
"active_alerts": 0
}#Health
Get server health
/servers/:server_id/health AuthenticatedCurrent health status and latest metric values for a server.
{
"server_id": "srv_a1b2c3d4",
"status": "healthy",
"last_seen": "2026-04-05T10:05:00Z",
"current": {
"cpu_percent": 21.6,
"ram_percent": 67.2,
"swap_used_mb": 0,
"disk_max_percent": 45.0,
"network_rx_mbps": 120.5,
"network_tx_mbps": 40.2,
"cpu_temp_c": 52,
"active_alerts": 0
}
}Get health history
/servers/:server_id/health/history AuthenticatedTime-series metric data.
| Param | Type | Description |
|---|---|---|
metric | string | cpu, memory, disk, network, temperature. |
from | ISO 8601 | Start time (default 1 hour ago). |
to | ISO 8601 | End time (default now). |
resolution | string | 1m, 5m, 1h, 1d (auto if omitted). |
Get server alerts
/servers/:server_id/alerts AuthenticatedDefaults to status=all, which includes resolved history; pass ?status=active for only the alerts currently firing. Query params: status (active, resolved, all; default all), severity (critical, warning, info), from, to, page.
#Channels
Create channel
/channels Write scopeCreate a notification channel. Supported types: email, telegram, slack, discord, pagerduty, webhook.
{
"name": "ops-telegram",
"channel_type": "telegram",
"config": {
"bot_token": "7123456789:AAH1bGciOiJSUzI1NiIs",
"chat_id": "-1001234567890"
}
} Email config takes recipients; Slack and Discord take webhook_url; PagerDuty takes routing_key; webhook takes url and optional secret.
List channels
/channels AuthenticatedGet channel
/channels/:channel_id AuthenticatedSensitive fields like bot tokens are partially masked in GET responses.
Update channel
/channels/:channel_id Write scopeDelete channel
/channels/:channel_id Write scopeTest channel
/channels/:channel_id/test Write scopeSend a test notification through the channel. Always 200, with { success: boolean, error?: string }. A delivery failure is a result, not an API error: the test ran, and what it found out was that the
channel did not accept the message. Read success; when it is false, error carries the upstream reason. An earlier version of
this page documented a 502 here, which the endpoint has never returned.
#Alerts
Acknowledge alert
/alerts/:alert_id/acknowledge Write scopeSilence notifications for the current occurrence; does not disable the rule. Event-type alerts (e.g. unexpected_reboot) auto-clear acknowledgement when a new occurrence stacks onto the card.
Resolve alert
/alerts/:alert_id/resolve Write scopeManually resolve an alert without waiting for the underlying condition to clear. Mostly used for event-type alerts (24-hour TTL otherwise) and for force-clearing stuck state alerts.
List muted rules
/servers/:server_id/mutes AuthenticatedMute a rule
/servers/:server_id/mutes Write scopeMute one alert rule for this server, one rule per request. Any currently active alert of that type is resolved immediately. Returns the updated muted_rules list.
{ "alert_type": "disk_space_high" }Unmute a rule
/servers/:server_id/mutes Write scopeRemove one rule from the muted list, one rule per request. Returns the updated muted_rules list.
{ "alert_type": "disk_space_high" }#Billing
Historical. As of August 2026 Glassmkr no longer charges for anything: both the hosted service and a self-hosted instance are free, and no server is suspended for payment reasons. The billing endpoints remain routable so existing integrations do not break, but no account carries a paid subscription. See the billing policy.
Billing status
/billing/status AuthenticatedReturns the account's billing record: plan, billing-period bounds, payment-method state, and the count of servers disabled for a missing card. Since August 2026 these fields describe historical subscription state where one existed.
Other billing endpoints
POST /billing/checkout is retired and answers 410 to every call: no new subscription can be created. POST /billing/portal, POST /billing/resume and POST /billing/downgrade remain for accounts with a residual legacy subscription to manage or cancel it.
#Meta
Version
/version PublicReturns the latest published Crucible version and the minimum supported version.
{
"crucible": {
"latest": "0.13.3",
"min_supported": "0.7.0",
"changelog_url": "https://github.com/glassmkr/crucible/releases"
},
"dashboard": { "version": "1.0.0" }
} The latest value is sourced from the npm registry's @glassmkr/crucible latest dist-tag.
#Rate limits
Token-bucket limiter applied as four overlapping tiers (first failure wins; failures still cost a token on the per-IP debit so brute-force probing burns budget):
| Tier | Capacity | Refill | Applies to |
|---|---|---|---|
| Per-IP | 100 | 10/sec | Every request, including pre-auth. |
| Per-key | 1000 | 100/sec | Authenticated requests, scoped to one collector or account key. |
| Per-account | 5000 | 500/sec | All authenticated requests within one customer. |
| POST /servers | 100 | 100/hour | Server registration sub-limit. |
| DELETE /servers/:id | 100 | 100/hour | Deletion sub-limit. |
| POST /servers/:id/rotate-key | 10 | 10/hour | Key-rotation sub-limit. |
The ingest endpoint enforces a per-server soft limit of one push per 55 seconds (returns 429, separate from the token-bucket layer). When token-bucket-rate-limited, the API returns 429 Too Many Requests with a Retry-After header.
#Pagination
List endpoints (currently GET /servers) use opaque cursor pagination: pass ?limit= (1-100, default 100) and the previous response's next_cursor as ?cursor=. next_cursor is null on the final page.
#Idempotency
POST /servers honors an Idempotency-Key header (1-255 printable ASCII). The first response (success or deterministic 4xx) is cached for 24 hours; replays return the cached response with an Idempotency-Replayed: true header. Concurrent retries with the same key while the original is still in flight return 409.
Last verified: 2026-05-22 against Crucible v0.13.3 and Dashboard v1.0.