Documents API

Browse and manage documents after they have been ingested: list a team’s library, fetch a document and its metadata, update metadata fields, or delete a document and all of its associated data. To read or edit a document’s content, and to publish it, see Reviewing and Editing Documents.

Authentication

All endpoints require a Bearer token. See Getting Started.

Authorization: Bearer <your-api-key>

Access is scoped to your team: a document that does not belong to your team responds with 404.

List documents

GET /api/documents

Lists the documents in your team’s library, newest ingests included. Use it to find a document’s id before calling any of the single-document endpoints below.

Query parameters

Unrecognized parameters are rejected with a 400 that names them and lists what is supported. A filter this endpoint does not know is never dropped silently, so a listing that comes back with results is one that ran the query you wrote.

Filters

ParameterTypeDefaultDescription
teamstringTeam slug. Required unless the API key is already scoped to one team.
searchstringFuzzy full-text search across title and author. Reach for this first when looking something up by name.
titlestringCase-insensitive substring match on the title: ?title=Misericordia matches “Dives in Misericordia”.
title_matchstringcontainsPass exact to match the whole title instead of a substring. Requires title.
authorstringCase-insensitive author match. Every word you supply must appear, in any order, so ?author=Alphonsus Liguori also matches “Alphonsus Maria de Liguori”. Repeatable — a document matching any one of the values is returned.
idstringA document UUID.
urnstringExact URN. URNs are unique, so this returns at most one document.
include_variantsbooleanautomaticLookups (search, title, urn, id, or isbn) include matching variants automatically; unfiltered listings do not. Pass true to enumerate variants or false to force a canonical-only lookup. Cannot be combined with parent_document_id when true.
isbnstringISBN-10 or ISBN-13; punctuation is ignored.
publication_datestringExact publication date.
published_fromstringOnly documents published at or after this timestamp.
categoriesstringCategory tag. Repeatable.
statusstringpublishedFilter by status, e.g. unpublished. Pass any for every status.
scopestringprivate, shared, organization, own, or any.
parent_document_idstringReturns the variant children of this document.
expandstringPass jobs to include each document’s recent ingest jobs.

Two defaults are worth knowing, because both make the listing smaller than you might expect:

  • Only published documents are returned. Pass status=any while an ingest is still in review.
  • Unfiltered listings return only top-level documents. A lookup by search, title, urn, id, or isbn also considers matching variant children, so linking a translation does not make it undiscoverable. Pass include_variants=true to enumerate variants alongside their parents, include_variants=false to force a canonical-only lookup, or ?parent_document_id=<parent-id> to list one work’s variants on their own. Every variant row carries parent_document_id, so it remains linked to the canonical work.

To enumerate a team in full, turn both defaults off:

curl -H "Authorization: Bearer $VULGATE_TEAM_API_KEY" \
  "https://vulgate.ai/api/documents?team=my-team&status=any&include_variants=true&per_page=100&count=exact"

Pagination

ParameterTypeDefaultDescription
per_pagenumber10Documents per page, from 1 to 100.
cursorstringThe pagination.nextCursor string from the previous response, passed back verbatim.
pagenumberA 1-based page number. Implies count=exact, and cannot be combined with cursor.
countstringPass exact to fill in count and pagination.pageCount. Off by default: an exact total costs an extra pass over every match.
sortstringtitle_ascOne of title_asc, title_desc, date_asc, date_desc, or relevance (only meaningful with search).

Response

{
  "data": [
    {
      "id": "doc-xyz789",
      "title": "Homo Apostolicus",
      "author": "Alphonsus de Ligorio",
      "status": "published",
      "urn": "urn:cts:theology:homoApostolicus.1759",
      "parent_document_id": null,
      "variant_type": null
    }
  ],
  "error": null,
  "count": null,
  "pagination": {
    "pageIndex": 0,
    "pageSize": 10,
    "pageCount": null,
    "hasMore": true,
    "nextCursor": "WzAsImRvYy14eXo3ODkiLG51bGwsIkhvbW8gQXBvc3RvbGljdXMiXQ"
  }
}

count and pagination.pageCount are null unless you asked for count=exact — there is no total to divide into pages until an exact count is requested. Use pagination.hasMore and pagination.nextCursor to decide whether to keep going.

Paging through every document

Cursor pagination is the default and has no depth limit. Follow pagination.nextCursor until pagination.hasMore is false:

# First page
curl -H "Authorization: Bearer $VULGATE_TEAM_API_KEY" \
  "https://vulgate.ai/api/documents?team=my-team&status=any&per_page=100"
 
# Each page after that, passing the previous response's nextCursor
curl -H "Authorization: Bearer $VULGATE_TEAM_API_KEY" \
  "https://vulgate.ai/api/documents?team=my-team&status=any&per_page=100&cursor=<nextCursor>"

nextCursor is opaque — send it back exactly as received. The pagination.cursor object alongside it reports which row the cursor points at, for debugging only; it is not a valid value for the cursor parameter. An unusable cursor is a 400, never a silent reset to the first page.

A cursor is emitted only when at least one more row exists. Stop when pagination.hasMore is false; there is no empty trailing page to request.

For a numbered pager instead, use page and read the real total from pagination.pageCount:

