API Reference

Base URL: https://app.glassmkr.com/api/v1

All requests and responses use JSON. Authenticated endpoints require a Bearer token in the Authorization header:

Authorization: Bearer YOUR_API_TOKEN

Error responses follow a consistent format:

{
  "error": "short_code",
  "message": "Human-readable description of what went wrong."
}

Table of contents

  • Authentication - register, login, logout, me, verify
  • Servers - register, list, get, update, delete, rotate-key, restore, restore-all
  • Ingest - push metrics
  • Health - health status, history, alerts
  • Channels - CRUD + test
  • Alerts - acknowledge, resolve, mutes
  • Billing - status, checkout, portal, resume, downgrade
  • Meta - version

For a deeper dive on authentication tokens (collector keys, account keys, scopes, idempotency, rate-limit tiers, audit log), see the Programmatic API page. For how passwords, sessions, and keys are stored and protected, see the Security posture page. For which endpoints are Free vs Pro and how 402 responses behave, see the Tier gating page.

Authentication

Register

POST /auth/register Public

Create a new Dashboard account.

Request body:

{
  "email": "[email protected]",
  "password": "min-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": "eyJhbGciOiJIUzI1NiIs..."
}

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.

Request body:

{
  "email": "[email protected]",
  "password": "min-12-characters"
}

Response (200):

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_at": "2026-04-12T10:00:00Z"
}

Tokens are valid for 7 days. Use the token in the Authorization header for all authenticated requests.

Error (401):

{
  "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.

Response (200):

{
  "id": "usr_a1b2c3d4",
  "email": "[email protected]",
  "name": "Jane Doe",
  "verified": true,
  "role": "owner",
  "created_at": "2026-04-05T10:00:00Z",
  "servers_count": 6,
  "plan": "pro"
}

Verify email

POST /auth/verify Public

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

Request body:

{
  "token": "verify_abc123def456"
}

Response (200):

{
  "message": "Email verified successfully."
}

Error (400):

{
  "error": "invalid_token",
  "message": "Verification token is invalid or has expired."
}

Servers

Register server

POST /servers Authenticated

Register a new server with Dashboard. Creates a fresh collector key. This is done via the dashboard ("+ Add Server") or programmatically with this endpoint.

Request body: only name, hostname, and tags 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 required (1-100 chars). hostname defaults to name when absent and must be a valid RFC 1035 hostname. tags is optional, max 20 strings of 1-50 chars each. Other fields are silently dropped (mass-assignment defence).

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. Set it as the `dashboard.api_key` field in /etc/glassmkr/collector.yaml on your agent host."
}

The collector key is shown once. Configure it on the agent before the dashboard tile leaves the "pending first snapshot" state. The Idempotency-Key header is supported (Stripe-style; 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": "0.9.1",
      "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 (see Billing for the suspended-no-card-on-file flow). disk_health_rollup is the worst per-drive state across all SMART-monitored drives: healthy, declining, failing, or broken. dmi_vendor / dmi_product / ipmi_sensors_count come from the most recent snapshot's DMI and IPMI blocks (Crucible 0.8.0+).

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.

Response (200):

{
  "server": {
    "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": "0.9.1",
    "config_overrides": {},
    "free_analysis_used": false,
    "active_alerts": 0,
    "created_at": "2026-04-05T10:00:00Z",
    "tags": ["production", "web"],
    "dmi_vendor": "GIGABYTE",
    "dmi_product": "R292-4S1-00",
    "ipmi_sensors_count": 106
  }
}

config_overrides is the per-server alert-threshold override map (set from the server's Settings page). free_analysis_used indicates whether this server has consumed its one free AI analysis (Free plan only).

Update server

PATCH /servers/:server_id Authenticated

Update name or tags on an existing server. hostname is intentionally not updatable so ops can find a box by hostname even after a rename.

Request body (any subset of):

{
  "name": "web-prod-renamed",
  "tags": ["production", "web", "fra1"]
}

Response (200):

{
  "server": {
    "id": "srv_a1b2c3d4",
    "name": "web-prod-renamed",
    "hostname": "web-prod-01.example.com",
    "tags": ["production", "web", "fra1"]
  }
}

Delete server

DELETE /servers/:server_id?confirm=true Authenticated

Remove a server and all its stored metrics. This action is irreversible. ?confirm=true is required; a bare DELETE returns 400.

Response (200):

{
  "success": true,
  "deleted": "web-prod-01"
}

Error (400):

{
  "error": "Pass ?confirm=true to delete. This removes all data for this server."
}

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/collector.yaml on the agent host and restart the service before the next ingest cycle to avoid a gap.

Response (200):

{
  "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."
}

Note: the field name on this endpoint is collector_key, not api_key as on POST /servers. This naming inconsistency is tracked for cleanup; document parsers should accept either path. Rate-limited to 10/hour/account.

Restore server

POST /servers/:server_id/restore Authenticated

Restore a single suspended server. Used when a server was disabled because no payment method was on file (status suspended with reason no_card_on_file). The customer must have a card on file at the time of the call, unless their account is exempt from billing enforcement (staff/internal accounts).

Response (200):

{
  "success": true,
  "server": {
    "id": "srv_a1b2c3d4",
    "status": "active",
    "suspended_at": null,
    "suspended_reason": null
  }
}

Error (400) when no card is on file:

{
  "error": "no_card_on_file",
  "message": "Add a payment method in Settings to restore this server."
}

Restore all suspended servers

POST /servers/restore-all Authenticated

Bulk-restore every server suspended for no_card_on_file. Used by the dashboard's "Settings → Disabled servers → Restore all" button. Same payment-method prerequisite as the single-server restore.

Response (200):

{
  "success": true,
  "restored": [
    { "id": "srv_a1b2c3d4", "name": "web-prod-01" },
    { "id": "srv_e5f6g7h8", "name": "web-prod-02" },
    { "id": "srv_i9j0k1l2", "name": "web-prod-03" }
  ],
  "count": 3,
  "message": "3 servers restored."
}

Ingest

Push snapshot

POST /ingest Collector key

Submit a Crucible snapshot. Called by the agent on every collection interval (default 300 s). 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 },
  "collector_version": "0.9.1",
  "timestamp": "2026-05-09T07:00:00Z"
}

