English
Distribute to Agents
REST API

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

EnvironmentBase URL
Cloudhttps://api.puppyone.ai/api/v1
Self-hostedhttp://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=docs

Good for browsing directories. Omit path or leave it empty to list the root.

2. Read a file

GET /content/{project_id}/cat?path=config.json

Returns 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=10

Returns 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