REST API
Operate on PuppyOne Content Nodes directly through HTTP.
Overview
If you are building your own agent, automation script, or backend service, you can call the PuppyOne REST API directly to read and write Content Nodes. This is the most flexible option when you need to plug PuppyOne into existing systems, workflows, or server-side logic.
Basics
Base URL
| Environment | Base URL |
|---|---|
| Cloud | https://api.puppyone.ai/api/v1 |
| Self-hosted | http://localhost:9090/api/v1 |
Authentication
All requests must include an auth header:
Authorization: Bearer <access_key>In most cases, you should create a dedicated access key for your REST API integration instead of reusing an MCP, sandbox, or file-sync key.
Core endpoints
All content operations use path-based addressing under the /content/{project_id}/ prefix. No UUIDs — you address files the same way you would on a local file system.
1. List a directory
GET /content/{project_id}/ls?path=docsGood for browsing directories. Omit path or leave it empty to list the root.
2. Read a file
GET /content/{project_id}/cat?path=config.jsonReturns the file content. JSON nodes return content, Markdown nodes return content_text.
3. Create a directory
POST /content/{project_id}/mkdir{
"path": "reports"
}4. Write a JSON file
POST /content/{project_id}/write{
"path": "reports/weekly.json",
"node_type": "json",
"content": {
"title": "Weekly Report",
"summary": "Completed product pricing adjustments this week."
},
"message": "Create weekly report"
}5. Write a Markdown file
POST /content/{project_id}/write{
"path": "docs/README.md",
"node_type": "markdown",
"content": "# Project Notes\n\nThis is the document body.",
"message": "Add project notes"
}6. Move / rename
POST /content/{project_id}/mv{
"old_path": "reports/weekly.json",
"new_path": "archive/weekly-old.json",
"message": "Archive old report"
}7. Delete
POST /content/{project_id}/rm{
"path": "reports/weekly.json",
"permanent": false
}When permanent is false (default), the file is moved to trash.
8. Version history
GET /content/{project_id}/versions?path=config.json&limit=10Returns the version history for a specific file.
Python example
import httpx
BASE_URL = "https://api.puppyone.ai/api/v1"
ACCESS_KEY = "sk_live_xxx"
PROJECT_ID = "proj_xxx"
headers = {
"Authorization": f"Bearer {ACCESS_KEY}",
"Content-Type": "application/json",
}
async def ls(path=""):
params = {"path": path} if path else {}
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{BASE_URL}/content/{PROJECT_ID}/ls",
headers=headers, params=params,
)
resp.raise_for_status()
return resp.json()["data"]
async def cat(path):
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{BASE_URL}/content/{PROJECT_ID}/cat",
headers=headers, params={"path": path},
)
resp.raise_for_status()
return resp.json()["data"]
async def write(path, content, node_type="json", message=None):
body = {"path": path, "content": content, "node_type": node_type}
if message:
body["message"] = message
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{BASE_URL}/content/{PROJECT_ID}/write",
headers=headers, json=body,
)
resp.raise_for_status()
return resp.json()["data"]
async def mv(old_path, new_path, message=None):
body = {"old_path": old_path, "new_path": new_path}
if message:
body["message"] = message
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{BASE_URL}/content/{PROJECT_ID}/mv",
headers=headers, json=body,
)
resp.raise_for_status()
return resp.json()["data"]
async def rm(path, permanent=False):
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{BASE_URL}/content/{PROJECT_ID}/rm",
headers=headers, json={"path": path, "permanent": permanent},
)
resp.raise_for_status()
return resp.json()["data"]
async def versions(path, limit=50):
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{BASE_URL}/content/{PROJECT_ID}/versions",
headers=headers, params={"path": path, "limit": limit},
)
resp.raise_for_status()
return resp.json()["data"]JavaScript / Node.js example
const BASE_URL = "https://api.puppyone.ai/api/v1";
const ACCESS_KEY = "sk_live_xxx";
const PROJECT_ID = "proj_xxx";
const headers = {
Authorization: `Bearer ${ACCESS_KEY}`,
"Content-Type": "application/json",
};
async function ls(path = "") {
const params = path ? new URLSearchParams({ path }) : "";
const resp = await fetch(`${BASE_URL}/content/${PROJECT_ID}/ls?${params}`, { headers });
if (!resp.ok) throw new Error(await resp.text());
return (await resp.json()).data;
}
async function cat(path) {
const params = new URLSearchParams({ path });
const resp = await fetch(`${BASE_URL}/content/${PROJECT_ID}/cat?${params}`, { headers });
if (!resp.ok) throw new Error(await resp.text());
return (await resp.json()).data;
}
async function write(path, content, nodeType = "json", message) {
const body = { path, content, node_type: nodeType };
if (message) body.message = message;
const resp = await fetch(`${BASE_URL}/content/${PROJECT_ID}/write`, {
method: "POST",
headers,
body: JSON.stringify(body),
});
if (!resp.ok) throw new Error(await resp.text());
return (await resp.json()).data;
}
async function rm(path, permanent = false) {
const resp = await fetch(`${BASE_URL}/content/${PROJECT_ID}/rm`, {
method: "POST",
headers,
body: JSON.stringify({ path, permanent }),
});
if (!resp.ok) throw new Error(await resp.text());
return (await resp.json()).data;
}When REST API is the right fit
- You are building a custom agent and want full control over requests, responses, and retries
- You need to integrate PuppyOne into an existing backend service or automation flow
- You only need data access and do not want the MCP client setup
If you want the fastest out-of-the-box experience, MCP is usually easier. If you need finer engineering control, REST API is a better fit.
Next steps
- Code Sandbox. Execute code against your data in an isolated environment
- Cursor integration. Use MCP if you prefer a client-based workflow