Agent Integration Guide
Complete guide for AI agents to discover counters, build and maintain dashboard boards, mint scoped credentials, and self-destruct keys.
1. Discover
2. Build Boards
3. Mint Keys
4. Self-Destruct
1. The Two Hosts and Authentication
counters.dev operates two separate hosts with distinct responsibilities. Every request carries the exact same authorization header:
Authorization: Bearer sk_live_...| Host | Surface | Endpoints | Accepted Credential |
|---|---|---|---|
| https://api.counters.dev | Data Plane | GET /v1/counters, values, series | Organization API key (sk_live_...) |
| https://hub.counters.dev | Hub & Control Plane | /v1/boards*, /v1/keys* | Organization API key (sk_live_...) |
Strict Isolation Rules
2. Scopes Matrix
API keys possess granular permissions. The board and key endpoints strictly check required scopes on every call:
| Operation | Host | Endpoint | Required Scope |
|---|---|---|---|
| List & discover counters | api.counters.dev | GET /v1/counters | counter:read |
| List or get boards | hub.counters.dev | GET /v1/boards, GET /v1/boards/{boardId} | board:read |
| Create or update boards | hub.counters.dev | POST /v1/boards, PUT /v1/boards/{boardId} | board:write |
| Delete boards | hub.counters.dev | DELETE /v1/boards/{boardId} | board:manage |
| Mint child API keys | hub.counters.dev | POST /v1/keys | key:mint |
| Self-destruct calling key | hub.counters.dev | DELETE /v1/keys/self | None (any key can self-revoke) |
When a scope check fails, the API answers with 403 Forbidden and body:
{
"type": "scope_denied",
"required": "board:write"
}3. Step 0: Discover Existing Counters
Widgets reference counters by string key. Always list existing counters first to confirm exact spellings. Format validation accepts any valid key (^[A-Za-z0-9._:-]+$, ≤ 200 chars), but a misspelled key will simply render an empty tile without raising an error.
curl -s "https://api.counters.dev/v1/counters?limit=100" \
-H "Authorization: Bearer $COUNTERS_API_KEY"{
"data": [
{ "key": "signups", "value": "12912", "epoch": 1 },
{ "key": "active_sessions", "value": "842", "epoch": 1 },
{ "key": "api_requests", "value": "450201", "epoch": 1 }
],
"nextCursor": null
}4. Boards & The Five Widget Types
A board document contains a name and an array of up to 12 widgets (total payload limit: 32 KiB). Server-minted fields (id, createdAt, updatedAt, revision) are generated by the server and must never be sent on write operations.
stat
Stat tile with a single large number
s (1 column)ticker
Compact live counter ticker
s (1 column)delta-chart
Delta line/bar chart over time intervals
l (full row) or mgoal
Goal progress against a human-supplied target
m (2 columns)table
Table listing historical bucket values
m (2 columns)5. Creating Boards (POST /v1/boards)
Always supply a lowercase UUID v4 in the Idempotency-Key header. This guarantees that network retries never produce duplicate boards.
curl -X POST "https://hub.counters.dev/v1/boards" \
-H "Authorization: Bearer $COUNTERS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000" \
-d '{
"name": "Production Metrics",
"widgets": [
{
"id": "w1",
"type": "stat",
"counterKey": "signups",
"title": "Total Signups",
"size": "s",
"options": { "format": "compact" }
},
{
"id": "w2",
"type": "delta-chart",
"counterKey": "signups",
"title": "Daily Signups",
"size": "l",
"options": { "rangePreset": "7d", "bucket": "1h" }
}
]
}'On success (201 Created), record the returned id and revision: 1 for any future modifications.
6. Updating Boards & Optimistic Concurrency
Updates replace the board whole. Always supply the If-Match header with the exact stored integer revision.
curl -X PUT "https://hub.counters.dev/v1/boards/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
-H "Authorization: Bearer $COUNTERS_API_KEY" \
-H "Content-Type: application/json" \
-H "If-Match: 1" \
-d '{
"name": "Production Metrics (Refreshed)",
"widgets": [ ...new complete array of widgets... ]
}'Handling Concurrency Conflicts (409 / 412)
409 Conflict or 412 Precondition Failed. Do not overwrite blindly. Re-read the board via GET /v1/boards/{boardId}, reconcile your widgets with the fresh state, and retry using the new revision number.7. Deleting Boards (DELETE /v1/boards/{boardId})
Deleting a board requires board:manage. It deletes the layout only; counter data and history remain completely intact.
curl -X DELETE "https://hub.counters.dev/v1/boards/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
-H "Authorization: Bearer $COUNTERS_API_KEY"8. Key Minting & Delegation Recipe
In multi-agent systems, avoid sharing root keys. Use the delegation pattern: an orchestrator holding key:mint mints a temporary, least-privilege key for a sub-agent. When the sub-agent finishes, it calls DELETE /v1/keys/self to self-destruct its key.
Step 1: Orchestrator Mints Scoped Child Key
Requires key:mint. Scopes must be a subset of caller's own scopes.
curl -X POST "https://hub.counters.dev/v1/keys" \
-H "Authorization: Bearer $COUNTERS_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "worker-board-builder",
"scopes": ["counter:read", "board:read", "board:write"],
"expires_at": "2026-09-27T12:00:00Z"
}'Step 2: Sub-Agent Executes & Self-Destructs
Any key can call this. Revocation is permanent (returns 401 afterwards).
curl -X DELETE "https://hub.counters.dev/v1/keys/self" \
-H "Authorization: Bearer $SUB_AGENT_KEY"9. Error Handling Reference
| Status | Problem Type | Meaning & Recommended Action |
|---|---|---|
| 400 | bad_request | Malformed body, unknown fields, missing If-Match, or malformed UUID. Fix payload against schema. |
| 401 | unauthorized | Missing, expired, or revoked API key, or sent JWT instead of API key. Re-obtain valid key. |
| 403 | scope_denied | Key lacks required scope. Check problem required member and ask human operator for that scope. |
| 404 | not_found | Unknown boardId, cross-org resource, or organization board surface is disabled. If all board routes 404, notify operator that board gate is disabled. |
| 409 / 412 | conflict / concurrency | Stale If-Match revision or Idempotency-Key conflict. Re-fetch board via GET, merge, and retry. |
| 413 | payload_too_large | Board exceeds 32 KiB cap. Use fewer widgets (cap 12) or shorter titles. |
| 429 | rate_limited | Per-key rate limit exceeded (20 rps). Wait according to Retry-After header and retry. |