Skip to content

Documents API

The Documents API manages content with a publish/unpublish lifecycle. Documents exist in one of two states:

  • Published (type: "published") — live, public content.
  • Unpublished (type: "unpublished") — drafts, pending-review, archived, or trashed content distinguished by a status string.

Revisions record snapshots of published documents.

Resource Types

JSON:API typeDomain entityDescription
publishedDocumentLive published document
published-summaryDocumentSummaryPublished document without content
unpublishedUnpublishedDraft or otherwise unpublished document
unpublished-summaryUnpublishedSummaryUnpublished document without content
revisionRevisionImmutable historical snapshot
revision-summaryRevisionSummaryRevision without content

Endpoints


GET /

Returns meta-information about the Documents API and its available endpoints.

Response

json
{
  "data": {
    "type": "api-info",
    "id": "documents",
    "attributes": {
      "name": "Documents API",
      "version": "1.0.0",
      "endpoints": [
        {
          "path": "/openapi.json",
          "methods": ["GET"],
          "description": "OpenAPI 3.1 specification for this API"
        },
        {
          "path": "/openapi.yaml",
          "methods": ["GET"],
          "description": "OpenAPI 3.1 specification for this API, as YAML"
        },
        {
          "path": "/capabilities",
          "methods": ["GET"],
          "description": "Underlying documents repository capabilities"
        },
        {
          "path": "/sync-token",
          "methods": ["GET"],
          "description": "Get an opaque per-scope change token (capability-gated)"
        },
        {
          "path": "/changes",
          "methods": ["GET"],
          "description": "List changes since a sync token (capability-gated)"
        },
        {
          "path": "/records",
          "methods": ["GET"],
          "description": "List full records (published + unpublished view per key)"
        },
        {
          "path": "/record-summaries",
          "methods": ["GET"],
          "description": "List record summaries (lightweight listing)"
        },
        {
          "path": "/published",
          "methods": ["POST"],
          "description": "Create a published document"
        },
        {
          "path": "/published/{key}",
          "methods": ["GET", "PATCH", "DELETE"],
          "description": "Read, update, or remove a published document"
        },
        {
          "path": "/published/{key}/unpublish",
          "methods": ["POST"],
          "description": "State transition: move a published document to unpublished"
        },
        {
          "path": "/unpublished",
          "methods": ["POST"],
          "description": "Create an unpublished draft"
        },
        {
          "path": "/unpublished/{key}",
          "methods": ["GET", "PATCH", "DELETE"],
          "description": "Read, update, or remove an unpublished draft"
        },
        {
          "path": "/unpublished/{key}/publish",
          "methods": ["POST"],
          "description": "State transition: publish an unpublished draft"
        },
        {
          "path": "/revisions",
          "methods": ["POST"],
          "description": "Create a revision for a document"
        },
        {
          "path": "/revisions/{key}",
          "methods": ["GET"],
          "description": "List revisions for a document"
        },
        {
          "path": "/revisions/{key}/{revisionId}",
          "methods": ["GET"],
          "description": "Read a specific revision of a document"
        },
        {
          "path": "/operations",
          "methods": ["POST"],
          "description": "Atomic operations (add/update/remove + publish/unpublish transitions)"
        }
      ]
    }
  }
}

GET /openapi.json

Returns the OpenAPI 3.1 specification for the Documents API as a JSON document.

Response200 OK, Content-Type: application/json

The response body is an OpenAPI 3.1.0 document. The servers array is rewritten to the absolute mount point so the document is usable as-is by code generators and API clients.

json
{
  "openapi": "3.1.0",
  "info": { "title": "Documents API", "version": "1.0.0" },
  "servers": [{ "url": "https://example.com/api/documents" }],
  "paths": { ... }
}

GET /openapi.yaml

Returns the same OpenAPI 3.1 specification as GET /openapi.json, serialized as YAML.

Response200 OK, Content-Type: application/yaml


GET /records

List records (published and/or unpublished) with full content for a given collection folder.

Query Parameters

