Skip to content
Foliosodev
Menu
On this page

REST API

Pages

List, search, read, create, update, move, tag and delete pages through the API. Pages are identified by their slug, and their content can be read and written as TipTap JSON or as Markdown.

All examples use https://acme.folioso.dev/api/v1; use your organisation's address instead.

The page object

json
{
  "id": 42,
  "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b",
  "title": "Deploying the API",
  "excerpt": "How we ship the API service to production, step by step.",
  "visibility": "space",
  "space": {
    "slug": "engineering",
    "name": "Engineering"
  },
  "group": {
    "id": 12,
    "name": "Runbooks"
  },
  "tags": [
    { "id": 31, "name": "php" }
  ],
  "content": {
    "type": "doc",
    "content": [
      {
        "type": "paragraph",
        "content": [{ "type": "text", "text": "How we ship the API service to production, step by step." }]
      }
    ]
  },
  "contentEpoch": 0,
  "url": "https://acme.folioso.dev/engineering/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b",
  "createdAt": "2026-10-01T14:03:11+00:00",
  "updatedAt": "2026-10-16T09:12:44+00:00"
}
Field Type Description
id integer The page's number. Every endpoint takes the slug, not this; use the id to link to the page from another page's content, as a folioso:page link or a pageLink node.
slug string The page's identifier, a UUID. Use it in /pages/{slug}.
title string The title. Can be empty for an untitled page.
excerpt string or null A short plain-text preview of the content. null until the page has been indexed for search since it last changed, which can take up to about 40 minutes. A rename or a move counts as a change, as well as an edit. Always null on recycle bin items.
visibility string space (visible to the space's members) or private (visible to the author and the people it's shared with).
space object The space's slug and name.
group object or null The group's id and name, or null if the page isn't in a group.
tags array The page's tags, each with id and name. Only returned for a single page.
content object or null The page body as a TipTap JSON document. Only returned for a single page.
markdown string or null The page body as Markdown, in place of content, when you ask for ?format=markdown. See Content formats.
contentEpoch integer Changes when the page is restored from a snapshot or its body is changed through the API. Returned with content or markdown. See Avoid overwriting a restore.
url string The page's address in the app.
createdAt, updatedAt string ISO 8601 timestamps.

Lists leave out tags, the body and contentEpoch, to keep them small. Fetch a single page to get them.

Page content

A page's body is stored as a TipTap JSON document: a doc node whose content is an array of block nodes, each of which can hold text nodes with marks.

json
{
  "type": "doc",
  "content": [
    {
      "type": "heading",
      "attrs": { "level": 2 },
      "content": [{ "type": "text", "text": "Install" }]
    },
    {
      "type": "paragraph",
      "content": [
        { "type": "text", "text": "Run " },
        { "type": "text", "text": "composer install", "marks": [{ "type": "code" }] },
        { "type": "text", "text": " first." }
      ]
    },
    {
      "type": "codeBlock",
      "attrs": { "language": "bash" },
      "content": [{ "type": "text", "text": "composer install --no-dev" }]
    }
  ]
}

You can read and write the same body as Markdown instead:

markdown
## Install

Run `composer install` first.

```bash
composer install --no-dev
```
  • Reading: single-page responses return content by default, or markdown with ?format=markdown.
  • Writing: send the body as content, a TipTap JSON object, or as markdown, a string, but not both. null for either empties the page.
  • Round trip: Markdown you read and send back unchanged stores the same document, with nothing lost.
  • Mentions in the content notify the people mentioned, and page links update the target's Linked from, as they do in the editor.

See Content formats for the rules, Writing Markdown and Folioso Markdown extensions for Markdown, and the TipTap JSON reference for every node and mark. To put images on a page, see Images.

List pages

http
GET /pages

Lists the pages you can see in the organisation, most recently updated first. Pages in a recycle bin aren't included.

