Admin API #
The admin API lets you manage lexicons, monitor records, run backfill jobs, and control user access. All endpoints live under /admin and require an AIP-issued Bearer token from a DID that exists in the users table, with the appropriate permissions for the endpoint being called. 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 users table, and the user's permissions are loaded to authorize the request.
Auto-bootstrap: If the users table is empty, the first authenticated request automatically creates the caller as the super user with all permissions granted.
Non-user DIDs receive a 403 Forbidden response. Users without the required permission for a specific endpoint also receive 403 Forbidden.
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 users table, or user lacks the required permission |
404 Not Found |
Lexicon, user, 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, user 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
Requires api-keys:create permission.
curl -X POST http://localhost:3000/admin/api-keys \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{
"name": "CI Deploy",
"permissions": ["lexicons:read", "lexicons:create", "backfill:create"]
}'
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | A label to identify this key's usage |
permissions |
string[] | yes | Permissions to grant the key (must be a subset of the creating user's own permissions) |
Response: 201 Created
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "CI Deploy",
"key": "hv_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"key_prefix": "hv_a1b2c3d4",
"permissions": ["lexicons:read", "lexicons:create", "backfill:create"]
}
The key field contains the full API key. It is only returned in this response — store it securely. The key's effective permissions are the intersection of the permissions specified here and the creating user's permissions at the time of each request.
List API keys #
GET /admin/api-keys
Requires api-keys:read permission.
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",
"permissions": ["lexicons:read", "lexicons:create", "backfill:create"],
"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 user. The full key is never included — only the prefix.
Revoke an API key #
DELETE /admin/api-keys/{id}
Requires api-keys:delete permission.
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
User Management #
Create a user #
POST /admin/users
Requires users:create permission. You cannot grant permissions you don't have yourself (escalation guard).
curl -X POST http://localhost:3000/admin/users \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{
"did": "did:plc:newuser",
"template": "operator"
}'
| Field | Type | Required | Description |
|---|---|---|---|
did |
string | yes | The AT Protocol DID of the user to add |
template |
string | no | Permission template: viewer, operator, manager, or full_access |
permissions |
string[] | no | Explicit list of permissions to grant (used instead of or in addition to template) |
If neither template nor permissions is provided, the user is created with no permissions.
Response: 201 Created
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"did": "did:plc:newuser",
"is_super": false,
"permissions": ["lexicons:read", "records:read", "script-variables:read", "users:read", "api-keys:read", "api-keys:create", "api-keys:delete", "backfill:read", "backfill:create", "stats:read", "events:read"]
}
List users #
GET /admin/users
Requires users:read permission.
curl http://localhost:3000/admin/users -H "$AUTH"
Response: 200 OK
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"did": "did:plc:admin",
"is_super": true,
"permissions": ["lexicons:create", "lexicons:read", "lexicons:delete", "records:read", "records:delete", "records:delete-collection", "script-variables:create", "script-variables:read", "script-variables:delete", "users:create", "users:read", "users:update", "users:delete", "api-keys:create", "api-keys:read", "api-keys:delete", "backfill:create", "backfill:read", "stats:read", "events:read"],
"created_at": "2025-01-01T00:00:00Z",
"last_used_at": "2025-01-02T12:00:00Z"
}
]
Get a user #
GET /admin/users/{id}
Requires users:read permission.
curl http://localhost:3000/admin/users/550e8400-e29b-41d4-a716-446655440000 -H "$AUTH"
Response: 200 OK with the same shape as a single item from the list response.
Update user permissions #
PATCH /admin/users/{id}/permissions
Requires users:update permission. You cannot grant permissions you don't have yourself, and you cannot modify the super user's permissions.
curl -X PATCH http://localhost:3000/admin/users/550e8400-e29b-41d4-a716-446655440000/permissions \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{
"grant": ["lexicons:create", "lexicons:delete"],
"revoke": ["records:delete"]
}'
| Field | Type | Required | Description |
|---|---|---|---|
grant |
string[] | no | Permissions to add |
revoke |
string[] | no | Permissions to remove |
Response: 200 OK with the updated user object.
Transfer super user #
POST /admin/users/transfer-super
Only the current super user can call this endpoint. Transfers super user status to another existing user.
curl -X POST http://localhost:3000/admin/users/transfer-super \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{ "target_user_id": "550e8400-e29b-41d4-a716-446655440000" }'
| Field | Type | Required | Description |
|---|---|---|---|
target_user_id |
string | yes | The ID of the user to receive super status |
Response: 200 OK
Delete a user #
DELETE /admin/users/{id}
Requires users:delete permission. You cannot delete the super user or yourself.
curl -X DELETE http://localhost:3000/admin/users/550e8400-e29b-41d4-a716-446655440000 \
-H "$AUTH"
Response: 204 No Content
Labelers #
Manage external labeler subscriptions. See the Labelers guide for background.
Add a labeler #
POST /admin/labelers
Requires labelers:create permission.
curl -X POST http://localhost:3000/admin/labelers \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{ "did": "did:plc:ar7c4by46qjdydhdevvrndac" }'
| Field | Type | Required | Description |
|---|---|---|---|
did |
string | yes | The labeler's AT Protocol DID |
Response: 201 Created (empty body)
List labelers #
GET /admin/labelers
Requires labelers:read permission.
curl http://localhost:3000/admin/labelers -H "$AUTH"
Response: 200 OK
[
{
"did": "did:plc:ar7c4by46qjdydhdevvrndac",
"status": "active",
"cursor": 1234,
"created_at": "2026-03-15T00:00:00Z",
"updated_at": "2026-03-15T00:00:00Z"
}
]
| Field | Type | Description |
|---|---|---|
did |
string | The labeler's DID |
status |
string | active or paused |
cursor |
number|null | Last processed event cursor (null if never synced) |
created_at |
string | ISO 8601 creation timestamp |
updated_at |
string | ISO 8601 last-updated timestamp |
Update a labeler #
PATCH /admin/labelers/{did}
Requires labelers:create permission.
curl -X PATCH http://localhost:3000/admin/labelers/did:plc:ar7c4by46qjdydhdevvrndac \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{ "status": "paused" }'
| Field | Type | Required | Description |
|---|---|---|---|
status |
string | yes | New status: active or paused |
Response: 200 OK
Delete a labeler #
DELETE /admin/labelers/{did}
Requires labelers:delete permission. Removes the subscription and all labels emitted by this labeler.
curl -X DELETE http://localhost:3000/admin/labelers/did:plc:ar7c4by46qjdydhdevvrndac \
-H "$AUTH"
Response: 204 No Content
Permissions #
Each admin API endpoint requires a specific permission. See the Permissions guide for the full list of permissions and templates.
| Endpoint | Required Permission |
|---|---|
POST /admin/lexicons |
lexicons:create |
GET /admin/lexicons |
lexicons:read |
GET /admin/lexicons/{id} |
lexicons:read |
DELETE /admin/lexicons/{id} |
lexicons:delete |
POST /admin/network-lexicons |
lexicons:create |
GET /admin/network-lexicons |
lexicons:read |
DELETE /admin/network-lexicons/{id} |
lexicons:delete |
GET /admin/stats |
stats:read |
GET /admin/tap/stats |
stats:read |
POST /admin/backfill |
backfill:create |
GET /admin/backfill/status |
backfill:read |
GET /admin/events |
events:read |
POST /admin/api-keys |
api-keys:create |
GET /admin/api-keys |
api-keys:read |
DELETE /admin/api-keys/{id} |
api-keys:delete |
POST /admin/users |
users:create |
GET /admin/users |
users:read |
GET /admin/users/{id} |
users:read |
PATCH /admin/users/{id}/permissions |
users:update |
DELETE /admin/users/{id} |
users:delete |
POST /admin/users/transfer-super |
Super user only |
GET /admin/script-variables |
script-variables:read |
POST /admin/script-variables |
script-variables:create |
DELETE /admin/script-variables/{key} |
script-variables:delete |
POST /admin/labelers |
labelers:create |
GET /admin/labelers |
labelers:read |
PATCH /admin/labelers/{did} |
labelers:create |
DELETE /admin/labelers/{did} |
labelers:delete |