Projects
Projects are containers for pages within an organization. Access is granted through org membership or project-level sharing.
Access Control
| Tier | Description |
|---|---|
| Org | Member of the project's organization |
| Project | Added as a project editor |
Access is granted if either tier applies.
List Projects
| Endpoint | GET /api/projects/ |
| Auth | Bearer token |
Query Parameters:
| Param | Description |
|---|---|
org_id |
Filter by organization |
details |
Set to full to include pages |
Response (200):
[
{
"external_id": "abc123",
"name": "My Project",
"description": "...",
"modified": "2025-01-15T10:30:00Z",
"created": "2025-01-10T08:00:00Z",
"creator": {
"external_id": "user123",
"email": "..."
},
"org": {
"external_id": "org123",
"name": "..."
},
"pages": null
}
]
With ?details=full, pages contains an array of page objects.
Get Project
| Endpoint | GET /api/projects/{external_id}/ |
| Auth | Bearer token |
Query: ?details=full to include pages.
Response (200):
{
"external_id": "abc123",
"name": "My Project",
"description": "Project description",
"modified": "2025-01-15T10:30:00Z",
"created": "2025-01-10T08:00:00Z",
"creator": {
"external_id": "user123",
"email": "[email protected]"
},
"org": {
"external_id": "org123",
"name": "My Organization"
},
"pages": null
}
Create Project
| Endpoint | POST /api/projects/ |
| Auth | Bearer token (org member) |
| Field | Type | Required? | Description |
|---|---|---|---|
org_id |
string | Yes | Organization ID |
name |
string | Yes | Project name |
description |
string | No | Description |
Response (201):
{
"external_id": "xyz789",
"name": "New Project",
"description": "Project description",
"modified": "2025-01-15T14:30:00Z",
"created": "2025-01-15T14:30:00Z",
"creator": {
"external_id": "user123",
"email": "[email protected]"
},
"org": {
"external_id": "org123",
"name": "My Organization"
},
"pages": null
}
Update Project
| Endpoint | PATCH /api/projects/{external_id}/ |
| Auth | Bearer token |
| Field | Type | Required? | Description |
|---|---|---|---|
name |
string | No | Project name |
description |
string | No | Description |
Response (200):
{
"external_id": "abc123",
"name": "Updated Project Name",
"description": "Updated description",
"modified": "2025-01-16T09:00:00Z",
"created": "2025-01-10T08:00:00Z",
"creator": {
"external_id": "user123",
"email": "[email protected]"
},
"org": {
"external_id": "org123",
"name": "My Organization"
},
"pages": null
}
Delete Project
| Endpoint | DELETE /api/projects/{external_id}/ |
| Auth | Bearer token (creator only) |
Response (204): No content.
Soft delete—project is hidden but not permanently removed.
Sharing
List Editors
| Endpoint | GET /api/projects/{external_id}/editors/ |
| Auth | Bearer token |
Response (200):
[
{
"external_id": "user123",
"email": "...",
"is_creator": false,
"is_pending": false
},
{
"external_id": "inv456",
"email": "...",
"is_creator": false,
"is_pending": true
}
]
Add Editor
| Endpoint | POST /api/projects/{external_id}/editors/ |
| Auth | Bearer token |
| Field | Type | Required? | Description |
|---|---|---|---|
email |
string | Yes | Email address to invite |
Response (201):
{
"external_id": "user789",
"email": "[email protected]",
"is_creator": false,
"is_pending": false
}
- Existing users are added immediately
- New users receive an email invitation
Remove Editor
| Endpoint | DELETE /api/projects/{external_id}/editors/{user_id}/ |
| Auth | Bearer token |
Response (204): No content.
Cannot remove the project creator.
Validate Invitation
| Endpoint | GET /api/projects/invitations/{token}/validate |
| Auth | None required |
Response (200):
{
"action": "redirect",
"redirect_to": "/projects/proj123",
"email": "[email protected]",
"project_name": "My Project"
}
For unauthenticated users, action is "signup" to indicate they should create an account.
Examples
List all projects
BASE_URL="<BASE_URL>"
TOKEN="<ACCESS_TOKEN>"
curl "$BASE_URL/api/projects/" \
-H "Authorization: Bearer $TOKEN"
import requests
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
response = requests.get(
f"{BASE_URL}/api/projects/",
headers={"Authorization": f"Bearer {TOKEN}"}
)
print(response.json())
const BASE_URL = "<BASE_URL>";
const TOKEN = "<ACCESS_TOKEN>";
const response = await fetch(`${BASE_URL}/api/projects/`, {
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/projects/")
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/projects/");
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/projects/", 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)
}
List projects with pages
BASE_URL="<BASE_URL>"
TOKEN="<ACCESS_TOKEN>"
curl "$BASE_URL/api/projects/?details=full" \
-H "Authorization: Bearer $TOKEN"
import requests
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
response = requests.get(
f"{BASE_URL}/api/projects/",
params={"details": "full"},
headers={"Authorization": f"Bearer {TOKEN}"}
)
print(response.json())
const BASE_URL = "<BASE_URL>";
const TOKEN = "<ACCESS_TOKEN>";
const response = await fetch(`${BASE_URL}/api/projects/?details=full`, {
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/projects/")
uri.query = URI.encode_www_form(details: "full")
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/projects/?details=full");
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/projects/?details=full", 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 project
BASE_URL="<BASE_URL>"
TOKEN="<ACCESS_TOKEN>"
curl -X POST "$BASE_URL/api/projects/" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"org_id": "org123",
"name": "New Project",
"description": "Project description"
}
EOF
import requests
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
ORG_ID = "org123"
response = requests.post(
f"{BASE_URL}/api/projects/",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"org_id": ORG_ID,
"name": "New Project",
"description": "Project description"
}
)
print(response.json())
const BASE_URL = "<BASE_URL>";
const TOKEN = "<ACCESS_TOKEN>";
const ORG_ID = "org123";
const response = await fetch(`${BASE_URL}/api/projects/`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
org_id: ORG_ID,
name: "New Project",
description: "Project description",
}),
});
console.log(await response.json());
require 'net/http'
require 'json'
require 'uri'
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
ORG_ID = "org123"
uri = URI("#{BASE_URL}/api/projects/")
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 = {
org_id: ORG_ID,
name: "New Project",
description: "Project description"
}.to_json
response = http.request(request)
puts JSON.parse(response.body)
<?php
$baseUrl = "<BASE_URL>";
$token = "<ACCESS_TOKEN>";
$orgId = "org123";
$ch = curl_init("$baseUrl/api/projects/");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $token",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode([
"org_id" => $orgId,
"name" => "New Project",
"description" => "Project description"
])
]);
$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>"
orgID = "org123"
)
func main() {
body, _ := json.Marshal(map[string]string{
"org_id": orgID,
"name": "New Project",
"description": "Project description",
})
req, _ := http.NewRequest("POST", baseURL+"/api/projects/", 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)
}
Share a project
BASE_URL="<BASE_URL>"
TOKEN="<ACCESS_TOKEN>"
PROJECT_ID="abc123"
curl -X POST "$BASE_URL/api/projects/$PROJECT_ID/editors/" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"email": "[email protected]"
}
EOF
import requests
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
PROJECT_ID = "abc123"
response = requests.post(
f"{BASE_URL}/api/projects/{PROJECT_ID}/editors/",
headers={"Authorization": f"Bearer {TOKEN}"},
json={"email": "[email protected]"}
)
print(response.json())
const BASE_URL = "<BASE_URL>";
const TOKEN = "<ACCESS_TOKEN>";
const PROJECT_ID = "abc123";
const response = await fetch(`${BASE_URL}/api/projects/${PROJECT_ID}/editors/`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email: "[email protected]" }),
});
console.log(await response.json());
require 'net/http'
require 'json'
require 'uri'
BASE_URL = "<BASE_URL>"
TOKEN = "<ACCESS_TOKEN>"
PROJECT_ID = "abc123"
uri = URI("#{BASE_URL}/api/projects/#{PROJECT_ID}/editors/")
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 = { email: "[email protected]" }.to_json
response = http.request(request)
puts JSON.parse(response.body)
<?php
$baseUrl = "<BASE_URL>";
$token = "<ACCESS_TOKEN>";
$projectId = "abc123";
$ch = curl_init("$baseUrl/api/projects/$projectId/editors/");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $token",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode(["email" => "[email protected]"])
]);
$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 = "abc123"
)
func main() {
body, _ := json.Marshal(map[string]string{"email": "[email protected]"})
req, _ := http.NewRequest("POST", baseURL+"/api/projects/"+projectID+"/editors/", 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)
}