Parameter Description
space Only pages in the space with this slug.
group Only pages in the group with this id.
tag Only pages with the tag with this id.
q Search instead of listing. See Search pages.
per_page Results per page, from 1 to 100. Default 25. A value outside that range is treated as the nearest limit, not refused.
page Which page of results to return. Default 1.

An unknown space, group or tag gives an empty list, not an error.

bash
curl "https://acme.folioso.dev/api/v1/pages?space=engineering&per_page=2" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Accept: application/json"
json
{
  "data": [
    {
      "id": 42,
      "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b",
      "title": "Deploying the API",
      "excerpt": "How we ship the API service to production, step by step.",
      "visibility": "space",
      "space": { "slug": "engineering", "name": "Engineering" },
      "group": { "id": 12, "name": "Runbooks" },
      "url": "https://acme.folioso.dev/engineering/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b",
      "createdAt": "2026-10-01T14:03:11+00:00",
      "updatedAt": "2026-10-16T09:12:44+00:00"
    },
    {
      "id": 1207,
      "slug": "1f0e2d3c-5b6a-4978-8a9b-0c1d2e3f4a5b",
      "title": "On-call checklist",
      "excerpt": null,
      "visibility": "private",
      "space": { "slug": "engineering", "name": "Engineering" },
      "group": null,
      "url": "https://acme.folioso.dev/engineering/1f0e2d3c-5b6a-4978-8a9b-0c1d2e3f4a5b",
      "createdAt": "2026-10-15T08:00:02+00:00",
      "updatedAt": "2026-10-15T08:41:19+00:00"
    }
  ],
  "links": {
    "first": "https://acme.folioso.dev/api/v1/pages?space=engineering&per_page=2&page=1",
    "last": "https://acme.folioso.dev/api/v1/pages?space=engineering&per_page=2&page=4",
    "prev": null,
    "next": "https://acme.folioso.dev/api/v1/pages?space=engineering&per_page=2&page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 4,
    "path": "https://acme.folioso.dev/api/v1/pages",
    "per_page": 2,
    "to": 2,
    "total": 8
  }
}

Pagination

List responses are paginated. meta.total is the number of matching pages and meta.last_page the number of result pages. Follow links.next until it's null to read them all; it keeps your other parameters. meta also carries a links array of numbered page links, for building page controls.

Search pages

http
GET /pages?q={terms}

Add q to search page titles and content instead of listing. The same search powers the search box in the app. See Search.

  • q can be up to 200 characters.
  • Pages whose title matches come first, then the rest by relevance, then by most recently updated.
  • You can combine q with space, group, tag, per_page and page.
  • Search results include pages shared with you directly, even in spaces you can't otherwise open. The plain list doesn't include those.
  • Titles are searchable straight away. New or changed content becomes searchable about 30 to 40 minutes after the last edit.
bash
curl -G "https://acme.folioso.dev/api/v1/pages" \
  --data-urlencode "q=deploy checklist" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Accept: application/json"

The response has the same shape as List pages. A blank q returns the normal list. A q with no letters or digits in it, such as -- or !?, has nothing to search for and returns an empty list.

Get a page

http
GET /pages/{slug}

Returns one page, with its tags, content and contentEpoch. Needs the read ability and permission to view the page.

Parameter Description
format json (the default) returns the body as content. markdown returns it as markdown. Anything else is a 422, and so is markdown for the rare page whose stored content can't be written as Markdown without loss. See When Markdown isn't available.
bash
curl https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Accept: application/json"
json
{
  "data": {
    "id": 42,
    "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b",
    "title": "Deploying the API",
    "excerpt": "How we ship the API service to production, step by step.",
    "visibility": "space",
    "space": { "slug": "engineering", "name": "Engineering" },
    "group": { "id": 12, "name": "Runbooks" },
    "tags": [{ "id": 31, "name": "php" }],
    "content": { "type": "doc", "content": [] },
    "contentEpoch": 0,
    "url": "https://acme.folioso.dev/engineering/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b",
    "createdAt": "2026-10-01T14:03:11+00:00",
    "updatedAt": "2026-10-16T09:12:44+00:00"
  }
}