ParameterTypeRequiredDefaultDescription
filter[folder]stringno""Collection (folder) to list from, e.g. posts or posts/drafts. Omit or pass an empty string to list all collections as folder resources (one per configured document collection).
filter[type]"published" | "unpublished" | "all"no"published"Filter by document state
filter[depth]numberno1Traversal depth (minimum 1)
page[number]numberno1Page number for page-based pagination (1-based)
page[size]numberno10Items per page — combines with page[number], page[after], or page[before]
page[offset]numbernoZero-based item offset for offset-based pagination
page[limit]numbernoMaximum number of items to return — combines with page[offset]
page[after]stringnoForward cursor; rejected with 400 if the backend does not support cursor pagination (check GET /capabilities)
page[before]stringnoBackward cursor; rejected with 400 if the backend does not support cursor pagination (check GET /capabilities)

Response — mixed array of published and unpublished resources

json
{
  "data": [
    {
      "type": "published",
      "id": "posts/hello-world",
      "attributes": {
        "type": "published",
        "status": "published",
        "language": "en",
        "content": {
          "title": "Hello World",
          "body": "This is my first post."
        },
        "createdAt": "2024-01-15T10:30:00Z",
        "updatedAt": "2024-01-16T08:00:00Z"
      }
    },
    {
      "type": "unpublished",
      "id": "posts/draft-post",
      "attributes": {
        "type": "unpublished",
        "status": "draft",
        "language": "en",
        "content": {
          "title": "Draft Post",
          "body": "Work in progress."
        },
        "createdAt": "2024-01-17T12:00:00Z",
        "updatedAt": "2024-01-17T12:00:00Z"
      }
    }
  ]
}

GET /record-summaries

List record summaries (without content) for a given collection folder. Accepts the same query parameters as GET /records. When filter[folder] is omitted or empty, returns one folder-summary resource per configured document collection.

Response — mixed array of published-summary and unpublished-summary resources

json
{
  "data": [
    {
      "type": "published-summary",
      "id": "posts/hello-world",
      "attributes": {
        "type": "published-summary",
        "status": "published",
        "createdAt": "2024-01-15T10:30:00Z",
        "updatedAt": "2024-01-16T08:00:00Z"
      }
    },
    {
      "type": "unpublished-summary",
      "id": "posts/draft-post",
      "attributes": {
        "type": "unpublished-summary",
        "status": "draft",
        "createdAt": "2024-01-17T12:00:00Z",
        "updatedAt": "2024-01-17T12:00:00Z"
      }
    }
  ]
}

GET /published/:key

Get a single published document by key.

Path Parameters

ParameterTypeDescription
keystringDocument key (URL-encoded)

Response

json
{
  "data": {
    "type": "published",
    "id": "posts/hello-world",
    "attributes": {
      "type": "published",
      "status": "published",
      "language": "en",
      "content": {
        "title": "Hello World",
        "body": "This is my first post."
      },
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-16T08:00:00Z"
    }
  }
}

Error Response404 Not Found

json
{
  "errors": [
    {
      "status": "404",
      "code": "not_found",
      "detail": "Document not found"
    }
  ]
}

POST /published

Create a new published document directly.

Request Headers

Content-Type: application/vnd.api+json

Request Body

json
{
  "data": {
    "type": "published",
    "id": "posts/hello-world",
    "attributes": {
      "language": "en",
      "content": {
        "title": "Hello World",
        "body": "This is my first post."
      }
    }
  }
}
FieldTypeRequiredDescription
data.type"published"yesResource type
data.idstringyesDocument key (e.g. "posts/my-doc")
data.attributes.languagestringnoBCP 47 language tag (e.g. "en"); defaults to "und" (undetermined)
data.attributes.contentobjectnoArbitrary document content

Response201 Created with the created document

json
{
  "data": {
    "type": "published",
    "id": "posts/hello-world",
    "attributes": {
      "type": "published",
      "status": "published",
      "language": "en",
      "content": {
        "title": "Hello World",
        "body": "This is my first post."
      },
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-15T10:30:00Z"
    }
  }
}

PATCH /published/:key

Update an existing published document.

Path Parameters

ParameterTypeDescription
keystringDocument key (URL-encoded)

Request Headers

Content-Type: application/vnd.api+json

Request Body

json
{
  "data": {
    "type": "published",
    "id": "posts/hello-world",
    "attributes": {
      "content": {
        "title": "Hello World (v2)",
        "body": "Updated content."
      }
    }
  }
}

Response — updated document (same shape as GET /published/:key)


DELETE /published/:key

Delete a published document.

Path Parameters

