View as Markdown

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

POST /auth/register Public

Create 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

POST /auth/login Public

Authenticate 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

POST /auth/logout Authenticated

Invalidate the current session token. Response (204) no content.

Get current user

GET /auth/me Authenticated

Returns 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

POST /auth/verify Public

Confirm an email address using the token from the verification email.

{ "token": "<verification token>" }

#Servers

Register server

POST /servers Authenticated

Register 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

GET /servers Authenticated

List all servers in the account.

Query parameters:

ParamTypeDescription
tagstringFilter by tag. Repeat for multiple tags (AND logic).
limitintPage size, 1-100 (default 100).
cursorstringOpaque 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

GET /servers/:server_id Authenticated

Get 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

PATCH /servers/:server_id Authenticated

Update 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

DELETE /servers/:server_id?confirm=true Authenticated

Move 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

DELETE /trashed-servers/{id} Account key

Permanent 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:purge capability. 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: 403 missing_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

POST /servers/:server_id/rotate-key Authenticated

Issue 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

POST /servers/:server_id/restore Authenticated

Restore a single suspended server. Restoring requires nothing beyond the call itself.

Restore all suspended servers

POST /servers/restore-all Authenticated

Bulk-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

POST /ingest Collector key

Submit 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

GET /servers/:server_id/health Authenticated

Current 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

GET /servers/:server_id/health/history Authenticated

Time-series metric data.

ParamTypeDescription
metricstringcpu, memory, disk, network, temperature.
fromISO 8601Start time (default 1 hour ago).
toISO 8601End time (default now).
resolutionstring1m, 5m, 1h, 1d (auto if omitted).

Get server alerts

GET /servers/:server_id/alerts Authenticated

Defaults 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

POST /channels Write scope

Create 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

GET /channels Authenticated

Get channel

GET /channels/:channel_id Authenticated

Sensitive fields like bot tokens are partially masked in GET responses.

Update channel

PUT /channels/:channel_id Write scope

Delete channel

DELETE /channels/:channel_id Write scope

Test channel

POST /channels/:channel_id/test Write scope

Send 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

POST /alerts/:alert_id/acknowledge Write scope

Silence 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

POST /alerts/:alert_id/resolve Write scope

Manually 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

GET /servers/:server_id/mutes Authenticated

Mute a rule

POST /servers/:server_id/mutes Write scope

Mute 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

DELETE /servers/:server_id/mutes Write scope

Remove 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

GET /billing/status Authenticated

Returns 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

GET /version Public

Returns 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):

TierCapacityRefillApplies to
Per-IP10010/secEvery request, including pre-auth.
Per-key1000100/secAuthenticated requests, scoped to one collector or account key.
Per-account5000500/secAll authenticated requests within one customer.
POST /servers100100/hourServer registration sub-limit.
DELETE /servers/:id100100/hourDeletion sub-limit.
POST /servers/:id/rotate-key1010/hourKey-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.