Skip to content

Lexicon Publishing

Admin API endpoints for publishing lexicons as com.atproto.lexicon.schema records and managing the rules that decide where they go. For the concepts, DNS requirements and dashboard, see Publishing Lexicons.

All endpoints are under /admin/lexicon-publishing. Reads need lexicons:read and writes need lexicons:publish. Choosing a linked repo as a rule’s target also needs linked-repos:read; for a caller without it, every linked-repo target in a response keeps kind, available and can_unpublish and has linked_repo_id, did, handle and unavailable_reason set to null, as does a linked-repo rule’s own linked_repo_id. Errors use the usual body, {"error": "<message>"}.

Terminal window
# All examples assume $TOKEN is an API key (hv_...)
AUTH="Authorization: Bearer $TOKEN"

A target is a place a lexicon can be published:

{
"kind": "linked_repo",
"linked_repo_id": "550e8400-e29b-41d4-a716-446655440000",
"did": "did:plc:abc123",
"handle": "partner.example.com",
"available": true,
"unavailable_reason": null,
"can_unpublish": true
}

kind is instance or linked_repo. available is false when nothing can be written there now, and unavailable_reason says why. can_unpublish is true when the target can also delete records.

A rule:

FieldTypeDescription
idstringRule ID
patternstring*, a lowercased prefix such as com.example.*, or an exact NSID
target_kindstringinstance or linked_repo
linked_repo_idstring/nullThe linked repo; null for the instance, or after its linked repo is removed
auto_publishbooleanPublish matching lexicons without a click
created_bystringDID of the admin who created it
created_atstringRFC 3339
updated_atstringRFC 3339

Rules returned by the API also carry a target object beside these fields.

GET /admin/lexicon-publishing

Permission: lexicons:read

Response:

{
"usable": true,
"targets": [],
"rules": [],
"lexicons": [
{
"nsid": "com.example.notes.getNote",
"layer": "manual",
"rule_id": "7c1e0f3a-0f7e-4c35-9f43-6d2b8d1a9f10",
"rule_pattern": "com.example.*",
"auto_publish": true,
"target": {},
"status": "outdated",
"publication": {
"nsid": "com.example.notes.getNote",
"did": "did:plc:abc123",
"uri": "at://did:plc:abc123/com.atproto.lexicon.schema/com.example.notes.getNote",
"cid": "bafyrei...",
"published_at": "2026-10-13T09:00:00+00:00",
"last_error": null,
"last_attempt_at": "2026-10-13T09:00:00+00:00",
"withdrawn_at": null
}
}
],
"orphans": []
}
  • usable: whether any target can publish, or can remove a record HappyView put there. While it’s false, the dashboard’s publishing page shows what to set up instead of its rules.

  • targets: the instance first, then one per linked repo.

  • rules: most specific first.

  • lexicons: every publishable lexicon, sorted by NSID. layer is manual or project; network lexicons never appear. rule_id, rule_pattern and target are null when no rule matches, and auto_publish is then false. status is one of no_rule, target_unavailable, unpublished, withdrawn, moved, outdated or current. publication is null until something has been attempted; it carries only the error columns when every attempt has failed. withdrawn_at is set when the lexicon was unpublished and has not been published since; such a lexicon reports withdrawn (unless no rule matches or its target is unavailable) and auto-publish skips it.

  • orphans: publication rows, in the same shape as publication, for records still in a repo whose lexicon is no longer publishable, such as one deleted after it was published. Unpublish works on them as on any other.

The overview computes each lexicon’s CID locally, so it makes no network calls.

GET /admin/lexicon-publishing/dns

Permission: lexicons:read

Looks up _lexicon.<authority> for every distinct authority among the publishable lexicons, eight at a time with a five-second timeout each. It’s separate from the overview because DNS is slow.

[
{
"authority": "notes.example.com",
"record_name": "_lexicon.notes.example.com",
"did": "did:plc:abc123",
"error": null
}
]

did is null when there’s no TXT record. error is set when the lookup itself failed, such as a timeout. Compare did with each lexicon’s target DID to see what needs adding.

GET /admin/lexicon-publishing/dns/{nsid}