ParameterTypeDescription
keystringDocument key (URL-encoded)

Response200 OK

json
{
  "meta": {
    "deleted": true
  }
}

POST /published/:key/unpublish

Move a published document to the unpublished state with the given status.

Path Parameters

ParameterTypeDescription
keystringPublished document key (URL-encoded)

Request Headers

Content-Type: application/vnd.api+json

Request Body

json
{
  "data": {
    "type": "unpublished",
    "attributes": {
      "status": "archived"
    }
  }
}
FieldTypeRequiredDescription
data.type"unpublished"yesResource type
data.attributes.statusstringyesTarget unpublished status (e.g. "archived", "trash")

Response — resulting unpublished document

json
{
  "data": {
    "type": "unpublished",
    "id": "posts/hello-world",
    "attributes": {
      "type": "unpublished",
      "status": "archived",
      "language": "en",
      "content": {
        "title": "Hello World",
        "body": "This is my first post."
      },
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-18T09:00:00Z"
    }
  }
}

GET /unpublished/:key

Get a single unpublished document by key.

Path Parameters

ParameterTypeDescription
keystringUnpublished document key (URL-encoded)

Response

json
{
  "data": {
    "type": "unpublished",
    "id": "posts/draft-post",
    "attributes": {
      "type": "unpublished",
      "status": "draft",
      "language": "en",
      "content": {
        "title": "Draft Post",
        "body": "Work in progress."
      },
      "createdAt": "2024-01-17T12:00:00Z",
      "updatedAt": "2024-01-17T12:00:00Z"
    }
  }
}

POST /unpublished

Create a new unpublished document (draft or other unpublished status).

Request Headers

Content-Type: application/vnd.api+json

Request Body

json
{
  "data": {
    "type": "unpublished",
    "id": "posts/draft-post",
    "attributes": {
      "status": "draft",
      "language": "en",
      "content": {
        "title": "Draft Post",
        "body": "Work in progress."
      }
    }
  }
}
FieldTypeRequiredDescription
data.type"unpublished"yesResource type
data.idstringyesDocument key (e.g. "posts/my-doc")
data.attributes.statusstringyesInitial status (e.g. "draft")
data.attributes.languagestringnoBCP 47 language tag; defaults to "und" (undetermined)
data.attributes.contentobjectnoArbitrary document content

Response201 Created — created unpublished document (same shape as GET /unpublished/:key)


PATCH /unpublished/:key

Update an existing unpublished document.

Path Parameters

ParameterTypeDescription
keystringUnpublished document key (URL-encoded)

Request Headers

Content-Type: application/vnd.api+json

Request Body

json
{
  "data": {
    "type": "unpublished",
    "id": "posts/draft-post",
    "attributes": {
      "status": "draft",
      "content": {
        "title": "Draft Post (Revised)",
        "body": "Revised content."
      }
    }
  }
}

Response — updated unpublished document (same shape as GET /unpublished/:key)


DELETE /unpublished/:key

Delete an unpublished document permanently.

Path Parameters

ParameterTypeDescription
keystringUnpublished document key (URL-encoded)

Response200 OK

json
{
  "meta": {
    "deleted": true
  }
}

POST /unpublished/:key/publish

Publish an unpublished document. Moves it to published state.

Path Parameters

ParameterTypeDescription
keystringUnpublished document key (URL-encoded)

Request Body — none required

Response — resulting published document

json
{
  "data": {
    "type": "published",
    "id": "posts/draft-post",
    "attributes": {
      "type": "published",
      "status": "published",
      "language": "en",
      "content": {
        "title": "Draft Post (Revised)",
        "body": "Revised content."
      },
      "createdAt": "2024-01-17T12:00:00Z",
      "updatedAt": "2024-01-18T14:00:00Z"
    }
  }
}

POST /revisions

Create a revision snapshot for a document.

Request Headers

Content-Type: application/vnd.api+json

Request Body

json
{
  "data": {
    "type": "revision",
    "id": "posts/hello-world",
    "attributes": {
      "revision": "v1.0.0",
      "language": "en",
      "content": {
        "title": "Hello World",
        "body": "Original content."
      }
    }
  }
}
FieldTypeRequiredDescription
data.type"revision"yesResource type
data.idstringyesDocument key (e.g. posts/my-doc)
data.attributes.revisionstringyesRevision identifier (e.g. a version tag or hash)
data.attributes.languagestringnoBCP 47 language tag; defaults to "und" (undetermined)
data.attributes.contentobjectnoSnapshot of the document content

