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
| Parameter | Type | Default | Description |
|---|---|---|---|
team | string | — | Team slug. Required unless the API key is already scoped to one team. |
search | string | — | Fuzzy full-text search across title and author. Reach for this first when looking something up by name. |
title | string | — | Case-insensitive substring match on the title: ?title=Misericordia matches “Dives in Misericordia”. |
title_match | string | contains | Pass exact to match the whole title instead of a substring. Requires title. |
author | string | — | Case-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. |
id | string | — | A document UUID. |
urn | string | — | Exact URN. URNs are unique, so this returns at most one document. |
include_variants | boolean | automatic | Lookups (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. |
isbn | string | — | ISBN-10 or ISBN-13; punctuation is ignored. |
publication_date | string | — | Exact publication date. |
published_from | string | — | Only documents published at or after this timestamp. |
categories | string | — | Category tag. Repeatable. |
status | string | published | Filter by status, e.g. unpublished. Pass any for every status. |
scope | string | — | private, shared, organization, own, or any. |
parent_document_id | string | — | Returns the variant children of this document. |
expand | string | — | Pass 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=anywhile an ingest is still in review. - Unfiltered listings return only top-level documents. A lookup by
search,title,urn,id, orisbnalso considers matching variant children, so linking a translation does not make it undiscoverable. Passinclude_variants=trueto enumerate variants alongside their parents,include_variants=falseto force a canonical-only lookup, or?parent_document_id=<parent-id>to list one work’s variants on their own. Every variant row carriesparent_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
| Parameter | Type | Default | Description |
|---|---|---|---|
per_page | number | 10 | Documents per page, from 1 to 100. |
cursor | string | — | The pagination.nextCursor string from the previous response, passed back verbatim. |
page | number | — | A 1-based page number. Implies count=exact, and cannot be combined with cursor. |
count | string | — | Pass exact to fill in count and pagination.pageCount. Off by default: an exact total costs an extra pass over every match. |
sort | string | title_asc | One 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
| Status | Cause |
|---|---|
400 | An 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. |
401 | Missing or invalid API key. |
Get a document
GET /api/documents/{document_id}
Path parameters
| Parameter | Type | Description |
|---|---|---|
document_id | string | The document’s UUID. |
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
expand | string | — | Pass 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:
| Field | Type | Description |
|---|---|---|
title | string | null | Document title (max 1000 chars). |
author | string | null | Author (max 500 chars). |
publication_date | string | null | Publication date. |
publisher | string | null | Publisher (max 200 chars). |
isbn / isbn_13 | string | null | ISBN-10 / ISBN-13. |
sku | string | null | External identifier (max 100 chars). Not required to be unique. |
language | string | null | Language code. |
license | string | null | License. |
scope | string | Visibility: private, shared, or organization. Only organization managers can set organization. |
categories | string[] | null | Category tags. |
collections | string[] | null | Bibliographic series or set the work was published in. Free text, not collection ids. |
document_collections | string[] | null | The collection ids this document belongs to. Replaces the whole set; omitted ids are unlinked. |
urn | string | null | Canonical URN (max 500 chars; must be unique). |
custom_properties | object | null | Arbitrary metadata, validated against the team’s custom-property definitions. |
parent_document_id | string | null | UUID of this variant’s parent document. Clearing it also clears variant_type unless that field is supplied in the same request. |
variant_type | canonical | edition | language | null | Variant 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:
| Endpoint | Method | Description |
|---|---|---|
/api/documents/{document_id}/content | GET | Read the extracted content as TEI/XML or Markdown. |
/api/documents/{document_id}/content | PATCH | Replace the content or apply targeted string edits. |
/api/documents/{document_id}/publish | POST | Publish or republish the current content. |
/api/documents/{document_id}/unpublish | POST | Take 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
}
| Status | Meaning |
|---|---|
200 | Document deleted. |
404 | Document not found or not accessible. |
500 | Deletion failed (the response error.message describes the failure). |