Permission: lexicons:read

Looks up the _lexicon record for one lexicon’s authority, with the same five-second timeout, and says which DID it should name.

{
"nsid": "com.example.notes.getNote",
"authority": "notes.example.com",
"record_name": "_lexicon.notes.example.com",
"did": null,
"error": null,
"expected_did": "did:plc:abc123"
}

expected_did is the DID of the account the lexicon’s rule publishes to, or failing that the account already holding its record. It’s null for a lexicon HappyView doesn’t publish, or when neither is known. Without linked-repos:read, a rule’s linked repo isn’t named, so expected_did then comes only from a publication. An invalid NSID is a 400.

POST /admin/lexicon-publishing/rules

Permission: lexicons:publish

Body:

FieldTypeDescription
patternstringRequired. *, a prefix ending in .*, or an exact NSID
target_kindstringRequired. instance or linked_repo
linked_repo_idstringRequired for linked_repo; ignored for instance
auto_publishbooleanOptional, defaults to false
{
"pattern": "com.example.*",
"target_kind": "linked_repo",
"linked_repo_id": "550e8400-e29b-41d4-a716-446655440000",
"auto_publish": true
}

Returns 201 with the rule and its target.

StatusCause
400A malformed pattern, an unknown target_kind, a missing or unknown linked_repo_id, or a target that is unavailable (the body gives its reason)
403The target is a linked repo and the caller lacks linked-repos:read
409A rule for this pattern already exists. Patterns are compared after normalisation, so COM.EXAMPLE.* collides with com.example.*

After a successful create, HappyView starts an auto-publish run in the background.

PATCH /admin/lexicon-publishing/rules/{id}

Permission: lexicons:publish

Any of target_kind, linked_repo_id and auto_publish. The pattern can’t change; delete the rule and add another. The target is re-validated only when it changes, so you can still turn off auto-publish on a rule whose linked repo has gone away. Switching to instance clears linked_repo_id.

Returns 200 with the rule and its target, 403 when it changes the target to a linked repo without linked-repos:read, or 404 for an unknown ID.

DELETE /admin/lexicon-publishing/rules/{id}

Permission: lexicons:publish

Returns 204, or 404 for an unknown ID. Records already published are left alone, and an auto-publish run starts in the background, since a broader rule may now cover the lexicons this one did.

POST /admin/lexicon-publishing/publish

Permission: lexicons:publish

Body: { "nsids": ["com.example.notes.getNote"] }, between 1 and 500 NSIDs. An empty or longer list is a 400.

NSIDs are published one after another, in the order given. A success clears withdrawn_at, so a lexicon unpublished earlier returns to auto-publish. The response is 200 even when some fail:

{
"results": [
{
"nsid": "com.example.notes.getNote",
"ok": true,
"uri": "at://did:plc:abc123/com.atproto.lexicon.schema/com.example.notes.getNote",
"cid": "bafyrei...",
"error": null
},
{
"nsid": "com.example.other",
"ok": false,
"uri": null,
"cid": null,
"error": "not a lexicon HappyView can publish"
}
]
}
POST /admin/lexicon-publishing/unpublish

Permission: lexicons:publish

Body: { "nsid": "com.example.notes.getNote" }

Deletes the record from the account it was published to, clears the stored did, uri, cid and published_at, and sets withdrawn_at. Auto-publish then leaves the lexicon alone until a publish succeeds for it. Returns 204.

StatusCause
404The NSID isn’t published
409No linked repo or attached account holds the DID the record was published to; nothing is changed
401The attached account needs to sign in again, or a linked repo needs reauthorization
403The linked repo’s grant lacks the delete scope, or the attached account’s PDS refused the delete for lack of scope (re-authenticate it from Service Identity)
502The attached account’s PDS refused the delete for another reason

These actions are written to the event log: lexicon_publishing.rule_created, lexicon_publishing.rule_updated, lexicon_publishing.rule_deleted, lexicon.published, lexicon.publish_failed and lexicon.unpublished. A failed publish whose error matches the last one recorded for that lexicon updates last_attempt_at without logging another lexicon.publish_failed.