Pages
Pages are documents within projects. Access is granted through organization membership, project sharing, or direct page sharing.
Access Control
Access uses an additive three-tier model—access is granted if any tier applies:
| Tier | Description |
|---|---|
| Org | Member of the page's project's organization |
| Project | Added as editor on the page's project |
| Page | Explicitly shared as page editor |
List Pages
Get all pages you can access.
| Endpoint | GET /api/pages/ |
| Auth | Bearer token |
Query Parameters:
| Param | Default | Description |
|---|---|---|
limit |
100 | Items per page (max 100) |
offset |
0 | Items to skip |
Response (200):
{
"items": [
{
"external_id": "abc123",
"title": "My Page",
"project_id": "proj123",
"details": { "content": "..." },
"updated": "2025-01-15T10:30:00Z",
"created": "2025-01-10T08:00:00Z",
"modified": "2025-01-15T10:30:00Z",
"is_owner": true
}
],
"count": 10
}
Autocomplete
Search pages by title for typeahead functionality.
| Endpoint | GET /api/pages/autocomplete/ |
| Auth | Bearer token |
Query: ?q=search+term
Response (200):
{
"pages": [
{
"external_id": "abc123",
"title": "Python Tutorial",
"updated": "..."
}
]
}
Create Page
| Endpoint | POST /api/pages/ |
| Auth | Bearer token |
| Field | Type | Required? | Description |
|---|---|---|---|
project_id |
string | Yes | Project to create page in |
title |
string | No | Page title |
details |
object | No | Content and metadata |
Response (201):
{
"external_id": "xyz789",
"title": "My New Page",
"project_id": "proj123",
"details": {},
"updated": "2025-01-15T14:30:00Z",
"created": "2025-01-15T14:30:00Z",
"modified": "2025-01-15T14:30:00Z",
"is_owner": true
}
Get Page
| Endpoint | GET /api/pages/{external_id}/ |
| Auth | Bearer token |
Response (200):
{
"external_id": "abc123",
"title": "My Page",
"project_id": "proj123",
"details": { "content": "Page content..." },
"updated": "2025-01-15T10:30:00Z",
"created": "2025-01-10T08:00:00Z",
"modified": "2025-01-15T10:30:00Z",
"is_owner": true
}
Update Page
| Endpoint | PUT /api/pages/{external_id}/ |
| Auth | Bearer token |
| Field | Type | Required? | Description |
|---|---|---|---|
title |
string | No | Page title |
details |
object | No | Content and metadata |
Response (200):
{
"external_id": "abc123",
"title": "Updated Title",
"project_id": "proj123",
"details": { "content": "New content..." },
"updated": "2025-01-16T09:00:00Z",
"created": "2025-01-10T08:00:00Z",
"modified": "2025-01-16T09:00:00Z",
"is_owner": true
}
Delete Page
| Endpoint | DELETE /api/pages/{external_id}/ |
| Auth | Bearer token (owner only) |
Response (204): No content.
Soft delete—page is hidden but not permanently removed.
Get Page Links
Get outgoing and incoming (backlinks) internal links for a page.
| Endpoint | GET /api/pages/{external_id}/links/ |
| Auth | Bearer token |
Response (200):
{
"outgoing": [
{
"external_id": "def456",
"title": "Linked Page",
"link_text": "see this page"
}
],
"incoming": [
{
"external_id": "ghi789",
"title": "Referencing Page",
"link_text": "My Page"
}
]
}
| Field | Description |
|---|---|
outgoing |
Pages this page links to |
incoming |
Pages that link to this page (backlinks) |
link_text |
Display text from the markdown link |
Links use format
[Link Text](/pages/{external_id}/)and are parsed on save.
Sharing
List Editors
| Endpoint | GET /api/pages/{external_id}/editors/ |
| Auth | Bearer token (owner only) |
Response (200):
[
{
"external_id": "user123",
"email": "[email protected]"
}
]
Add Editor
| Endpoint | POST /api/pages/{external_id}/editors/ |
| Auth | Bearer token (owner only) |
| Field | Type | Required? | Description |
|---|---|---|---|
email |
string | Yes | Email address to invite |
Response (201):
{
"external_id": "user789",
"email": "[email protected]"
}
Remove Editor
| Endpoint | DELETE /api/pages/{external_id}/editors/{user_id}/ |
| Auth | Bearer token (owner only) |
Response (204): No content.
Validate Invitation
| Endpoint | GET /api/pages/invitations/{invitation_id}/validate/ |
Response (200):
{
"is_valid": true,
"page": {
"external_id": "abc123",
"title": "Shared Page"
},
"inviter": {
"email": "[email protected]"
}
}
Notes
- Pages use CRDT for real-time collaborative editing
updatedis managed by the collaboration systemmodifiedupdates when you change title/details via REST
Examples
List your pages
BASE_URL="<BASE_URL>"
TOKEN="<ACCESS_TOKEN>"
curl "$BASE_URL/api/pages/" \
-H "Authorization: Bearer $TOKEN"
import requests
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
response = requests.get(
f"{BASE_URL}/api/pages/",
headers={"Authorization": f"Bearer {TOKEN}"}
)
print(response.json())
const BASE_URL = "<BASE_URL>";
const TOKEN = "<ACCESS_TOKEN>";
const response = await fetch(`${BASE_URL}/api/pages/`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
console.log(await response.json());
require 'net/http'
require 'json'
require 'uri'
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
uri = URI("#{BASE_URL}/api/pages/")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{TOKEN}"
response = http.request(request)
puts JSON.parse(response.body)
<?php
$baseUrl = "<BASE_URL>";
$token = "<ACCESS_TOKEN>";
$ch = curl_init("$baseUrl/api/pages/");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"]
]);
$response = curl_exec($ch);
curl_close($ch);
print_r(json_decode($response, true));
package main
import (
"encoding/json"
"fmt"
"net/http"
)
const (
baseURL = "<BASE_URL>"
token = "<ACCESS_TOKEN>"
)
func main() {
req, _ := http.NewRequest("GET", baseURL+"/api/pages/", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result)
}
Create a page
BASE_URL="<BASE_URL>"
TOKEN="<ACCESS_TOKEN>"
curl -X POST "$BASE_URL/api/pages/" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"project_id": "proj123",
"title": "My New Page"
}
EOF
import requests
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
PROJECT_ID = "proj123"
response = requests.post(
f"{BASE_URL}/api/pages/",
headers={"Authorization": f"Bearer {TOKEN}"},
json={"project_id": PROJECT_ID, "title": "My New Page"}
)
print(response.json())
const BASE_URL = "<BASE_URL>";
const TOKEN = "<ACCESS_TOKEN>";
const PROJECT_ID = "proj123";
const response = await fetch(`${BASE_URL}/api/pages/`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
project_id: PROJECT_ID,
title: "My New Page",
}),
});
console.log(await response.json());
require 'net/http'
require 'json'
require 'uri'
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
PROJECT_ID = "proj123"
uri = URI("#{BASE_URL}/api/pages/")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{TOKEN}"
request["Content-Type"] = "application/json"
request.body = { project_id: PROJECT_ID, title: "My New Page" }.to_json
response = http.request(request)
puts JSON.parse(response.body)
<?php
$baseUrl = "<BASE_URL>";
$token = "<ACCESS_TOKEN>";
$projectId = "proj123";
$ch = curl_init("$baseUrl/api/pages/");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $token",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode([
"project_id" => $projectId,
"title" => "My New Page"
])
]);
$response = curl_exec($ch);
curl_close($ch);
print_r(json_decode($response, true));
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
const (
baseURL = "BASE_URL"
token = "ACCESS_TOKEN"
projectID = "proj123"
)
func main() {
body, _ := json.Marshal(map[string]string{
"project_id": projectID,
"title": "My New Page",
})
req, _ := http.NewRequest("POST", baseURL+"/api/pages/", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result)
}
Get a specific page
BASE_URL="<BASE_URL>"
TOKEN="<ACCESS_TOKEN>"
PAGE_ID="abc123"
curl "$BASE_URL/api/pages/$PAGE_ID/" \
-H "Authorization: Bearer $TOKEN"
import requests
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
PAGE_ID = "abc123"
response = requests.get(
f"{BASE_URL}/api/pages/{PAGE_ID}/",
headers={"Authorization": f"Bearer {TOKEN}"}
)
print(response.json())
const BASE_URL = "<BASE_URL>";
const TOKEN = "<ACCESS_TOKEN>";
const PAGE_ID = "abc123";
const response = await fetch(`${BASE_URL}/api/pages/${PAGE_ID}/`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
console.log(await response.json());
require 'net/http'
require 'json'
require 'uri'
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
PAGE_ID = "abc123"
uri = URI("#{BASE_URL}/api/pages/#{PAGE_ID}/")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{TOKEN}"
response = http.request(request)
puts JSON.parse(response.body)
<?php
$baseUrl = "<BASE_URL>";
$token = "<ACCESS_TOKEN>";
$pageId = "abc123";
$ch = curl_init("$baseUrl/api/pages/$pageId/");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"]
]);
$response = curl_exec($ch);
curl_close($ch);
print_r(json_decode($response, true));
package main
import (
"encoding/json"
"fmt"
"net/http"
)
const (
baseURL = "<BASE_URL>"
token = "<ACCESS_TOKEN>"
pageID = "abc123"
)
func main() {
req, _ := http.NewRequest("GET", baseURL+"/api/pages/"+pageID+"/", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result)
}
Update a page
BASE_URL="<BASE_URL>"
TOKEN="<ACCESS_TOKEN>"
PAGE_ID="abc123"
curl -X PUT "$BASE_URL/api/pages/$PAGE_ID/" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Updated Title", "details": {"content": "New content..."}}'
import requests
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
PAGE_ID = "abc123"
response = requests.put(
f"{BASE_URL}/api/pages/{PAGE_ID}/",
headers={"Authorization": f"Bearer {TOKEN}"},
json={"title": "Updated Title", "details": {"content": "New content..."}}
)
print(response.json())
const BASE_URL = "<BASE_URL>";
const TOKEN = "<ACCESS_TOKEN>";
const PAGE_ID = "abc123";
const response = await fetch(`${BASE_URL}/api/pages/${PAGE_ID}/`, {
method: "PUT",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Updated Title",
details: { content: "New content..." },
}),
});
console.log(await response.json());
require 'net/http'
require 'json'
require 'uri'
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
PAGE_ID = "abc123"
uri = URI("#{BASE_URL}/api/pages/#{PAGE_ID}/")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Put.new(uri)
request["Authorization"] = "Bearer #{TOKEN}"
request["Content-Type"] = "application/json"
request.body = {
title: "Updated Title",
details: { content: "New content..." }
}.to_json
response = http.request(request)
puts JSON.parse(response.body)
<?php
$baseUrl = "<BASE_URL>";
$token = "<ACCESS_TOKEN>";
$pageId = "abc123";
$ch = curl_init("$baseUrl/api/pages/$pageId/");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $token",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode([
"title" => "Updated Title",
"details" => ["content" => "New content..."]
])
]);
$response = curl_exec($ch);
curl_close($ch);
print_r(json_decode($response, true));
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
const (
baseURL = "<BASE_URL>"
token = "<ACCESS_TOKEN>"
pageID = "abc123"
)
func main() {
body, _ := json.Marshal(map[string]interface{}{
"title": "Updated Title",
"details": map[string]string{"content": "New content..."},
})
req, _ := http.NewRequest("PUT", baseURL+"/api/pages/"+pageID+"/", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result)
}
Get page links (backlinks)
BASE_URL="<BASE_URL>"
TOKEN="<ACCESS_TOKEN>"
PAGE_ID="abc123"
curl "$BASE_URL/api/pages/$PAGE_ID/links/" \
-H "Authorization: Bearer $TOKEN"
import requests
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
PAGE_ID = "abc123"
response = requests.get(
f"{BASE_URL}/api/pages/{PAGE_ID}/links/",
headers={"Authorization": f"Bearer {TOKEN}"}
)
print(response.json())
const BASE_URL = "<BASE_URL>";
const TOKEN = "<ACCESS_TOKEN>";
const PAGE_ID = "abc123";
const response = await fetch(`${BASE_URL}/api/pages/${PAGE_ID}/links/`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
console.log(await response.json());
Search pages
BASE_URL="<BASE_URL>"
TOKEN="<ACCESS_TOKEN>"
curl "$BASE_URL/api/pages/autocomplete/?q=python" \
-H "Authorization: Bearer $TOKEN"
import requests
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
response = requests.get(
f"{BASE_URL}/api/pages/autocomplete/",
params={"q": "python"},
headers={"Authorization": f"Bearer {TOKEN}"}
)
print(response.json())
const BASE_URL = "<BASE_URL>";
const TOKEN = "<ACCESS_TOKEN>";
const response = await fetch(`${BASE_URL}/api/pages/autocomplete/?q=python`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
console.log(await response.json());
require 'net/http'
require 'json'
require 'uri'
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
uri = URI("#{BASE_URL}/api/pages/autocomplete/")
uri.query = URI.encode_www_form(q: "python")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{TOKEN}"
response = http.request(request)
puts JSON.parse(response.body)
<?php
$baseUrl = "<BASE_URL>";
$token = "<ACCESS_TOKEN>";
$ch = curl_init("$baseUrl/api/pages/autocomplete/?q=python");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"]
]);
$response = curl_exec($ch);
curl_close($ch);
print_r(json_decode($response, true));
package main
import (
"encoding/json"
"fmt"
"net/http"
)
const (
baseURL = "<BASE_URL>"
token = "<ACCESS_TOKEN>"
)
func main() {
req, _ := http.NewRequest("GET", baseURL+"/api/pages/autocomplete/?q=python", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result)
}