Response201 Created — created revision

json
{
  "data": {
    "type": "revision",
    "id": "posts/hello-world",
    "attributes": {
      "type": "revision",
      "revision": "v1.0.0",
      "language": "en",
      "content": {
        "title": "Hello World",
        "body": "Original content."
      },
      "createdAt": "2024-01-15T10:30:00Z"
    }
  }
}

GET /revisions/:key

List revision summaries for a document key.

Path Parameters

ParameterTypeDescription
keystringDocument key (URL-encoded)

Query Parameters

ParameterTypeDefaultDescription
page[after]stringForward cursor for pagination
page[before]stringBackward cursor for pagination
page[size]numberItems per page; max 100, clamped silently

Response — collection of revision-summary resources

json
{
  "data": [
    {
      "type": "revision-summary",
      "id": "posts/hello-world",
      "attributes": {
        "type": "revision-summary",
        "revision": "v1.0.0",
        "createdAt": "2024-01-15T10:30:00Z"
      }
    },
    {
      "type": "revision-summary",
      "id": "posts/hello-world",
      "attributes": {
        "type": "revision-summary",
        "revision": "v1.1.0",
        "createdAt": "2024-01-16T08:00:00Z"
      }
    }
  ],
  "links": {
    "self": "http://localhost:3001/revisions/posts%2Fhello-world",
    "first": "http://localhost:3001/revisions/posts%2Fhello-world",
    "next": null,
    "prev": null,
    "last": null
  },
  "meta": {
    "page": {
      "total": 2
    }
  }
}

GET /revisions/:key/:revisionId

Get a single revision by document key and revision identifier.

Path Parameters

ParameterTypeDescription
keystringDocument key (URL-encoded)
revisionIdstringRevision identifier (e.g. v1.0.0)

Response

json
{
  "data": {
    "type": "revision",
    "id": "posts/hello-world",
    "attributes": {
      "type": "revision",
      "revision": "v1.0.0",
      "language": "en",
      "content": {
        "title": "Hello World",
        "body": "Original content."
      },
      "createdAt": "2024-01-15T10:30:00Z"
    }
  }
}

POST /operations

Execute a fail-fast batch of operations on documents. Supports adding published or unpublished documents, state transitions (publish/unpublish), content updates, and removals.

All operations are validated for request shape before any I/O. A shape-invalid batch returns 400 with zero writes. Valid batches are applied sequentially; the first repository failure stops processing. A mid-batch repository failure leaves previously-applied ops applied — this endpoint is a fail-fast batch, not a transaction.

Request Headers

Content-Type: application/vnd.api+json

Supported Operations

opRequired fieldsDescription
adddata with type: "unpublished" or "published"Create a document
updatedata with type: "unpublished" and an idUpdate unpublished content
updatehref: "/publish", ref: { type: "unpublished", id }Publish an unpublished document
updatehref: "/unpublish", ref: { type: "document", id }, data.attributes.statusUnpublish a published document
removeref with type: "document" or "unpublished"Delete a document

Request Body

json
{
  "operations": [
    {
      "op": "add",
      "data": {
        "type": "unpublished",
        "id": "posts/new-draft",
        "attributes": {
          "status": "draft",
          "language": "en",
          "content": { "title": "New Draft" }
        }
      }
    },
    {
      "op": "update",
      "href": "/publish",
      "ref": {
        "type": "unpublished",
        "id": "posts/ready-to-publish"
      }
    },
    {
      "op": "update",
      "href": "/unpublish",
      "ref": {
        "type": "document",
        "id": "posts/outdated"
      },
      "data": {
        "type": "unpublished",
        "attributes": {
          "status": "archived"
        }
      }
    },
    {
      "op": "update",
      "data": {
        "type": "unpublished",
        "id": "posts/new-draft",
        "attributes": {
          "status": "pending_review",
          "content": { "title": "New Draft (Updated)" }
        }
      }
    },
    {
      "op": "remove",
      "ref": {
        "type": "document",
        "id": "posts/to-delete"
      }
    }
  ]
}

Response

Results are returned in the same order as the applied operations (may be fewer than submitted if processing stopped at a failure). Remove operations return a meta entry.

