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
{
"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.
{
"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:
## Install
Run `composer install` first.
```bash
composer install --no-dev
```- Reading: single-page responses return
contentby default, ormarkdownwith?format=markdown. - Writing: send the body as
content, a TipTap JSON object, or asmarkdown, a string, but not both.nullfor 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
GET /pagesLists 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.
curl "https://acme.folioso.dev/api/v1/pages?space=engineering&per_page=2" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/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
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.
qcan be up to 200 characters.- Pages whose title matches come first, then the rest by relevance, then by most recently updated.
- You can combine
qwithspace,group,tag,per_pageandpage. - 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.
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
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. |
curl https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/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
POST /pagesCreates 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.
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:
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
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.
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:
{
"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_idto move the page into another group of the same space, ornullto take it out of its group. - To another space: send
spacewith the target's slug. You need the editor or admin role in the target space. The page lands outside any group unless you also sendgroup_idfor 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.
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. Leavetagsout to keep them. - Names are text up to 50 characters each, with no blanks or repeats, and at most 50 in a list.
nullor a string instead of a list is a422.
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
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.
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.