A page in another organisation, a page in a recycle bin and a slug that doesn't exist all get 404. A page in this organisation that you aren't allowed to view gets 403.

Create a page

http
POST /pages

Creates a page in a space. Needs the write ability and the editor or admin role in the space (or an organisation admin).

Field Rules Description
title Required. Text, up to 255 characters. The title. Leading and trailing spaces are trimmed.
space Required. A space slug. The space to create the page in.
group_id Optional. An integer. A group in that space. Leave it out (or send null) to create the page outside any group.
content Optional. A JSON object or null. The body as a TipTap document. Leave it out for an empty page.
markdown Optional. Text or null. The body as Markdown, instead of content.
tags Optional. An array of up to 50 tag names. The page's tags, by name. See Tags.
format Optional. json or markdown. The format of the body in the response. Usually sent in the query string.

New pages are created with space visibility, so everyone in the space can see them, and are placed at the end of their group, not nested under another page. You'll be the author. The API can't make a page private or nest it under another page; do that in the app.

bash
curl https://acme.folioso.dev/api/v1/pages \
  -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Release notes 4.2",
    "space": "engineering",
    "group_id": 12,
    "content": {
      "type": "doc",
      "content": [
        { "type": "paragraph", "content": [{ "type": "text", "text": "What changed in 4.2." }] }
      ]
    }
  }'

The same page, with its body in Markdown and two tags:

bash
curl "https://acme.folioso.dev/api/v1/pages?format=markdown" \
  -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Release notes 4.2",
    "space": "engineering",
    "group_id": 12,
    "tags": ["release", "php"],
    "markdown": "What changed in 4.2."
  }'

The response is 201 Created with the new page, including tags, content (or markdown) and contentEpoch, in the same shape as Get a page. If you asked for ?format=markdown and the content you sent can't be written back as Markdown without loss, the page is still created and the response carries content instead of markdown. See When Markdown isn't available.

The request is checked in this order, and refused at the first problem. Nothing is created when a request is refused.

Status When
422 A field is missing or invalid, or content and markdown were both sent.
404 No such space in this organisation. Also the answer for a space you can't see, such as a private space you have no role in, so the response never confirms that one exists.
403 You can't add pages to that space.
404 No such group in that space. A group must belong to the space you name.
422 The Markdown has a malformed Folioso extension.

Update a page

http
PATCH /pages/{slug}

Renames a page, replaces its content, moves it to another group or space, or sets its tags, in any combination. It can't change the page's visibility or nest it under another page. Needs the write ability and permission to edit the page: an editor or admin in its space, the page's author, or someone it's shared with for editing. Moving and tagging need more. See Move, regroup and tag a page.

Send only the fields you want to change:

Field Rules Description
title Optional. Text up to 255 characters, or null. The new title. An empty title or null leaves the page untitled.
content Optional. A JSON object or null. Replaces the whole body. null empties the page. Leave the field out to keep the current content.
markdown Optional. Text or null. Replaces the whole body with Markdown, instead of content. null or "" empties the page.
epoch Optional. An integer, 0 or more. The contentEpoch you last read. See below.
space Optional. A space slug. Moves the page to this space.
group_id Optional. An integer, or null. Moves the page into this group, or out of any group with null.
tags Optional. An array of up to 50 tag names. Replaces the page's tags. See Tags.
format Optional. json or markdown. The format of the body in the response. Usually sent in the query string.

content and markdown replace the body in full; there's no partial update. Read the page, change the document or the Markdown, and send it back. See Content formats.

bash
curl https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b \
  -X PATCH \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Deploying the API (v2)" }'

