From 9e4c36c05b9917d598381f40bfb8ad60e2dc6e6d Mon Sep 17 00:00:00 2001 From: Keysat Date: Mon, 11 May 2026 17:48:50 -0500 Subject: [PATCH] Agent integration: new docs page + sidebar entry across all docs Ports the in-repo KEYSAT_AGENT_GUIDE.md into the docs site as a first-class page rather than linking out to a raw markdown file on GitHub. The page covers authentication, scoped API keys, OpenAPI discovery, error envelope conventions, common workflows (issue / revoke / find / cancel / change-tier / free-machine), webhook event types + signature verification, robust-agent patterns, a "comp-license-via-email" recipe, and the operator-only operations that aren't exposed to any scoped key. Sidebar gains an "Agent integration" entry under Get started on every page (index, install, integrate, wire-format, operate, agent itself). Docs index "These docs cover" + "Where to next" grids each gain a third card pointing at the agent guide so it's discoverable from the introduction page even for visitors who don't scan the sidebar. --- agent.html | 305 +++++++++++++++++++++++++++++++++++++++++++++++ index.html | 11 ++ install.html | 1 + integrate.html | 1 + operate.html | 1 + wire-format.html | 1 + 6 files changed, 320 insertions(+) create mode 100644 agent.html diff --git a/agent.html b/agent.html new file mode 100644 index 0000000..38dcabf --- /dev/null +++ b/agent.html @@ -0,0 +1,305 @@ + + + + + +Keysat Docs — Agent integration + + + + + +
+ Keysat + Docs +
+ +
+ + +
+
Get started · Agent integration
+

Agent integration guide.

+

How to build agents, bots, and automation that operate a Keysat instance. Keysat was designed from the start to be agent-friendly: the admin API uses plain HTTP + JSON with Bearer-token auth, an OpenAPI 3.1 spec drives discovery, scoped API keys grant least-privilege access without exposing the master credential, errors carry stable machine-readable codes, and webhooks let an agent react to events instead of polling.

+

This guide covers the operator side of Keysat — running, configuring, and performing day-to-day operations. For the buyer side (validating licenses inside your app), see Integrate the SDK.

+ +

Quick start

+
# 1. Discover the API surface
+curl https://your-keysat-host/v1/openapi.json
+
+# 2. Generate a scoped API key (admin UI: Settings → API keys, or via curl)
+curl -X POST https://your-keysat-host/v1/admin/api-keys \
+  -H "Authorization: Bearer $MASTER_ADMIN_KEY" \
+  -H "Content-Type: application/json" \
+  -d '{"label":"Support bot","role":"support"}'
+# Response includes `token: ks_...`. Save it — it's only shown once.
+
+# 3. Use the scoped key
+curl https://your-keysat-host/v1/admin/licenses?status=active \
+  -H "Authorization: Bearer ks_..."
+ +

Authentication

+

All admin endpoints use HTTP Bearer auth:

+
Authorization: Bearer <token>
+

Two kinds of tokens are accepted.

+ +

Master admin API key — the env-configured KEYSAT_ADMIN_API_KEY (visible in StartOS Actions → Show credentials on first install). Full access to every endpoint. This is the operator's credential. Don't hand it to agents.

+ +

Scoped API keys — additional tokens generated in admin UI → Settings → API keys. Each carries a role that bounds what it can do. Format: ks_<43 chars>. Operators can revoke any scoped key from the same UI; revoked tokens stop working immediately.

+ +

Role to scope mapping

+ + + + + + + + +
RoleWhat it can do
read-onlyList / get every resource. Mutate nothing.
license-issuerAll read-only scopes + issue / revoke / suspend / change-tier on licenses. Cannot touch products, policies, or codes.
supportAll license-issuer scopes + cancel subscriptions + force-deactivate machines.
full-adminEvery scope. Equivalent to the master key for most endpoints.
+

Endpoints that touch settings (operator name, payment provider connections, self-license activation, scoped API key management) always require the master admin key. A full-admin scoped key cannot, for example, generate another scoped key — that's a self-defeating elevation path.

