Projects API
Public, read-only JSON API that lists published docs sets with their technology stacks.
Projects API
The projects API lets other sites, such as a portfolio, list the products a docs site documents. It returns each published docs set's title, description, SEO metadata, canonical URLs, docs group and technology stack, and nothing else.
It is disabled by default. Enable it in the plugin config:
1payloadMarkdownDocs({2 publicApi: {3 projects: true,4 },5})
Once it is enabled, every published docs set is listed. There is no separate allowlist: drafts and docs sets that were never published are never listed. The consumer package reads the API from any number of sites.
Endpoints
1GET /api/documentation/projects2GET /api/documentation/projects?group=libraries,tools3GET /api/documentation/projects/:slug
group limits the list to docs sets in those docs groups (by group slug).
Change the path with publicApi.projects.path:
1payloadMarkdownDocs({2 publicApi: {3 projects: { path: '/portfolio/projects' },4 },5})
Response
1{2 "schemaVersion": 1,3 "groups": [{ "slug": "products", "title": "Products" }],4 "projects": [5 {6 "slug": "vaulthalla",7 "title": "Vaulthalla",8 "description": "The Final Cloud — encrypted storage.",9 "group": { "slug": "products", "title": "Products" },10 "seo": {11 "title": "Vaulthalla — encrypted cloud storage",12 "description": "The Final Cloud — encrypted storage.",13 "image": { "url": "https://vaulthalla.io/api/media/file/og.png", "alt": "Vaulthalla", "width": 1200, "height": 630 }14 },15 "urls": {16 "product": "https://vaulthalla.io/products/vaulthalla",17 "docs": "https://vaulthalla.io/products/vaulthalla"18 },19 "stack": [20 { "category": "languages", "items": [{ "name": "C++23" }, { "name": "TypeScript" }] },21 { "category": "databases", "items": [{ "name": "PostgreSQL" }] }22 ],23 "updatedAt": "2026-10-18T12:00:00.000Z",24 "lastSyncedAt": "2026-10-18T12:00:00.000Z"25 }26 ]27}
seois what the docs set's own page uses in its metadata: the plugin-seometavalues, falling back to the docs set's nav title or title and its description.imageisnullwhen none is set.urlsare absolute. They use the same public origin as the generatedllms.txtfiles (see Public URLs In Generated Files).docsdiffers fromproductforproduct-nesteddocs sets.groupisnullfor ungrouped docs sets.groupslists the groups that have listed projects, in display order.stackis[]when the docs set has nostack.toml.- Projects are ordered by group, then by title.
/:slug answers { "schemaVersion": 1, "project": { ... } }. When the slug is
not a published docs set, it answers 404 with
{ "schemaVersion": 1, "error": { "code": "not_found", "message": "..." } }.
The response contains only these fields. Repository bindings, OIDC and workflow
settings, branches, sync internals and admin-only fields are never exposed. The
docs-sets REST collection stays admin-only.
Caching
Responses carry a strong ETag: the SHA-256 of the response body. Any change to
a listed docs set, its group or its URLs changes it, and so does adding,
unpublishing or deleting a docs set. Requests with a matching If-None-Match
get 304 Not Modified.
1Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400
Compatibility
schemaVersion covers the wire format only. It is independent of the npm
version. Within a schema version, fields are only added, and clients ignore
fields they do not know. A breaking change bumps schemaVersion, so a client
that expects version 1 reports a mismatch instead of misreading data.
Consumer package
payload-markdown-docs-consumer (also published as
@valkyrianlabs/payload-markdown-docs-consumer) is a dependency-free client for
other sites. It needs no Payload, Next.js or React. It fetches several sites at
once, validates schemaVersion, keeps every key scoped to its site, and reports
sites that fail without hiding the others:
1import { createDocsProjectsClient } from 'payload-markdown-docs-consumer'2 3const client = createDocsProjectsClient({4 sites: [5 { id: 'vaulthalla', baseUrl: 'https://vaulthalla.io' },6 { id: 'valkyrianlabs', baseUrl: 'https://valkyrianlabs.com' },7 ],8 fetchOptions: { next: { revalidate: 300 } },9})10 11const { sites, projects, complete, errors } = await client.getProjects()
sites keeps the configured order. Each site has groups (each with its
projects) and ungrouped, and projects is the flattened list. Project
keys such as valkyrianlabs/docs stay unique when two sites use the same slug.
The package README documents every option and error code. The package is
released together with payload-markdown-docs and carries the same version.