Admin API

Service Entries

Manage the service entries published in the instance's DID document. Each entry declares an XRPC service type (e.g. AtprotoLabeler, BskyFeedGenerator) and controls which lexicons route to HappyView's endpoint. All endpoints require the settings:manage permission.

const TOKEN = "hv_..."; // your API key
const headers = { Authorization: `Bearer ${TOKEN}` };

List service entries

GET /admin/service-entries
interface ServiceEntry {
  id: number;
  fragment_id: string;
  service_type: string;
  access_mode: string;
  created_at: string;
  updated_at: string;
}

const response = await fetch("http://127.0.0.1:3000/admin/service-entries", {
  headers,
});
const data: ServiceEntry[] = await response.json();

Response: 200 OK

[
  {
    "id": 1,
    "fragment_id": "#happyview",
    "service_type": "AtprotoLabeler",
    "access_mode": "all",
    "created_at": "2026-05-09T12:00:00Z",
    "updated_at": "2026-05-09T12:00:00Z"
  }
]

Create a service entry

POST /admin/service-entries
const response = await fetch("http://127.0.0.1:3000/admin/service-entries", {
  method: "POST",
  headers: {
    ...headers,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    fragment_id: "#feed",
    service_type: "BskyFeedGenerator",
  }),
});
const data: ServiceEntry = await response.json();
FieldTypeRequiredDescription
fragment_idstringyesDID document fragment (e.g. #feed, #happyview)
service_typestringyesAT Protocol service type (e.g. BskyFeedGenerator)

The entry is created with access_mode set to all. The endpoint URL is derived from PUBLIC_URL.

Response: 201 Created

Update a service entry

PUT /admin/service-entries/{id}
await fetch("http://127.0.0.1:3000/admin/service-entries/1", {
  method: "PUT",
  headers: {
    ...headers,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    service_type: "AtprotoLabeler",
    access_mode: "allowlist",
  }),
});
FieldTypeRequiredDescription
fragment_idstringnoUpdated DID document fragment
service_typestringnoUpdated service type
access_modestringnoall, allowlist, or disabled

All fields are optional. Only provided fields are updated.

Response: 204 No Content

Delete a service entry

DELETE /admin/service-entries/{id}
await fetch("http://127.0.0.1:3000/admin/service-entries/1", {
  method: "DELETE",
  headers,
});

Returns 404 Not Found if the entry doesn't exist.

Response: 204 No Content

List XRPCs for a service entry

GET /admin/service-entries/{id}/xrpcs

Returns the lexicon IDs (NSIDs) bound to this service entry.

const response = await fetch("http://127.0.0.1:3000/admin/service-entries/1/xrpcs", {
  headers,
});
const data: string[] = await response.json();

Response: 200 OK

["app.bsky.feed.getFeedSkeleton", "app.bsky.feed.describeFeedGenerator"]

Add XRPCs to a service entry

POST /admin/service-entries/{id}/xrpcs
await fetch("http://127.0.0.1:3000/admin/service-entries/1/xrpcs", {
  method: "POST",
  headers: {
    ...headers,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    lexicon_ids: ["app.bsky.feed.getFeedSkeleton"],
  }),
});
FieldTypeRequiredDescription
lexicon_idsstring[]yesLexicon NSIDs to bind to this entry

Response: 204 No Content

Remove XRPCs from a service entry

DELETE /admin/service-entries/{id}/xrpcs
await fetch("http://127.0.0.1:3000/admin/service-entries/1/xrpcs", {
  method: "DELETE",
  headers: {
    ...headers,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    lexicon_ids: ["app.bsky.feed.getFeedSkeleton"],
  }),
});
FieldTypeRequiredDescription
lexicon_idsstring[]yesLexicon NSIDs to unbind from this entry

Response: 204 No Content

List services for a lexicon

GET /admin/lexicons/{id}/services

Returns service entries that grant access to the specified lexicon.

const response = await fetch("http://127.0.0.1:3000/admin/lexicons/app.bsky.feed.getFeedSkeleton/services", {
  headers,
});
const data: ServiceEntry[] = await response.json();

Response: 200 OK — returns an array of ServiceEntry objects.

Sync to PLC directory

These endpoints publish your service entries to the PLC directory so they appear in the DID document. The sync method depends on the service identity mode.

Direct sync (did:plc mode)

POST /admin/service-entries/sync-plc

Signs and submits a PLC update operation using the stored rotation key. Only available when the identity mode is did_plc.

curl -X POST http://127.0.0.1:3000/admin/service-entries/sync-plc -H "$AUTH"

Returns 400 Bad Request if the identity mode is not did_plc or no DID is configured.

Response: 204 No Content

Request PLC token (attach_account mode)

POST /admin/service-entries/sync-plc/request

Requests a PLC operation signature token via the attached account's PDS. This sends an email confirmation code to the account holder. Only available when the identity mode is attach_account.

curl -X POST http://127.0.0.1:3000/admin/service-entries/sync-plc/request -H "$AUTH"

Returns 400 Bad Request if the identity mode is not attach_account.

Response: 204 No Content

Submit PLC token (attach_account mode)

POST /admin/service-entries/sync-plc/submit

Submits the email confirmation token to sign and publish the PLC operation via the attached account's PDS.

curl -X POST http://127.0.0.1:3000/admin/service-entries/sync-plc/submit \
  -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{ "token": "123456" }'
FieldTypeRequiredDescription
tokenstringyesEmail confirmation code from the PDS

Returns 400 Bad Request if the identity mode is not attach_account.

Response: 204 No Content