+ +

Discovering the API

+

Two complementary discovery mechanisms.

+ +

OpenAPI 3.1 spec

+

GET /v1/openapi.json — unauthenticated. Returns a curated spec covering the agent-relevant subset of endpoints. Use this with:

+
    +
  • OpenAI Custom GPTs: paste the URL as an Action.
  • +
  • OpenAI Assistants / Functions: feed the spec to tool definition generators.
  • +
  • Claude tool use: derive your tools array from the spec; Claude Code agents can WebFetch the spec at runtime.
  • +
  • LangChain / AutoGen / Smolagents: use their OpenAPI loaders.
  • +
  • Code generation: openapi-generator-cli generate -i /v1/openapi.json -g python -o ./client.
  • +
+

The spec is a stable agent surface, not auto-derived from handler signatures. We commit to keeping documented endpoints and field shapes stable across minor releases.

+ +

Embedded endpoint listing

+

This guide's Common workflows section below covers the most common agent tasks with copy-paste examples.

+ +

Response envelope conventions

+

Every error response uses the same JSON envelope:

+
{
+  "ok": false,
+  "error": "tier_cap",
+  "message": "Your Creator tier allows up to 5 products. You're at 5...",
+  "upgrade_url": "https://licensing.keysat.xyz/buy/keysat?policy=pro"
+}
+

error is a stable machine-readable code; message is human-readable. The upgrade_url field appears on 402 (tier cap) responses so a UI can render an upgrade CTA without parsing message strings.

+ +

Error codes

+ + + + + + + + + + + + + + +
HTTPerror codeWhen
400bad_requestMalformed body, missing required field, invalid enum value
401unauthorizedNo Authorization: Bearer header
403forbiddenWrong token, revoked scoped key, role doesn't grant required scope
404not_foundResource id doesn't exist
409conflictSlug collision, delete-with-references blocked, etc.
402tier_capOperator's self-tier doesn't include the required entitlement
429rate_limitedRate limit hit (e.g. /v1/recover, /v1/validate)
502upstream_errorBTCPay / Zaprite call failed
503service_unavailable / btcpay_not_configuredProvider not yet connected
500internal_errorBug. Includes a trace id in logs; report it.
+ +

Validate response

+

POST /v1/validate is the one endpoint that returns 200 in all cases. Inspect ok + reason:

+ + + + + + + + + + + + +
reasonMeaning
bad_signatureSignature doesn't verify against the trust-root pubkey
not_foundLicense key not in the daemon's DB
revokedOperator revoked it
suspendedOperator suspended it (reversible)
expiredPast expires_at
fingerprint_mismatchDifferent machine than the one bound on first activate
product_mismatchLicense is for a different product than the caller asserted
machine_cap_exceededActivating this fingerprint would exceed max_machines
+ +

Common workflows

+ +

Issue a comp license

+
curl -X POST $KS/v1/admin/licenses \
+  -H "Authorization: Bearer ks_..." \
+  -H "Content-Type: application/json" \
+  -d '{
+    "product_slug": "recap",
+    "policy_slug": "pro",
+    "buyer_email": "alice@example.com",
+    "buyer_note": "Conference speaker comp"
+  }'
+

Returns the issued license object including license_key. The buyer pastes the key into their app; subsequent validate calls return ok: true with the policy's entitlements.

+

Scope required: licenses:write (any role except read-only).

+ +

Revoke a license

+
curl -X POST $KS/v1/admin/licenses/$LICENSE_ID/revoke \
+  -H "Authorization: Bearer ks_..." \
+  -H "Content-Type: application/json" \
+  -d '{"reason":"refund issued"}'
+

Idempotent. The next online validate from the buyer's app returns reason: revoked.

+

Scope required: licenses:write.

+ +

Find a license by email

+
curl "$KS/v1/admin/licenses?buyer_email=alice@example.com" \
+  -H "Authorization: Bearer ks_..."
+

