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
/auth/register PublicCreate 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
/auth/login PublicAuthenticate 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
/auth/logout AuthenticatedInvalidate the current session token.
Response (204): No content.
Get current user
/auth/me AuthenticatedReturns 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
/auth/verify PublicConfirm 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
/servers AuthenticatedRegister 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
/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": "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
/servers/:server_id AuthenticatedGet 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
/servers/:server_id AuthenticatedUpdate 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
/servers/:server_id?confirm=true AuthenticatedRemove 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
/servers/:server_id/rotate-key AuthenticatedIssue 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
/servers/:server_id/restore AuthenticatedRestore 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
/servers/restore-all AuthenticatedBulk-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
/ingest Collector keySubmit 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.coresis only present when per-core monitoring is enabled (Crucible 0.3.0+).thermalis hwmon-derived (Crucible 0.8.0+); preferred over IPMI for thecpu_temperature_highrule.dmipopulates the dashboard tile's hardware vendor/product line and IPMI badge (Crucible 0.8.0+).disks[].optionscontains mount options; used by thefilesystem_readonlyrule 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
/servers/:server_id/health AuthenticatedGet 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
/servers/:server_id/health/history AuthenticatedGet time-series metric data for a server.
Query parameters:
| Param | Type | Description |
|---|---|---|
metric | string | Metric name: cpu, memory, disk, network, temperature. |
from | ISO 8601 | Start time (default: 1 hour ago). |
to | ISO 8601 | End time (default: now). |
resolution | string | Data 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
/servers/:server_id/health/alerts AuthenticatedList active and historical alerts for a server.
Query parameters:
| Param | Type | Description |
|---|---|---|
status | string | active, resolved, or all (default: all). |
severity | string | Filter: critical, warning, info. |
from | ISO 8601 | Start time for history. |
to | ISO 8601 | End time for history. |
page | int | Page 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
/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
/channels AuthenticatedResponse (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
/channels/:channel_id AuthenticatedResponse (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
/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
/channels/:channel_id AuthenticatedResponse (200): { "success": true }.
Test channel
/channels/:channel_id/test AuthenticatedSend 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
/alerts/:alert_id/acknowledge AuthenticatedAcknowledge 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
/alerts/:alert_id/resolve AuthenticatedManually 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
/servers/:server_id/mutes AuthenticatedList 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
/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
/billing/status AuthenticatedReturns 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
/version PublicReturns 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):
| 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 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.