json
{
  "results": [
    {
      "data": {
        "type": "unpublished",
        "id": "posts/new-draft",
        "attributes": {
          "type": "unpublished",
          "status": "draft",
          "language": "en",
          "content": { "title": "New Draft" },
          "createdAt": "2024-01-18T09:00:00Z",
          "updatedAt": "2024-01-18T09:00:00Z"
        }
      }
    },
    {
      "data": {
        "type": "published",
        "id": "posts/ready-to-publish",
        "attributes": {
          "type": "published",
          "status": "published",
          "language": "en",
          "content": { "title": "Ready to Publish" },
          "createdAt": "2024-01-17T10:00:00Z",
          "updatedAt": "2024-01-18T09:00:00Z"
        }
      }
    },
    {
      "data": {
        "type": "unpublished",
        "id": "posts/outdated",
        "attributes": {
          "type": "unpublished",
          "status": "archived",
          "language": "en",
          "content": { "title": "Outdated Post" },
          "createdAt": "2024-01-10T08:00:00Z",
          "updatedAt": "2024-01-18T09:00:00Z"
        }
      }
    },
    {
      "data": {
        "type": "unpublished",
        "id": "posts/new-draft",
        "attributes": {
          "type": "unpublished",
          "status": "pending_review",
          "language": "en",
          "content": { "title": "New Draft (Updated)" },
          "createdAt": "2024-01-18T09:00:00Z",
          "updatedAt": "2024-01-18T09:05:00Z"
        }
      }
    },
    {
      "meta": {
        "deleted": true,
        "ref": {
          "type": "document",
          "id": "posts/to-delete"
        }
      }
    }
  ]
}

Error entries (when a repository operation fails mid-batch)

json
{
  "results": [
    {
      "errors": [
        {
          "status": "404",
          "title": "Operation Failed",
          "detail": "Document not found: posts/missing"
        }
      ]
    }
  ]
}

GET /sync-token

Returns an opaque sync token representing the current state of the documents repository (or a specific folder). Use the token with GET /changes to poll for changes since a known point.

Capability-gated — backends that do not support change signals return 501 Not Implemented. Check GET /capabilities before calling this endpoint.

Query Parameters

ParameterTypeRequiredDescription
filter[folder]stringnoScope the token to a specific folder (e.g. posts). Omit to get a repo-wide token.

Response — a single sync-token resource

json
{
  "data": {
    "type": "sync-token",
    "id": "self",
    "attributes": {
      "syncToken": "eyJzZXEiOjQyfQ=="
    }
  }
}

When scoped to a folder the response includes a folder attribute:

json
{
  "data": {
    "type": "sync-token",
    "id": "posts",
    "attributes": {
      "syncToken": "eyJzZXEiOjQyfQ==",
      "folder": "posts"
    }
  }
}

Error Responses

StatusCondition
501Backend does not support change signals (NotImplementedError)
403Caller is not authorised to call getSyncToken (AuthorizationError)

GET /changes

List change events since a previously obtained sync token. Returns one change-summary resource per changed key, and a new meta.syncToken to use on the next poll.

Capability-gated — backends that do not support change signals return 501 Not Implemented. Check GET /capabilities before calling this endpoint.

Query Parameters

ParameterTypeRequiredDescription
filter[since]stringyesSync token from a prior GET /sync-token or GET /changes response. Returns 400 if absent.
filter[folder]stringnoRestrict change feed to a specific folder. Should match the folder used to obtain the token.

Response — collection of change-summary resources

json
{
  "data": [
    {
      "type": "change-summary",
      "id": "posts/hello-world",
      "attributes": {
        "deleted": false,
        "version": "abc123"
      }
    },
    {
      "type": "change-summary",
      "id": "posts/old-draft",
      "attributes": {
        "deleted": true
      }
    }
  ],
  "meta": {
    "page": { "total": 2 },
    "syncToken": "eyJzZXEiOjQzfQ=="
  }
}

The meta.syncToken value is the cursor for the next call. attributes.version is present only when the backend includes per-document version tokens; attributes.deleted: true indicates a key was removed since the prior token.

Error Responses

StatusCondition
400filter[since] is absent
501Backend does not support change signals (NotImplementedError)
403Caller is not authorised to call listChanges (AuthorizationError)

Released under the MIT License.