Admin API #
The admin API lets you manage lexicons, monitor records, run backfill jobs, and control admin access. All endpoints live under /admin and require an AIP-issued Bearer token from a DID that exists in the admins table. You can also manage all of this through the web dashboard.
Auth #
The admin API supports two authentication methods:
- OAuth (AIP) — the Bearer token is validated against AIP's
/oauth/userinfoendpoint to retrieve the caller's DID. - API keys — read/write tokens starting with
hv_. See the API Keys guide for details.
In both cases the resolved DID is checked against the admins table.
Auto-bootstrap: If the admins table is empty, the first authenticated request automatically inserts the caller as the initial admin.
Non-admin DIDs receive a 403 Forbidden response.
All error responses return JSON with an error field:
{
"error": "description of what went wrong"
}
| Status | Meaning |
|---|---|
400 Bad Request |
Invalid input (missing required fields, malformed lexicon JSON) |
401 Unauthorized |
Missing or invalid Bearer token. See AIP documentation for token issues |
403 Forbidden |
Authenticated DID is not in the admins table |
404 Not Found |
Lexicon, admin, or backfill job not found |
# All examples assume $TOKEN is an AIP-issued access token or API key
AUTH="Authorization: Bearer $TOKEN"
Lexicons #
Upload / upsert a lexicon #
POST /admin/lexicons
curl -X POST http://localhost:3000/admin/lexicons \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{
"lexicon_json": { "lexicon": 1, "id": "xyz.statusphere.status", "defs": { "main": { "type": "record", "key": "tid", "record": { "type": "object", "required": ["status", "createdAt"], "properties": { "status": { "type": "string", "maxGraphemes": 1 }, "createdAt": { "type": "string", "format": "datetime" } } } } } },
"backfill": true,
"target_collection": null
}'
| Field | Type | Required | Description |
|---|---|---|---|
lexicon_json |
object | yes | Raw lexicon JSON (must have lexicon: 1 and id) |
backfill |
boolean | no | Whether uploading triggers historical backfill (default true) |
target_collection |
string | no | For query/procedure lexicons, the record collection they operate on |
script |
string | no | Lua script for query/procedure endpoints |
index_hook |
string | no | Index hook Lua script for record lexicons |
Response: 201 Created (new) or 200 OK (upsert)
{
"id": "xyz.statusphere.status",
"revision": 1
}
List lexicons #
GET /admin/lexicons
curl http://localhost:3000/admin/lexicons -H "$AUTH"
Response: 200 OK
[
{
"id": "xyz.statusphere.status",
"revision": 1,
"lexicon_type": "record",
"backfill": true,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
Get a lexicon #
GET /admin/lexicons/{id}
curl http://localhost:3000/admin/lexicons/xyz.statusphere.status -H "$AUTH"
Response: 200 OK with full lexicon details including raw JSON.
Delete a lexicon #
DELETE /admin/lexicons/{id}
curl -X DELETE http://localhost:3000/admin/lexicons/xyz.statusphere.status -H "$AUTH"
Response: 204 No Content
Network Lexicons #
Network lexicons are fetched from the AT Protocol network via DNS TXT resolution and kept updated via Tap. See Lexicons - Network lexicons for background.
Add a network lexicon #
POST /admin/network-lexicons
curl -X POST http://localhost:3000/admin/network-lexicons \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{
"nsid": "xyz.statusphere.status",
"target_collection": null
}'
| Field | Type | Required | Description |
|---|---|---|---|
nsid |
string | yes | The NSID of the lexicon to watch |
target_collection |
string | no | For query/procedure lexicons, the record collection they operate on |
HappyView resolves the NSID authority via DNS TXT, fetches the lexicon from the authority's PDS, parses it, and stores it.
Response: 201 Created
{
"nsid": "xyz.statusphere.status",
"authority_did": "did:plc:authority",
"revision": 1
}
List network lexicons #
GET /admin/network-lexicons
curl http://localhost:3000/admin/network-lexicons -H "$AUTH"
Response: 200 OK
[
{
"nsid": "xyz.statusphere.status",
"authority_did": "did:plc:authority",
"target_collection": null,
"last_fetched_at": "2025-01-01T00:00:00Z",
"created_at": "2025-01-01T00:00:00Z"
}
]
Remove a network lexicon #
DELETE /admin/network-lexicons/{nsid}
curl -X DELETE http://localhost:3000/admin/network-lexicons/xyz.statusphere.status \
-H "$AUTH"
Removes the network lexicon tracking and also deletes the lexicon from the lexicons table and in-memory registry.
Response: 204 No Content
Stats #
Record counts #
GET /admin/stats
curl http://localhost:3000/admin/stats -H "$AUTH"
Response: 200 OK
{
"total_records": 12345,
"collections": [{ "collection": "xyz.statusphere.status", "count": 500 }]
}
Tap Stats #
Aggregate stats from the Tap instance. Useful for monitoring backfill progress. See Backfill - Job lifecycle for context.
Get Tap stats #
GET /admin/tap/stats
curl http://localhost:3000/admin/tap/stats -H "$AUTH"
Response: 200 OK
{
"repo_count": 5234,
"record_count": 1048576,
"outbox_buffer": 42
}
| Field | Type | Description |
|---|---|---|
repo_count |
number | Total repos Tap is tracking |
record_count |
number | Total records Tap has indexed |
outbox_buffer |
number | Pending events awaiting delivery (high = Tap is busy) |
Returns 502 Bad Gateway if Tap is unreachable.
Backfill #
Create a backfill job #
POST /admin/backfill
curl -X POST http://localhost:3000/admin/backfill \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{ "collection": "xyz.statusphere.status" }'
| Field | Type | Required | Description |
|---|---|---|---|
collection |
string | no | Limit to a single collection (backfills all if omitted) |
did |
string | no | Limit to a single DID (discovers all via relay if omitted) |
Response: 201 Created
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending"
}
List backfill jobs #
GET /admin/backfill/status
curl http://localhost:3000/admin/backfill/status -H "$AUTH"
Response: 200 OK
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"collection": "xyz.statusphere.status",
"did": null,
"status": "completed",
"total_repos": 42,
"processed_repos": 42,
"total_records": 1000,
"error": null,
"started_at": "2025-01-01T00:01:00Z",
"completed_at": "2025-01-01T00:05:00Z",
"created_at": "2025-01-01T00:00:00Z"
}
]
Event Logs #
HappyView records an audit trail of system events: lexicon changes, record operations, Lua script executions and errors, admin actions, backfill jobs, and Tap connectivity. See the Event Logs guide for details on event types and retention.
List event logs #
GET /admin/events
curl "http://localhost:3000/admin/events?severity=error&limit=10" -H "$AUTH"
| Param | Type | Required | Description |
|---|---|---|---|
event_type |
string | no | Filter by exact event type (e.g. script.error) |
category |
string | no | Filter by category prefix (e.g. lexicon matches all lexicon events) |
severity |
string | no | Filter by severity: info, warn, or error |
subject |
string | no | Filter by subject (lexicon ID, record URI, admin DID, etc.) |
cursor |
string | no | Pagination cursor (ISO 8601 timestamp from previous response) |
limit |
number | no | Results per page (default 50, max 100) |
Response: 200 OK
{
"events": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"event_type": "script.error",
"severity": "error",
"actor_did": "did:plc:abc123",
"subject": "com.example.feed.like",
"detail": {
"error": "attempt to index nil value",
"script_source": "function handle() ... end",
"input": { "status": "hello" },
"caller_did": "did:plc:abc123",
"method": "com.example.feed.like"
},
"created_at": "2026-03-01T12:00:00Z"
}
],
"cursor": "2026-03-01T11:59:00Z"
}
Events are returned in reverse chronological order (newest first). Pass the cursor value from the response to fetch the next page.
API Keys #
Manage API keys for programmatic access. See the API Keys guide for usage details.
Create an API key #
POST /admin/api-keys
curl -X POST http://localhost:3000/admin/api-keys \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{ "name": "CI Deploy" }'
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | A label to identify this key's usage |
Response: 201 Created
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "CI Deploy",
"key": "hv_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"key_prefix": "hv_a1b2c3d4"
}
The key field contains the full API key. It is only returned in this response — store it securely.
List API keys #
GET /admin/api-keys
curl http://localhost:3000/admin/api-keys -H "$AUTH"
Response: 200 OK
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "CI Deploy",
"key_prefix": "hv_a1b2c3d4",
"created_at": "2026-03-01T00:00:00Z",
"last_used_at": "2026-03-06T12:00:00Z",
"revoked_at": null
}
]
Only returns keys belonging to the authenticated admin. The full key is never included — only the prefix.
Revoke an API key #
DELETE /admin/api-keys/{id}
curl -X DELETE http://localhost:3000/admin/api-keys/550e8400-e29b-41d4-a716-446655440000 \
-H "$AUTH"
Sets revoked_at on the key. The key remains in the database for audit purposes but can no longer authenticate.
Response: 204 No Content
Admin management #
Add an admin #
POST /admin/admins
curl -X POST http://localhost:3000/admin/admins \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{ "did": "did:plc:newadmin" }'
Response: 201 Created
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"did": "did:plc:newadmin"
}
List admins #
GET /admin/admins
curl http://localhost:3000/admin/admins -H "$AUTH"
Response: 200 OK
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"did": "did:plc:admin",
"created_at": "2025-01-01T00:00:00Z",
"last_used_at": "2025-01-02T12:00:00Z"
}
]
Remove an admin #
DELETE /admin/admins/{id}
curl -X DELETE http://localhost:3000/admin/admins/550e8400-e29b-41d4-a716-446655440000 \
-H "$AUTH"
Response: 204 No Content