The response is 200 OK with the updated page, in the same shape as Get a page. As on create, a ?format=markdown response falls back to content when the page's content can't be written as Markdown without loss; the update has still been made.

Changing the body, through content or markdown, gives the page a new contentEpoch, which the response returns. Anyone with the page open in the editor has it reloaded with your content. Sending a body identical to the stored one changes nothing: the epoch stays the same and nobody is reloaded.

Warning

Leaving content and markdown out keeps the body as it is, but sending "content": null, "markdown": null or "markdown": "" empties the page.

Avoid overwriting a restore

If someone restores a page from a version history snapshot while your integration is working on it, writing back the copy you read earlier would undo the restore. To prevent that, send the contentEpoch you read as epoch:

json
{
  "markdown": "## Install\n\nRun `composer install` first.",
  "epoch": 3
}

If the page has been restored, or its content replaced through the API, since you read it, the update is refused with 409 Conflict and nothing changes. Read the page again and reapply your change. If you don't send epoch, no check is made. Your own content updates change the epoch too whenever they change the body, so always send the contentEpoch from your last response, not the one you first read.

A content update reloads anyone with the page open. Someone editing it on their own is reloaded on their next save, and anything they typed since their last save is lost.

Move, regroup and tag a page

space, group_id and tags organise a page. Sending any of them needs more than permission to edit: the editor or admin role in the page's space, or authorship of the page. Someone the page is shared with for editing can change its title and body but not these, and gets 403 if they send any of them.

  • Within its space: send group_id to move the page into another group of the same space, or null to take it out of its group.
  • To another space: send space with the target's slug. You need the editor or admin role in the target space. The page lands outside any group unless you also send group_id for a group in the target space. As with Move to space… in the app, the pages nested under it move too, its address doesn't change, and snapshots from before the move are hidden. See Move a page.
  • Its own space: sending the slug of the space the page is already in, without group_id, moves nothing.
bash
curl https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b \
  -X PATCH \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{ "space": "operations", "group_id": 27, "tags": ["runbook", "deploy"] }'

The request is checked in this order, and refused at the first problem. Everything before the 409 is checked before anything changes, so those refusals change nothing:

Status When
404 The page doesn't exist or belongs to another organisation.
403 You sent space, group_id or tags without the role to organise the page.
422 A field is invalid.
403 You can't edit the page.
404 No such space in this organisation. Also the answer for a space you can't see.
403 You can't add pages to the target space.
404 No such group in that space. The group must be in the space the page ends up in.
403 A tag must be created and you can't add tags to the space.
422 The Markdown has a malformed Folioso extension.
409 The epoch you sent is out of date.

Once checked, the changes apply in order: the title and body first, then the move, then the tags. A 409 on the body stops the move and the tags too. A move to another space is recorded in the organisation's audit log, as in the app.

Tags

tags on POST /pages and PATCH /pages/{slug} is the page's complete set of tags, by name:

  • Names match the space's existing tags exactly, including letter case.
  • A name the space doesn't have yet is created as a new tag there, with the editor's default colour. Creating a tag needs the editor or admin role in the space, as in the app.
  • On PATCH, the list replaces the page's tags, and [] removes them all. Leave tags out to keep them.
  • Names are text up to 50 characters each, with no blanks or repeats, and at most 50 in a list. null or a string instead of a list is a 422.

When a PATCH moves the page to another space, the names are matched in the target space. Without tags, the page's tags come with it, matched by name in the new space or created there, as in the app.

See Tags.

Delete a page

http
DELETE /pages/{slug}

Moves the page to the recycle bin, exactly like Delete in the app. Needs the write ability and permission to delete the page: its author, or an admin of its space or organisation. Editors who didn't write the page can't delete it.

bash
curl https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b \
  -X DELETE \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Accept: application/json"

The response is 204 No Content. The page can be restored from the recycle bin in the app until it's permanently deleted. The API can read the recycle bin, but can't restore or permanently delete pages.