Returns matching licenses (without the license_key field — that's only returned on issue / recover). Use the id for follow-up operations.

+

Scope required: licenses:read.

+ +

Cancel a buyer's subscription

+
# Look up the subscription id first (filter by license_id if you have it)
+curl "$KS/v1/admin/subscriptions?status=active" \
+  -H "Authorization: Bearer ks_..."
+
+# Then cancel
+curl -X POST $KS/v1/admin/subscriptions/$SUB_ID/cancel \
+  -H "Authorization: Bearer ks_..." \
+  -d '{"reason":"buyer requested"}'
+

License stays valid through the current cycle's expires_at. Renewal worker stops issuing new invoices.

+

Scope required: subscriptions:write.

+ +

Free a machine seat

+
curl -X POST $KS/v1/admin/machines/$MACHINE_ID/deactivate \
+  -H "Authorization: Bearer ks_..." \
+  -d '{"reason":"buyer moved devices"}'
+

The seat opens up. The buyer's next validate from any machine takes the freed seat.

+

Scope required: machines:write.

+ +

Programmatic tier change (comp upgrade)

+
curl -X POST $KS/v1/admin/licenses/$LICENSE_ID/change-tier \
+  -H "Authorization: Bearer ks_..." \
+  -d '{
+    "target_policy_slug": "pro",
+    "reason": "support resolution"
+  }'
+

Always applies as comp (no invoice) from the admin path. Buyer-initiated paid upgrades go through /v1/upgrade (different endpoint, signed-license auth).

+

Scope required: licenses:write.

+ +

Webhooks — react to events instead of polling

+

Configure webhook endpoints in admin UI → Webhooks. The daemon POSTs JSON payloads, HMAC-SHA256 signed with the endpoint's secret, on these events:

+ + + + + + + + + + + + + +
EventFires on
license.issuedNew license minted (purchase, comp, redeem)
license.revoked / license.suspended / license.unsuspendedAdmin operations
license.tier_changedTier upgrade/downgrade applied
invoice.paidA BTCPay / Zaprite invoice settled
subscription.renewal_pendingRenewal worker created a fresh invoice
subscription.renewal_skippedRenewal skipped (e.g. policy archived)
subscription.cancelledBuyer or admin cancelled
subscription.lapsedPast-due grace expired
machine.activatedFirst validate from a new fingerprint
+ +

Verify signatures:

+
import hmac, hashlib
+
+def verify(body_bytes: bytes, signature_header: str, secret: str) -> bool:
+    expected = hmac.new(secret.encode(), body_bytes, hashlib.sha256).hexdigest()
+    return hmac.compare_digest(expected, signature_header)
+

The header is X-Keysat-Signature. Failed deliveries retry with exponential backoff up to 10 attempts; permanently-failed deliveries land in the DLQ visible at admin UI → Webhooks → Failed.

+ +

Designing a robust agent

+

A few patterns that work well in practice.

+ +

Idempotency

+

The daemon's mutation endpoints are idempotent where they can be. Revoke, suspend, unsuspend, archive, unarchive, subscription cancel — all return success on the second call without changing state. Your agent can safely retry on network errors.

+ +

Pagination

+

List endpoints return up to ~100 rows by default. Use ?limit=N and ?offset=N for larger result sets. The OpenAPI spec documents the limits per endpoint.

+ +

Rate limits

+

The admin endpoints have no per-IP rate limit today — operators are trusted. The public endpoints (/v1/validate, /v1/recover) are rate-limited per client IP (10/min for /recover; /validate is unlimited but a reasonable agent calls it once per app boot + once per hour).

+ +

Master key handling

+

If your automation needs full-admin because it touches operator-only operations (creating other API keys, changing payment providers), use the master key from a secret manager. If it can stay within license / product / policy operations, always use a scoped key. Operators can revoke a compromised scoped key without rotating the master credential.

+ +

Backoff on 5xx

+