curl -H "Authorization: Bearer $VULGATE_TEAM_API_KEY" \
  "https://vulgate.ai/api/documents?team=my-team&status=any&per_page=50&page=2"

Resolving a title to a document ID

Catalogue reconciliation needs an ID per row, not a browse. Match the exact title rather than searching for it — search is deliberately fuzzy and will happily return a different work that shares one word:

curl -G -H "Authorization: Bearer $VULGATE_TEAM_API_KEY" \
  --data-urlencode "team=my-team" \
  --data-urlencode "title=Uniformity with God's Will" \
  --data-urlencode "title_match=exact" \
  --data-urlencode "status=any" \
  "https://vulgate.ai/api/documents"

If the match is a translation or edition, its row includes the canonical work’s parent_document_id. Only matching variants are added to lookup results; the other translations of that work are not injected into the result set.

If you already hold a URN, ?urn= is exact and returns at most one document. Variant URNs are considered automatically. Pass status=any when the document may still be in review.

Author spellings vary across a corpus — the same person may be catalogued as “Alphonsus Liguori”, “Alphonsus Maria de Liguori” and “Alfonso Maria de’ Liguori”. author matches on every word you supply, so the first of those finds the second; GET /api/documents/authors lists the spellings actually in use, with counts.

Errors

StatusCause
400An unknown parameter, a per_page outside 1-100, a malformed cursor, cursor combined with page, include_variants combined with parent_document_id, title_match without title, a non-UUID id or parent_document_id, or a missing team. error.code identifies which, and error.message names the offending parameter — including the supported spelling when a common alias such as limit or offset is used.
401Missing or invalid API key.

Get a document

GET /api/documents/{document_id}

Path parameters

ParameterTypeDescription
document_idstringThe document’s UUID.

Query parameters

ParameterTypeDefaultDescription
expandstringPass files to include the document’s file metadata in the response. May be repeated.

Response

{
  "data": {
    "id": "doc-xyz789",
    "title": "Annual Report 2024",
    "author": null,
    "scope": "organization",
    "status": "published",
    "sku": "dbost",
    "custom_properties": { "source_code": "dbost" },
    "document_format": "PDF",
    "parent_document_id": null,
    "variant_type": "canonical",
    "children": [
      {
        "id": "doc-edition-2",
        "title": "Annual Report 2024, second edition",
        "variant_type": "edition",
        "status": "published",
        "urn": null,
        "language": "en"
      }
    ]
  },
  "error": null
}

children is always included as a lightweight array of the document’s directly linked variants. It contains only children visible to the caller and is empty when there are none.

Returns 404 if the document does not exist or is not accessible.

Update a document

Update document metadata. Only the fields included in the request body are changed.

PATCH /api/documents/{document_id}

Request body

Send any subset of the following fields:

FieldTypeDescription
titlestring | nullDocument title (max 1000 chars).
authorstring | nullAuthor (max 500 chars).
publication_datestring | nullPublication date.
publisherstring | nullPublisher (max 200 chars).
isbn / isbn_13string | nullISBN-10 / ISBN-13.
skustring | nullExternal identifier (max 100 chars). Not required to be unique.
languagestring | nullLanguage code.
licensestring | nullLicense.
scopestringVisibility: private, shared, or organization. Only organization managers can set organization.
categoriesstring[] | nullCategory tags.
collectionsstring[] | nullBibliographic series or set the work was published in. Free text, not collection ids.
document_collectionsstring[] | nullThe collection ids this document belongs to. Replaces the whole set; omitted ids are unlinked.
urnstring | nullCanonical URN (max 500 chars; must be unique).
custom_propertiesobject | nullArbitrary metadata, validated against the team’s custom-property definitions.
parent_document_idstring | nullUUID of this variant’s parent document. Clearing it also clears variant_type unless that field is supplied in the same request.
variant_typecanonical | edition | language | nullVariant relationship. canonical must have no parent; edition and language require parent_document_id.

Response

{
  "data": { "id": "doc-xyz789", "title": "Annual Report 2024", "sku": "dbost" },
  "error": null
}

The response also reports ignored_fields (any keys you sent that are not recognized — check it to catch typos), document_status, and needs_republish, which is always false here: metadata edits apply immediately and never require a republish.

Returns 400 for invalid fields (including a duplicate urn) and 404 if the document is not found.

Content and publishing

A document’s content is read and edited through its own endpoints, and a document is only searchable once it has been published:

EndpointMethodDescription
/api/documents/{document_id}/contentGETRead the extracted content as TEI/XML or Markdown.
/api/documents/{document_id}/contentPATCHReplace the content or apply targeted string edits.
/api/documents/{document_id}/publishPOSTPublish or republish the current content.
/api/documents/{document_id}/unpublishPOSTTake a published document out of the library, keeping its ingest.

Content edits never go live on their own — publish again to roll them out. See Reviewing and Editing Documents for the full reference.

Delete a document

Permanently deletes a document and all of its associated data — storage files, cover images, jobs, sections, and parts.

This cannot be undone. If you only want the document out of the library, unpublish it instead — that keeps the ingest and can be reversed by publishing again.

DELETE /api/documents/{document_id}

Response

{
  "data": { "id": "doc-xyz789" },
  "error": null
}
StatusMeaning
200Document deleted.
404Document not found or not accessible.
500Deletion failed (the response error.message describes the failure).