View as Markdown

#The two key types

Key typePrefixUsed forCreated via
Account API keygmk_acct_live_Management endpoints: list servers, create servers, rotate keys, query audit log.POST /api/v1/account/keys via curl / Ansible / Terraform after recent re-authentication via POST /api/v1/account/verify-password. Available on every account.
Collector keygmk_cru_live_Telemetry ingestion only. Scoped to one server. Cannot list other servers or read account settings.Returned by POST /api/v1/servers. Rotate via POST /api/v1/servers/{id}/rotate-key.

Older agents may still have col_* collector keys. Both formats authenticate against /api/v1/ingest; you can rotate at your own pace.

#Quickstart: provision 50 servers in two minutes

1. Create an account API key

Two calls. Use a logged-in browser session (cookie jar). Account-key creation cannot use another account key.

# 1. Re-authenticate (opens a 5-minute step-up window).
curl -sS -X POST https://app.glassmkr.com/api/v1/account/verify-password \
  -b session.cookie \
  -H "Content-Type: application/json" \
  -d '{"password":"your-dashboard-password"}'

# 2. Create the key (returned in plaintext exactly once; save it).
#    scope must be "write" to create servers ("write" is the default).
curl -sS -X POST https://app.glassmkr.com/api/v1/account/keys \
  -b session.cookie \
  -H "Content-Type: application/json" \
  -d '{"name":"ansible-prod","scope":"write"}'

On the hosted service, server creation is capped at the 10-node per-account cap, so a 50-server run assumes a self-hosted instance (no node limits).

2. Create the servers via curl

API_KEY="gmk_acct_live_..."

for i in $(seq 1 50); do
  RESPONSE=$(curl -sS -X POST https://app.glassmkr.com/api/v1/servers \
    -H "Authorization: Bearer $API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: bootstrap-$(date +%s)-$i" \
    -d '{"name":"web-'$i'","hostname":"web-'$i'.prod.example.com","tags":["prod","web"]}')

  COLLECTOR_KEY=$(echo "$RESPONSE" | jq -r '.server.api_key')
  echo "web-$i  =>  $COLLECTOR_KEY"
done

3. Provision the agent on each host

curl -sf https://glassmkr.com/install.sh | sudo GLASSMKR_API_KEY=$COLLECTOR_KEY bash

#Route alerts in the same run

Notification channels are managed by the same account key: create, update, test and delete over the API. Six types: slack, discord, pagerduty, telegram, email, webhook. A provisioning pipeline can register a server and wire its alerts to the right destination in the same script:

# Create the channel (write scope). Slack and Discord take a webhook_url;
# PagerDuty takes a routing_key; email takes an email address; generic webhook takes a webhook_url.
curl -sS -X POST https://app.glassmkr.com/api/v1/channels \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channel_type":"slack","name":"prod-alerts","config":{"webhook_url":"https://hooks.slack.com/services/..."}}'
# -> the new channel id comes back in .channel.id

# Prove delivery end to end before you rely on it. The test endpoint returns
# 200 with {"success":false} when delivery fails, so assert on .success.
curl -sS -X POST "https://app.glassmkr.com/api/v1/channels/$CHANNEL_ID/test" \
  -H "Authorization: Bearer $API_KEY" | jq -e '.success' >/dev/null \
  || { echo "channel test failed"; exit 1; }

Channels apply account-wide and can be filtered per channel by alert priority (P1 to P4). Full lifecycle reference (including PUT and DELETE): API reference: channels.

#Rate limits

Three default tiers (per-IP, per-key, per-account) plus per-endpoint sub-limits for the high-risk operations:

TierCapacityRefill
Per-IP100 burst10/sec
Per-key1000 burst100/sec
Per-account5000 burst500/sec
POST /servers100 / hour / accountn/a
DELETE /servers/{id}100 / hour / accountn/a
POST /servers/{id}/rotate-key10 / hour / accountn/a
POST /account/keys10 / hour / accountn/a
POST /account/keys/{id}/rotate10 / hour / accountn/a

On 429, the response body includes tier and retry_after_seconds; the Retry-After header is set.

#Idempotency

POST /api/v1/servers accepts an Idempotency-Key header (Stripe-style). Retries within 24h with the same key return the cached response (status + body). Use one for every retried server-creation operation in your automation.

#Step-up authentication

API key creation and rotation require recent password re-verification. POST your current password to /api/v1/account/verify-password (session auth, not bearer-token) to stamp last_password_verified_at. After that, sensitive operations succeed for 5 minutes.

This protects against session-stealing attacks: an attacker with a leaked cookie cannot mint a long-lived API key without also knowing the password.

#Audit log

Every API call writes one row to the audit log. Read it via GET /api/v1/account/audit:

  • Paginated by ts cursor (?limit=50&cursor=...)
  • Filterable by key_id, resource_type, resource_id, action, result
  • Retention: 365 days of alert history in both deployment forms (a ClickHouse table TTL; self-hosted operators can change it)
  • Append-only: we cannot edit history server-side

#Securing your keys

  • Store in your secret manager (1Password, Vault, AWS Secrets Manager, Doppler). Never in .env committed to git.
  • Use a separate key per integration (Ansible, CI, Terraform). Revoking one does not disrupt the others.
  • Set an expires_at on short-lived CI keys.
  • If a key leaks: revoke immediately at DELETE /api/v1/account/keys/{id} or via the dashboard.
  • GitHub secret-scanning partner registration for gmk_acct_live_ and gmk_cru_live_ prefixes is queued; once active, accidentally-committed keys auto-revoke.

#Errors

Errors that carry a machine-readable code, which today means plan and quota refusals, rate limiting, and idempotency conflicts, return this envelope:

{
  "error": "machine_readable_code",
  "message": "Human-readable explanation",
  "documentation_url": "https://glassmkr.com/docs/programmatic-api#rate-limits"
}

Not every error looks like this, and code that parses our responses should not assume it does. An authentication failure returns {"message": "Authentication failed"} with no code. A method mismatch returns plain text. A path that does not exist returns an HTML error page rather than JSON. Branch on the HTTP status first and treat the body as a best-effort explanation.

Every response carries an x-request-id header. Quote that value when contacting support; we correlate it against the audit log and application logs. It is a header, not a body field.

Last verified: 2026-08-27 against the live API, by requesting each shape rather than by reading the handler.