Optional top-level blocks (omitted by older agents): security, zfs, io_errors, io_latency, conntrack, systemd, ntp, file_descriptors, thermal, dmi, expected_reboot. Dashboard accepts unknown fields via passthrough; new collector versions can extend the schema without a Dashboard deploy in lockstep. The canonical Zod schema lives at apps/dashboard/src/lib/server/ingest/snapshot-schema.ts.

Response (200):

{
  "success": true,
  "received_at": "2026-05-09T07:00:00.123Z",
  "new_alerts": 0,
  "active_alerts": 0
}

new_alerts is the count of alert types that fired for the first time in this snapshot. active_alerts is the total currently unresolved across all rules.

Notes on the payload:

  • cpu.cores is only present when per-core monitoring is enabled (Crucible 0.3.0+).
  • thermal is hwmon-derived (Crucible 0.8.0+); preferred over IPMI for the cpu_temperature_high rule.
  • dmi populates the dashboard tile's hardware vendor/product line and IPMI badge (Crucible 0.8.0+).
  • disks[].options contains mount options; used by the filesystem_readonly rule to detect read-only remounts.
  • ipmi.ecc_errors_from_sel (optional) covers Dell iDRAC and HPE iLO firmwares that report ECC only via SEL.

Health

Get server health

GET /servers/:server_id/health Authenticated

Get the current health status and latest metric values for a server.

Response (200):

