Documentation

API Access: Programmatic Data for Your Automation

Step-by-step guide Updated 24 August 2026

Stylo.Bot exposes a versioned REST API (/api/v1/*) so your own systems can pull the traffic intelligence the dashboard shows: per-domain summaries, fleet health, threat-feed advisories, and license entitlement, without a human logging in.

The API is read-mostly by design. Programmatic keys get reads plus management of their own keys. Config edits and policy changes stay behind the operator console.

Live docs

Browse the interactive API reference — every commercial control-plane endpoint on this page, schema and all, in a Scalar UI you can click through. It isn't wired to a live backend here (we don't run production API traffic against our own marketing site), so "Try it" calls need a server URL — point it at your own control plane or gateway and it calls the real thing.

Every install also serves its own interactive documentation built from its own live OpenAPI document, no server-URL edit needed since it's already talking to itself. Your key goes in the Authorize field.

  • Gateway installs: https://<your-gateway>/docs/api — the per-domain traffic surface plus, on the open-source build, the wider FOSS /api/v1 surface
  • Control plane installs (commercial): https://<your-control-plane>/docs/api — fleet, license, threat feed, and key management

Two products, two auth stories

Open source (self-hosted) Commercial (portal)
Header X-SB-Api-Key: <your-key> Authorization: Bearer sbk_...
Where the key comes from your install's config (BotDetection:ApiKeys) your portal account
Granularity one key, full read access to the surface it's configured for capability scopes per key, optionally domain-bound
Where to manage your own config the portal account area

The two never mix: a portal sbk_ key only works against a commercial install; a config-defined X-SB-Api-Key only works against the install that defines it.

Creating an API key (commercial)

  1. Log in to the portal with your account.
  2. Open Account -> API tokens (or your org's API keys page).
  3. Create a key: give it a name, pick the scopes it needs, and optionally bind it to specific domains.
  4. Copy the plaintext now. Keys are stored hashed. The value is shown exactly once and can never be recovered. If you lose it, rotate or revoke it from the same page.

Scopes

Scope What it grants
traffic:read per-domain traffic summaries, top bots, endpoint stats
fleet:read fleet gateways + fleet health + endpoint/detection-rate rollups
threats:read threat advisories + CVE fingerprints
license:read your entitlement (tier, features, domains, expiry)
keys:manage create, rotate, and revoke keys on your own account — never beyond your own scopes or domains

artifacts:read and artifacts:download are accepted at key creation but don't unlock anything yet; no artifact-download endpoints are live in this version.

A key with no scopes at all is an operator key (full access, no domain restriction) — that's how the platform's own tooling authenticates; portal-issued keys always carry an explicit scope list. Omit scopes when creating a key and it defaults to license:read only.

Calling the API

# Commercial: your entitlement
curl -H "Authorization: Bearer sbk_..." \
  "https://your-control-plane/api/v1/license"

# Commercial: what this key can do
curl -H "Authorization: Bearer sbk_..." \
  "https://your-control-plane/api/v1/me"

# Commercial: fleet health
curl -H "Authorization: Bearer sbk_..." \
  "https://your-control-plane/api/v1/fleet/health"

# Commercial: threat advisories, newest first, paged
curl -H "Authorization: Bearer sbk_..." \
  "https://your-control-plane/api/v1/threats/advisories?limit=50"

# Open source: per-domain summary using your config-defined key
curl -H "X-SB-Api-Key: your-configured-key" \
  "https://your-gateway/api/v1/summary"

GET /api/v1/me on the control plane returns exactly this shape (real response, captured from a running instance):

{
  "data": {
    "key": {
      "id": "2dc8e00f3ccc",
      "label": "demo-key",
      "accountId": "acct_demo",
      "scopes": ["traffic:read", "fleet:read", "threats:read", "license:read", "keys:manage"],
      "domains": [],
      "isOperatorKey": false
    },
    "license": {
      "tier": "starter",
      "isTrial": false,
      "expiresAt": "2026-09-16T14:52:39.778291+00:00",
      "features": ["stylobot.config-editor.live", "stylobot.config-editor.global"]
    }
  },
  "meta": { "generatedAt": "2026-08-17T14:52:39.778515+00:00" }
}

Conventions

  • Envelope: single resources are { "data": ..., "meta": { "generatedAt": ... } }; collections add "pagination": { "limit": ..., "nextCursor": ... }. nextCursor: null means the last page — pass the cursor back as ?cursor=... to continue. Field names are camelCase throughout.
  • Errors: RFC 7807 Problem Details — { "type", "title", "status", "detail", "code" }, with a requestId field on most (not all) error responses for support correlation. code is the discriminant: unauthorized, insufficient_scope, rate_limited, domain_not_licensed, invalid_cursor, not_found, validation_failed.
  • Status codes: 401 no or invalid credential, 403 valid key but missing scope, 402 valid key but the domain or feature isn't on your license, 429 rate limited.
  • Rate limits: every response under /api/v1 carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (unix seconds). Over the limit you get 429 with a Retry-After header. Defaults are 60/minute and 1,000/hour per key, tighter of the two wins; your account can be issued higher limits. Limits are enforced per instance (per-node, not cluster-wide) in this version.
  • Paging: ?limit= (1-500, default 100 — an out-of-range value falls back to 100, it is not clamped to 500) and ?cursor= (opaque, never hand-build one). Not every collection endpoint pages — some return their full result set in one page (nextCursor always null) because the underlying rollup is already bounded. Check the endpoint reference below.
  • Request ids: most responses carry X-Request-ID, echoed as requestId in error bodies where present.

Endpoint reference (commercial)

All commercial endpoints require Authorization: Bearer sbk_... and the scope listed. Hosted on your control plane unless marked "gateway".

Method & path Scope Paged? Returns
GET /api/v1/me any key key context + your license state
GET /api/v1/health fleet:read { status: "ok", version }
GET /api/v1/license license:read { orgId, tier, isTrial, isDemo, expiresAt, features, domains, maxNodes }
GET /api/v1/license/entitlements license:read no one row per licensed domain: { domain, licensed, reason, tier }
GET /api/v1/license/entitlements/{domain} license:read same shape, one domain; reason is licensed, not_in_license, or unconfigured
GET /api/v1/fleet/gateways?limit&cursor&status fleet:read yes { gatewayId, version, status, lastHeartbeatAt } per gateway
GET /api/v1/fleet/health fleet:read { gateways: { total, online, offline }, oldestHeartbeatAt, version }
GET /api/v1/fleet/endpoint-stats?limit fleet:read no { path, requests, botRequests, botPct }
GET /api/v1/fleet/detection-rate?bucket&window fleet:read no { buckets: [{ at, requests, detected }] }
GET /api/v1/threats/advisories?severity&status&limit&cursor threats:read yes advisory summaries: { id, sourceFeed, externalId, title, severity, publishedAt, status }
GET /api/v1/threats/advisories/{id} threats:read full advisory record, or 404
GET /api/v1/threats/fingerprints?status&limit&cursor threats:read yes { id, advisoryId, status, confidence, clusterId, clusterLabel, updatedAt }
GET /api/v1/threats/fingerprints/active threats:read no currently-active fingerprints only
GET /api/v1/keys keys:manage yes your account's own keys (never the plaintext)
POST /api/v1/keys keys:manage creates a key, returns the plaintext once
DELETE /api/v1/keys/{id} keys:manage 204, revokes the key
POST /api/v1/keys/{id}/rotate keys:manage new plaintext once, old key stops working immediately
GET /api/v1/domains?limit (gateway) traffic:read no observed domains: { domain, requests, bots, licensed }
GET /api/v1/domains/{domain}/traffic/summary?period (gateway) traffic:read { domain, period, periodStart, periodEnd, requests, verdicts: { bot, human, unknown }, botPercentage }
GET /api/v1/domains/{domain}/traffic/timeseries?period&bucket (gateway) traffic:read no { at, requests, bot, human } buckets
GET /api/v1/domains/{domain}/traffic/topbots?window&limit (gateway) traffic:read no { name, signature, requests, sharePct }
GET /api/v1/domains/{domain}/traffic/endpoints?window&limit (gateway) traffic:read no { method, path, requests, botRequests, botPct, last_seen } — note last_seen is the one field that isn't camelCase; everything else on this route is

The /api/v1/domains* group needs a gateway with PostgreSQL persistence enabled — SQLite-only installs don't have it yet. A domain outside your license returns 402 with code: "domain_not_licensed" rather than data.

GET /api/v1/me is a control-plane-only path. Your gateway also answers GET /api/v1/me, but that's a separate, older endpoint (key/detector-policy context, not scopes or license) — hit the control plane for the shape shown above.

Example error

Asking for a scope your key doesn't have:

{
  "type": "https://stylo.bot/errors/insufficient_scope",
  "title": "Insufficient scope",
  "status": 403,
  "detail": "This API key does not have the required scope: fleet:read.",
  "code": "insufficient_scope",
  "requestId": "0HNNSDSNK8GMS"
}

What is NOT in the API

Programmatic keys are read-only by design, aside from managing their own siblings under keys:manage. Config edits, policy changes, install tokens, compliance and report surfaces stay behind the operator console. Release-artifact downloads aren't wired into this API version.