Documentation
Folio guides & examples
Folio turns any HTML string into a hosted, shareable URL. It works with any AI, any language, any tool — if you can make an HTTP request, you can publish.
For agents
Drop the snippet below into any agent's system prompt and it will publish to Folio automatically — no extra wiring needed.
When you produce any analysis, report, chart, table, or document meant for a human to read: 1. Render it as a complete, self-contained HTML page. Inline all CSS. Load chart libraries (Chart.js, D3, Plotly) from CDN. 2. Publish it by calling publish_report (if available as a tool) or POST to https://folio.ac/api/publish. 3. Return only the URL to the user. Do not paste raw HTML into the chat.
Quickstart
The only required field is html. A title is optional but makes your dashboard useful. No API key or account needed for anonymous publishes.
curl -X POST https://folio.ac/api/publish \
-H "Content-Type: application/json" \
-d '{"title": "Q2 Analysis", "html": "<h1>Hello</h1>"}'
# Response:
# { "url": "https://folio.ac/r/abc123", "slug": "abc123" }Share the url in Slack, Notion, email — it renders the full HTML, runs scripts, and accepts inline comments from signed-in viewers.
Your API key
Sign in and open your dashboard. Your API key is shown below the stats — copy it once and use it in every automated publish call. Reports published with your key are linked to your account and show up in your dashboard alongside anything you publish through the UI.
fol_. Keep it secret — anyone who has it can publish Folios under your account. You can regenerate it from the dashboard at any time; the old key stops working immediately.curl -X POST https://folio.ac/api/publish \
-H "Content-Type: application/json" \
-H "x-api-key: fol_your_key_here" \
-d '{
"title": "Weekly digest",
"html": "<h1>Hello</h1>",
"expiresInDays": 30
}'
# Report appears in your dashboard at folio.ac/dashboardimport requests
FOLIO_KEY = "fol_your_key_here" # copy from dashboard
def publish(html: str, title: str = "", **kwargs) -> str:
r = requests.post(
"https://folio.ac/api/publish",
json={"html": html, "title": title, **kwargs},
headers={"x-api-key": FOLIO_KEY},
)
r.raise_for_status()
return r.json()["url"]
url = publish(html=open("report.html").read(), title="Q3 analysis", expiresInDays=30)
print(url) # → https://folio.ac/r/abc123 (also visible in dashboard)const FOLIO_KEY = process.env.FOLIO_API_KEY; // store in .env, never hardcode
async function publish(html: string, title = ""): Promise<string> {
const res = await fetch("https://folio.ac/api/publish", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": FOLIO_KEY!,
},
body: JSON.stringify({ html, title }),
});
if (!res.ok) throw new Error(await res.text());
return (await res.json()).url;
}Expiry
Every Folio expires. The default is 7 days. Signed-in users can choose 7, 14, 30, or 90 days at publish time — either via the UI or the API.
curl -X POST https://folio.ac/api/publish \
-H "Content-Type: application/json" \
-d '{
"title": "Quarterly Report",
"html": "...",
"expiresInDays": 30
}'7, 14, 30, 90. Any other value falls back to 7 days. Only signed-in users can extend beyond 7 days.Custom URLs
Claim a username and publish at folio.ac/you/r/my-report instead of a random slug. Custom URLs are memorable, link-shareable, and show up in your dashboard.
- Sign in with Google.
- Open the publish form, expand Options, and claim your handle (e.g.
alice). - Enter a custom slug for the report (e.g.
q2-revenue). - Your Folio publishes at
folio.ac/alice/r/q2-revenue.
curl -X POST https://folio.ac/api/publish \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"title": "Q2 Revenue",
"html": "...",
"customSlug": "q2-revenue"
}'
# Response (if username is 'alice'):
# { "url": "https://folio.ac/alice/r/q2-revenue" }q4-report.Password protection
Add a password field to gate a Folio. Viewers enter it once and get a session cookie — they won't be prompted again on the same device.
curl -X POST https://folio.ac/api/publish \
-H "Content-Type: application/json" \
-d '{
"title": "Internal Draft",
"html": "...",
"password": "review2025"
}'import requests
r = requests.post("https://folio.ac/api/publish", json={
"title": "Internal Draft",
"html": render_report(),
"password": "review2025",
})
print(r.json()["url"])Dashboard & analytics
Sign in and go to /dashboard to see every Folio you've published. Click any row to open its detail view.
Python SDK
pip install folio-sdk
import folio
# Set your API key once — copy it from folio.ac/dashboard
folio.configure("fol_your_key_here")
# Or set the FOLIO_API_KEY environment variable instead:
# export FOLIO_API_KEY=fol_your_key_here
url = folio.publish(html, title="Q2 Analysis")
print(url) # → https://folio.ac/r/abc123url = folio.publish(
html, # required — complete HTML string
title="Q2 Revenue", # shown in header and dashboard
expires_in_days=30, # 7 | 14 | 30 | 90 (default: 7)
password="internal-only", # optional gate for viewers
custom_slug="q2-revenue", # → folio.ac/you/r/q2-revenue
)import anthropic
import folio
folio.configure("fol_your_key_here")
client = anthropic.Anthropic()
msg = client.messages.create(
model="claude-opus-4-7",
max_tokens=8096,
messages=[{
"role": "user",
"content": "Analyse this data and return a complete HTML report with charts.",
}],
)
url = folio.publish(msg.content[0].text, title="Data analysis", expires_in_days=30)
print(f"Report: {url}")FOLIO_API_KEY from the environment automatically — no configure() call needed if the variable is set. You can also pass api_key="fol_..." directly to publish() to override per-call.All languages & runtimes
Folio speaks plain HTTP. Use the official Python SDK or copy a snippet for your environment.
pip install folio-sdk
import folio
folio.configure("fol_your_key_here") # or set FOLIO_API_KEY env var
url = folio.publish(html, title="Q2 Analysis", expires_in_days=30)
print(url) # → https://folio.ac/r/abc123import requests
def publish(html: str, title: str = "", password: str = "") -> str:
payload = {"html": html, "title": title}
if password:
payload["password"] = password
r = requests.post(
"https://folio.ac/api/publish",
json=payload,
headers={"x-api-key": "YOUR_KEY"}, # optional
)
r.raise_for_status()
return r.json()["url"]
url = publish(html=open("report.html").read(), title="Weekly digest")
print(url)const publishFolio = async (html, title = "") => {
const res = await fetch("https://folio.ac/api/publish", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ html, title }),
});
const data = await res.json();
return data.url;
};
const url = await publishFolio(html, "Sprint summary");
console.log(url);interface FolioOptions {
html: string;
title?: string;
password?: string;
customSlug?: string;
expiresInDays?: 7 | 14 | 30 | 90;
}
async function publishFolio(opts: FolioOptions): Promise<string> {
const res = await fetch("https://folio.ac/api/publish", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(opts),
});
if (!res.ok) throw new Error(await res.text());
return (await res.json()).url;
}curl -X POST https://folio.ac/api/publish \ -F "file=@report.html" \ -F "title=Monthly report" # multipart/form-data upload — no JSON needed
require "net/http"
require "json"
uri = URI("https://folio.ac/api/publish")
res = Net::HTTP.post(uri,
{ html: File.read("report.html"), title: "Analysis" }.to_json,
"Content-Type" => "application/json"
)
puts JSON.parse(res.body)["url"]package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func publish(html, title string) (string, error) {
body, _ := json.Marshal(map[string]string{"html": html, "title": title})
resp, err := http.Post("https://folio.ac/api/publish", "application/json", bytes.NewReader(body))
if err != nil {
return "", err
}
defer resp.Body.Close()
var out struct{ URL string `json:"url"` }
json.NewDecoder(resp.Body).Decode(&out)
return out.URL, nil
}
func main() {
html, _ := os.ReadFile("report.html")
url, _ := publish(string(html), "Go report")
fmt.Println(url)
}AI & LLM integrations
Any model that can generate HTML and make HTTP requests can publish to Folio. Here are patterns for the most common setups.
Claude (API)
import anthropic, requests
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-opus-4-7",
max_tokens=8096,
messages=[{
"role": "user",
"content": "Analyse this dataset and return a complete HTML report with charts.",
}],
)
html = message.content[0].text # Claude returns the HTML
url = requests.post("https://folio.ac/api/publish", json={
"title": "Dataset analysis",
"html": html,
"expiresInDays": 30,
}).json()["url"]
print(f"Report live at: {url}")Claude Code / Cursor (MCP)
Add Folio as an MCP server and any Claude Code or Cursor session can publish reports directly — no extra code needed. Get your API key from the dashboard.
{
"mcpServers": {
"folio": {
"type": "http",
"url": "https://folio.ac/mcp",
"headers": {
"Authorization": "Bearer fol_your_key_here"
}
}
}
}For Cursor, add the same block under Settings → MCP Servers. Once connected, tell Claude to publish and it will call the tool automatically:
"Analyse this CSV and publish a report with charts." # → Claude generates HTML, calls publish_report, returns: # Published. Share this URL: https://folio.ac/r/abc123
ChatGPT — Custom GPT Action (recommended)
Set up once, then any ChatGPT conversation in that GPT can publish directly to Folio:
- Go to chatgpt.com → Explore GPTs → Create.
- Open the Configure tab, scroll to Actions, click Create new action.
- In the Schema field, click Import from URL and paste:
https://folio.ac/api/openapi - Under Authentication, choose API Key, header name
x-api-key, and paste your key from the dashboard. - Save and start chatting. Tell the GPT to generate and publish a report — it will call the action and return a folio.ac link.
You are a data analyst. When asked to create a report or visualisation: 1. Generate a complete, self-contained HTML page with inline CSS and Chart.js from CDN. 2. Call the publishReport action with that HTML and a descriptive title. 3. Return the URL from the response to the user.
Python / server-side (no code executor)
from openai import OpenAI
import requests
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": "Return ONLY a complete HTML page with a Chart.js bar chart of this data: ...",
}],
)
html = response.choices[0].message.content
url = requests.post("https://folio.ac/api/publish", json={
"title": "Sales dashboard",
"html": html,
}, headers={"x-api-key": "fol_your_key_here"}).json()["url"]
print(url) # → https://folio.ac/r/abc123Google Gemini
import google.generativeai as genai
import requests
genai.configure(api_key="YOUR_KEY")
model = genai.GenerativeModel("gemini-1.5-pro")
response = model.generate_content(
"Write a complete HTML report with embedded Chart.js visualisations for this data: ..."
)
html = response.text
url = requests.post("https://folio.ac/api/publish", json={
"title": "Gemini report",
"html": html,
"expiresInDays": 14,
}).json()["url"]
print(url)LangChain / LangGraph agents
from langchain.tools import tool
import requests
@tool
def publish_report(html: str, title: str = "") -> str:
"""Publish an HTML report and return a shareable URL."""
r = requests.post("https://folio.ac/api/publish", json={"html": html, "title": title})
return r.json()["url"]
# Add to your agent's tool list:
# agent = create_react_agent(llm, tools=[publish_report, ...])Any agent framework
https://folio.ac/api/publish, 3) share the URL. Any LLM that can generate HTML works — Mistral, Llama, Cohere, local models via Ollama, etc.API reference
One endpoint. One method. JSON or multipart.
/api/publish| Field | Type | Required |
|---|---|---|
| html | string | Yes |
| title | string | No |
| password | string | No |
| customSlug | string | No |
| expiresInDays | 7|14|30|90 | No |
Response
{
"url": "https://folio.ac/r/abc123", // canonical shareable URL
"slug": "abc123" // random internal slug
}Authentication
Pass your personal API key (copy it from the dashboard) as x-api-key: fol_… or Authorization: Bearer fol_…. Reports published with your key appear in your dashboard alongside UI-published Folios.
If the server has a PUBLISH_API_KEY environment variable set, that key acts as a server-wide gate — only requests that pass it (or use a valid user key) are accepted.
Multipart upload
curl -X POST https://folio.ac/api/publish \ -F "file=@report.html" \ -F "title=My report" \ -F "password=secret"
File must have a .html extension. The title field defaults to the filename if omitted.
Questions? Open an issue on GitHub.
Publish a Folio →
Comments
Any signed-in viewer can click anywhere on a Folio to drop a numbered comment pin — Figma-style. Pins are anchored to the document position, not the scroll position, so they stay in place even on long reports.