{
  "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

Get time-series metric data for a server.

Query parameters:

ParamTypeDescription
metricstringMetric name: cpu, memory, disk, network, temperature.
fromISO 8601Start time (default: 1 hour ago).
toISO 8601End time (default: now).
resolutionstringData point interval: 1m, 5m, 1h, 1d (auto-selected if omitted).

Response (200):

{
  "server_id": "srv_a1b2c3d4",
  "metric": "cpu",
  "from": "2026-04-05T09:00:00Z",
  "to": "2026-04-05T10:00:00Z",
  "resolution": "1m",
  "data": [
    {
      "timestamp": "2026-04-05T09:00:00Z",
      "user": 14.2,
      "system": 5.1,
      "iowait": 0.8,
      "idle": 79.9,
      "steal": 0.0
    }
  ]
}

Get server alerts

GET /servers/:server_id/health/alerts Authenticated

List active and historical alerts for a server.

Query parameters:

ParamTypeDescription
statusstringactive, resolved, or all (default: all).
severitystringFilter: critical, warning, info.
fromISO 8601Start time for history.
toISO 8601End time for history.
pageintPage number (default: 1).

Response (200):

{
  "alerts": [
    {
      "id": "alt_x1y2z3",
      "rule": "ram_high",
      "severity": "warning",
      "status": "active",
      "value": 92.3,
      "threshold": 90,
      "message": "RAM usage is 92.3% (threshold: 90%)",
      "triggered_at": "2026-04-05T09:47:00Z",
      "resolved_at": null,
      "acknowledged": false,
      "acknowledged_by": null
    }
  ],
  "total": 1,
  "page": 1
}

Channels

Create channel

POST /channels Authenticated (Admin)

Create a new notification channel.

Request body (Telegram example):

{
  "name": "ops-telegram",
  "type": "telegram",
  "config": {
    "bot_token": "7123456789:AAH1bGciOiJSUzI1NiIs",
    "chat_id": "-1001234567890"
  }
}

Request body (Email example):

{
  "name": "ops-email",
  "type": "email",
  "config": {
    "recipients": ["[email protected]", "[email protected]"]
  }
}

Request body (Slack example):

{
  "name": "ops-slack",
  "type": "slack",
  "config": {
    "webhook_url": "https://hooks.slack.com/services/T00/B00/XXX"
  }
}

Response (201):

{
  "id": "ch_m1n2o3",
  "name": "ops-telegram",
  "type": "telegram",
  "created_at": "2026-04-05T10:00:00Z"
}

List channels

GET /channels Authenticated

Response (200):

{
  "channels": [
    {
      "id": "ch_m1n2o3",
      "name": "ops-telegram",
      "type": "telegram",
      "created_at": "2026-04-05T10:00:00Z",
      "last_used": "2026-04-05T10:05:00Z"
    }
  ]
}

Get channel

GET /channels/:channel_id Authenticated

Response (200):

{
  "id": "ch_m1n2o3",
  "name": "ops-telegram",
  "type": "telegram",
  "config": {
    "bot_token": "712345****",
    "chat_id": "-1001234567890"
  },
  "created_at": "2026-04-05T10:00:00Z",
  "last_used": "2026-04-05T10:05:00Z"
}

Note: sensitive fields like bot tokens are partially masked in GET responses.

Update channel

PUT /channels/:channel_id Authenticated (Admin)

Update a channel's name or configuration. Send only the fields you want to change.

Request body:

{
  "name": "ops-telegram-renamed",
  "config": {
    "chat_id": "-1009876543210"
  }
}

Response (200): Updated channel object.

Delete channel

DELETE /channels/:channel_id Authenticated

Response (200): { "success": true }.

Test channel

POST /channels/:channel_id/test Authenticated

Send a test notification through the channel. The test message includes a timestamp and the channel name.

Response (200):

{
  "success": true,
  "message": "Test notification sent to ops-telegram."
}

Error (502):

{
  "success": false,
  "error": "delivery_failed",
  "message": "Telegram API returned 401: Unauthorized. Check your bot token."
}

Alerts

Acknowledge alert

POST /alerts/:alert_id/acknowledge Authenticated

Acknowledge an active alert. This silences notifications for the current occurrence but does not disable the alert rule. Event-type alerts (e.g. unexpected_reboot) auto-clear acknowledgement when a new occurrence stacks onto the card so the next push gets dispatched.

Request body: none.

Response (200):

{
  "success": true,
  "alert": {
    "id": "alt_x1y2z3",
    "alert_type": "cpu_iowait_high",
    "acknowledged": true
  }
}

Resolve alert

POST /alerts/:alert_id/resolve Authenticated

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.

Response (200):

{
  "success": true,
  "alert": { "id": "alt_x1y2z3", "alert_type": "cpu_iowait_high" }
}

List muted rules

GET /servers/:server_id/mutes Authenticated

List all muted alert rules for a server.

Response (200):

{
  "server_id": "srv_a1b2c3d4",
  "muted_rules": ["disk_space_high", "cpu_iowait_high"],
  "updated_at": "2026-04-05T10:00:00Z"
}

Update muted rules

PUT /servers/:server_id/mutes Authenticated (Admin)

Set the list of muted alert rules for a server. This replaces the entire muted list.

Request body:

{
  "muted_rules": ["disk_space_high", "cpu_iowait_high"]
}

Response (200):

{
  "server_id": "srv_a1b2c3d4",
  "muted_rules": ["disk_space_high", "cpu_iowait_high"],
  "updated_at": "2026-04-05T10:10:00Z"
}

Changes take effect on the next ingest cycle from the server. Pass an empty array to unmute all rules.

Billing

Billing status

GET /billing/status Authenticated

Returns the customer's plan, billing-period bounds, payment-method state, and the count of servers currently disabled for missing-card. Drives the dashboard's plan banner and dunning UI.

Response (200):

{
  "plan": "pro",
  "has_default_payment_method": true,
  "current_period_end": "2026-06-09T00:00:00Z",
  "cancel_at_period_end": false,
  "billing_enforcement_exempt": false,
  "active_server_count": 7,
  "suspended_no_card_count": 0,
  "free_server_quota": 3
}

Pro customers without a payment method see has_default_payment_method: false. The grace period is the later of current_period_end and customer.created_at + 30 days; after that, servers beyond the free quota are suspended (status suspended, reason no_card_on_file) until a card is added and they're restored.

Other billing endpoints

Stripe-driven flows that the dashboard wires up; programmatic callers should use these only when scripting. All return JSON.

  • POST /billing/checkout — create a Stripe Checkout session for plan upgrade.
  • POST /billing/portal — create a Stripe Customer Portal session.
  • POST /billing/resume — re-enable auto-renew on a cancelled subscription.
  • POST /billing/downgrade — schedule a downgrade to the Free plan at period end.

Meta

Version

GET /version Public

Returns the latest published Crucible version and the minimum supported version. Crucible agents poll this to surface "update available" hints; the Dashboard dashboard uses it to decide which tiles get the "Update to X" chip.

Response (200):

{
  "crucible": {
    "latest": "0.9.1",
    "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 (auto-synced on a short cache); pinning a fallback in code happens only after a publish.

Rate limits

The API uses a token-bucket limiter applied as four overlapping tiers (first failure wins; failures still cost a token on subsequent tiers via 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 also enforces a per-server soft limit of one push per 55 seconds (returns 429 with a static body, 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 honours 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.