internal_error (500) is a bug or a transient DB lock. Retry with exponential backoff (1s, 2s, 4s, 8s, give up). Don't retry on 4xx — those are deterministic client errors.

+ +

Concrete recipe — "Comp a license to anyone who emails support@"

+
import os, requests, imaplib, email
+
+KS = os.environ["KEYSAT_URL"]
+TOKEN = os.environ["KEYSAT_API_KEY"]  # license-issuer-scoped key
+
+def issue_comp_license(buyer_email: str, product_slug: str, reason: str) -> str:
+    r = requests.post(
+        f"{KS}/v1/admin/licenses",
+        headers={"Authorization": f"Bearer {TOKEN}"},
+        json={
+            "product_slug": product_slug,
+            "policy_slug": "default",
+            "buyer_email": buyer_email,
+            "buyer_note": reason,
+        },
+        timeout=10,
+    )
+    r.raise_for_status()
+    return r.json()["license_key"]
+
+# Poll IMAP, parse incoming requests, call issue_comp_license, reply with the key
+

That's the entire pattern. The agent doesn't need full admin — just the license-issuer role. If it ever gets compromised, you revoke the scoped key in the admin UI and generate a new one in 30 seconds.

+ +

What's NOT exposed to agents

+

Some operations are deliberately operator-only and not accessible to any scoped key, including full-admin:

+
    +
  • Generating / revoking scoped API keys (/v1/admin/api-keys)
  • +
  • Connecting / disconnecting payment providers
  • +
  • Setting the operator name
  • +
  • Activating the self-license (/v1/admin/self-license)
  • +
  • Resetting the analytics install_uuid
  • +
  • Changing the web UI password (StartOS Action only)
  • +
+

These all require the master KEYSAT_ADMIN_API_KEY. The reasoning: an agent that can rotate its own credentials, connect arbitrary payment processors, or change the operator identity is no longer bounded by the role it was given.

+ +

Help us improve this guide

+

The OpenAPI spec is the source of truth for the API surface. This guide is a hand-curated overlay focused on the workflows we've seen agents actually need. If you're building something the spec covers but this guide doesn't make obvious, open an issue at github.com/keysat-xyz/keysat with the workflow shape and we'll add it.

+
+ + +
+ + + diff --git a/index.html b/index.html index 5f48b81..d204602 100644 --- a/index.html +++ b/index.html @@ -21,6 +21,7 @@ Introduction Install & setup Integrate the SDK + Agent integration
Concepts
@@ -61,6 +62,11 @@

Integrate the SDK →

Add the SDK to your app, embed your public key, verify a license at startup. About five lines of code.

+ + Agent / automation +

Agent integration →

+

OpenAPI spec, scoped API keys, webhooks. Build bots that issue comp licenses, react to events, or automate support flows.

+

Architecture

@@ -138,6 +144,11 @@

Integrate the SDK →

Embed your public key, add the SDK to your app, verify a license offline.

+ + Step 1 for agent builders +

Agent integration →

+

Operate your Keysat instance programmatically. OpenAPI spec, scoped keys, webhooks, recipes.

+
diff --git a/install.html b/install.html index eaa3c1d..02f7885 100644 --- a/install.html +++ b/install.html @@ -21,6 +21,7 @@ Introduction Install & setup Integrate the SDK + Agent integration
Concepts
diff --git a/integrate.html b/integrate.html index 1d35807..39fe979 100644 --- a/integrate.html +++ b/integrate.html @@ -21,6 +21,7 @@ Introduction Install & setup Integrate the SDK + Agent integration
Concepts
diff --git a/operate.html b/operate.html index 8a2c139..316f175 100644 --- a/operate.html +++ b/operate.html @@ -21,6 +21,7 @@ Introduction Install & setup Integrate the SDK + Agent integration
Concepts
diff --git a/wire-format.html b/wire-format.html index 787e1fd..ac97747 100644 --- a/wire-format.html +++ b/wire-format.html @@ -21,6 +21,7 @@ Introduction Install & setup Integrate the SDK + Agent integration
Concepts