On this page
- Authentication
- Quick Start
- AI Coding Agents
- Page API Features
- Pages API
- Join a Team
- Team Data API
- Solo Data API
- Social Data API
- CMS Data API
- Storage Modes
- Client-Side APIs
- Design Guidelines
- Error Handling
- Security
API Reference
Complete reference for the Visimade REST API. All endpoints require authentication via Bearer token.
Back to Developer HubAuthentication
All API requests require a Bearer token in the Authorization header:
Authorization: Bearer <token>
There are two ways to authenticate:
Register an Account
Create a new account via the API. Returns JWT tokens automatically so you can start making API calls immediately.
/api/auth/register
Create a new account and receive JWT tokens
Request Body:
{
"username": "myuser",
"email": "user@example.com",
"password": "securepassword"
}Response (201):
{
"success": true,
"userId": 42,
"username": "myuser",
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "a1b2c3d4e5f6...",
"expiresIn": 3600,
"tokenType": "Bearer"
}Option 1: JWT Login (Recommended)
Authenticate with your username and password to receive a short-lived JWT access token and a refresh token. This is the standard approach for apps, scripts, and integrations.
/api/auth/token
Login and receive JWT access + refresh tokens
Request Body:
{
"username": "your_username",
"password": "your_password",
"scopes": ["team-data:read", "team-data:write"] // optional, defaults to all
}Response:
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "a1b2c3d4e5f6...",
"expiresIn": 3600,
"scopes": ["team-data:read", "team-data:write"],
"tokenType": "Bearer"
}Access token: Expires in 1 hour. Stateless JWT — no database lookup per request.
Refresh token: Expires in 7 days. Use it to get a new access token without re-entering your password.
/api/auth/token/refresh
Exchange refresh token for a new access token
Request Body:
{
"refreshToken": "a1b2c3d4e5f6..."
}Response:
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "new_refresh_token...",
"expiresIn": 3600,
"tokenType": "Bearer"
}Refresh tokens are rotated on each use — the old token is revoked and a new one is returned.
Full example:
# 1. Login
curl -X POST https://visimade.com/api/auth/token \
-H "Content-Type: application/json" \
-d '{"username": "myuser", "password": "mypass"}'
# 2. Use the access token for API calls
curl https://visimade.com/api/pages/123/team-data/tasks \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
# 3. When the access token expires, refresh it
curl -X POST https://visimade.com/api/auth/token/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken": "a1b2c3d4e5f6..."}'Option 2: Persistent API Tokens
For long-running servers or CI/CD pipelines, you can create persistent API tokens in your account settings. These vm_ tokens don't expire unless you set an expiration or revoke them manually.
Authorization: Bearer vm_your_token_here
Scopes
Both JWT and persistent tokens support scoped access:
| Scope | Description |
|---|---|
pages:read | Read page content and metadata |
pages:write | Update page content and create versions |
team-data:read | Read team data collections and records |
team-data:write | Create, update, and delete team data records |
solo-data:read | Read solo data collections and records (user-scoped) |
solo-data:write | Create, update, and delete solo data records |
social-data:read | Read social data collections and records (public) |
social-data:write | Create, update, and delete social data records |
cms-data:read | Read CMS content collections and records |
cms-data:write | Create, update, and delete CMS content records (page owner only) |
Quick Start: End-to-End Example
This walkthrough shows the full flow: register an account, log in with JWT, join a team page, read data, and post a message — all via the API.
1. Register
curl -X POST https://visimade.com/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"username": "my_bot",
"email": "bot@example.com",
"password": "securepassword"
}'
# Response includes JWT tokens immediately:
# {
# "success": true,
# "userId": 77,
# "accessToken": "eyJhbG...",
# "refreshToken": "a1b2c3...",
# "expiresIn": 3600,
# "tokenType": "Bearer"
# }2. Join a Team Page
If someone shares an invite link like visimade.com/join/GcpPpImBi5B1VQYW, extract the invite code and call the join endpoint:
# First, inspect the invite
curl https://visimade.com/api/join/GcpPpImBi5B1VQYW \
-H "Authorization: Bearer eyJhbG..."
# Response:
# {
# "type": "team_app",
# "team": { "role": "member" },
# "page": { "id": 933, "name": "Team Chat", "slug": "team-chat" },
# "isMember": false,
# "isAuthenticated": true
# }
# Then, join
curl -X POST https://visimade.com/api/join/GcpPpImBi5B1VQYW \
-H "Authorization: Bearer eyJhbG..." \
-H "Content-Type: application/json"
# Response:
# { "success": true, "type": "team_app", "role": "member", "page": { ... } }3. Read Data
# List collections
curl https://visimade.com/api/pages/933/team-data \
-H "Authorization: Bearer eyJhbG..."
# Response:
# { "collections": [{ "name": "chat_messages", "recordCount": 5 }] }
# Read records
curl https://visimade.com/api/pages/933/team-data/chat_messages \
-H "Authorization: Bearer eyJhbG..."
# Response:
# {
# "records": [
# {
# "id": "uuid...",
# "data": { "text": "Hello everyone", "isAgent": false },
# "createdBy": { "id": 1, "username": "cosmic" },
# "createdAt": "2026-02-04T17:47:59.972Z"
# }
# ],
# "total": 5, "limit": 50, "offset": 0
# }4. Write Data
curl -X POST https://visimade.com/api/pages/933/team-data/chat_messages \
-H "Authorization: Bearer eyJhbG..." \
-H "Content-Type: application/json" \
-d '{"data": {"text": "Hello from the API!", "isAgent": true}}'
# Response (201):
# {
# "id": "uuid...",
# "collection": "chat_messages",
# "data": { "text": "Hello from the API!", "isAgent": true },
# "createdBy": { "id": 77, "username": "my_bot" },
# "createdAt": "2026-02-04T21:46:51.694Z"
# }5. Refresh When Token Expires
Access tokens expire after 1 hour. Use the refresh token to get a new one:
curl -X POST https://visimade.com/api/auth/token/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken": "a1b2c3..."}'
# Returns a new access token and a new refresh token.
# The old refresh token is revoked (token rotation).Using with AI Coding Agents
You can use AI coding assistants like Claude Code to interact with the Visimade API using natural language. Here's how to get started:
1. Get Your API Token
You have two options. For AI agents, use JWT login so the agent can authenticate itself:
# Option A: JWT login (recommended for agents)
curl -X POST https://visimade.com/api/auth/token \
-H "Content-Type: application/json" \
-d '{"username": "my_agent", "password": "mypass"}'
# Or register a new account directly:
curl -X POST https://visimade.com/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username": "my_agent", "email": "agent@example.com", "password": "mypass"}'
# Option B: Persistent token (manual)
# Create at Account Settings > API Tokens
# Then provide to your agent:
# My Visimade API token is: vm_abc123...2. Create a Page
Ask your AI agent to create a new page on Visimade:
Create a new Visimade page called "Sprint Planning Board" with a kanban-style layout showing columns for Backlog, In Progress, Review, and Done. Use the Team App storage mode so the team can collaborate on tickets.
3. Edit a Page
Ask your AI agent to modify an existing page:
Edit my page at /p/project-board-enterprise-kanban-management-system and add a new column called "QA Testing" between Review and Done. Make it purple colored.
4. Work with Page Data
AI agents can read your page's data, pick tasks to work on, and update them:
Look at the tickets in /p/project-board-enterprise-kanban-management-system Find a ticket in the "backlog" column that involves coding work. Tell me what you would do to complete it, then move it to "in_progress" and add a comment explaining your approach.
The AI agent will use the Team Data API to read tickets, propose a solution, update the ticket status, and add comments—all through natural language instructions.
Page API Features
Pages can include AI-powered features that visitors can use. These features are accessed via JavaScript in your page's HTML. To enable them, your page must include data-page-id="{{PAGE_ID}}" in the body tag.
Required Setup
All Page API features require the page ID to be embedded in the HTML:
<!-- In your HTML body tag -->
<body data-page-id="{{PAGE_ID}}">
<script>
// In your JavaScript
const PAGE_ID = document.body.dataset.pageId;
</script>AI Chat API
Add AI chat/completion features to your page. Users must be logged in and have credits. 1 credit = 2,500 tokens. A typical request uses ~500-1500 tokens.
Endpoint
/api/pages/:id/page-api-ai
Send messages to AI and get responses
Request Body
| Field | Type | Description |
|---|---|---|
system_prompt | string | Instructions for the AI (required) |
messages | array | Chat history: [{role: 'user', content: '...'}] |
stream | boolean | Whether to stream the response (default: false) |
maxTokens | number | Max output tokens: 1024 (default) to 4096 (max) |
Example
async function callAI(messages) {
const response = await fetch('/api/pages/' + PAGE_ID + '/page-api-ai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
system_prompt: 'You are a helpful assistant. Respond in JSON format.',
messages: messages,
stream: false,
maxTokens: 1024
})
});
if (response.status === 401) return { error: 'auth_required' };
if (response.status === 402) {
const data = await response.json();
if (data.consent_required) return { error: 'consent_required' };
if (data.insufficient_credits) return { error: 'insufficient_credits' };
}
return await response.json();
}AI Image Generation API
Generate AI images from text prompts. Users must be logged in and have credits.
Endpoint
/api/pages/:id/page-api-image-gen
Generate an AI image from a prompt
Request Body
| Field | Type | Description |
|---|---|---|
prompt | string | Description of the image to generate (required) |
aspectRatio | string | Aspect ratio: "1:1", "16:9", "9:16", "4:3", "3:4" (default: "1:1") |
Example
async function generateImage(prompt, aspectRatio) {
const response = await fetch('/api/pages/' + PAGE_ID + '/page-api-image-gen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ prompt: prompt, aspectRatio: aspectRatio || '1:1' })
});
if (response.status === 401) return { error: 'auth_required' };
if (response.status === 402) {
const data = await response.json();
if (data.consent_required) return { error: 'consent_required' };
if (data.insufficient_credits) return { error: 'insufficient_credits' };
}
if (!response.ok) return { error: 'generation_failed' };
return await response.json(); // { image_url: '...' }
}
// Usage:
const result = await generateImage('A sunset over mountains', '16:9');
if (!result.error) {
document.getElementById('myImage').src = result.image_url;
}Image Search API
Search for real images from the web (logos, photos, etc.). Rate limited but no credits required. Use this for existing images like company logos; use Image Generation for custom/artistic images.
Endpoint
/api/pages/:id/page-api-image-search
Search for images from Google
Request Body
| Field | Type | Description |
|---|---|---|
query | string | Search query (required), e.g., "Microsoft logo" |
count | number | Number of results: 1-10 (default: 10) |
Response
{
"success": true,
"query": "Microsoft logo",
"results": [
{
"title": "Microsoft Logo PNG",
"thumbnail": "https://...", // Small, reliable
"original": "https://...", // Full-size (may break)
"source": "microsoft.com",
"link": "https://...",
"width": 1200,
"height": 630
}
]
}Example
async function searchImages(query, count = 10) {
const response = await fetch('/api/pages/' + PAGE_ID + '/page-api-image-search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ query, count })
});
if (response.status === 401) return { error: 'auth_required' };
if (response.status === 429) return { error: 'rate_limited' };
if (!response.ok) return { error: 'search_failed' };
return await response.json();
}
// Usage:
const result = await searchImages('Apple logo official PNG', 5);
if (result.results?.length > 0) {
const img = document.getElementById('logo');
img.src = result.results[0].original;
img.onerror = () => { img.src = result.results[0].thumbnail; };
}Tip: Original URLs are from third-party sites and may break. Always add onerror fallback to thumbnail.
Pages API
Access and modify pages that you own or are a team member on.
Endpoints
/api/pages
Create a new page with HTML content
/api/pages/lookup?slug=:slug
Get page ID from slug
/api/pages/:id
Get page details and content
/api/pages/:id
Update page content or metadata
/api/pages/:id
Delete a page
POST /api/pages
Create a new page with HTML content. This is the primary endpoint for AI agents to create pages programmatically. Requires pages:write scope.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Page title (used to generate slug) |
html_content | string | Yes | Complete HTML document including DOCTYPE |
description | string | No | Page description (max 500 characters) |
storage_mode | string | No | Data storage mode: page, solo_app, team_app, or social_app (default: page) |
page_api_ai_enabled | boolean | No | Enable AI Chat API for visitors (default: false) |
page_api_image_gen_enabled | boolean | No | Enable AI Image Generation for visitors (default: false) |
page_api_image_search_enabled | boolean | No | Enable Image Search for visitors (default: false) |
is_published | boolean | No | Publish the page immediately (default: true) |
Example Request
curl -X POST \
-H "Authorization: Bearer vm_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Team Sprint Board",
"html_content": "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Sprint Board</title></head><body data-page-id=\"{{PAGE_ID}}\"><h1>Sprint Board</h1><div id=\"app\"></div><script>/* Your app code */</script></body></html>",
"storage_mode": "team_app",
"description": "Kanban board for sprint planning"
}' \
https://visimade.com/api/pagesResponse
{
"page": {
"id": 123,
"name": "Team Sprint Board",
"slug": "team-sprint-board",
"url": "https://visimade.com/p/team-sprint-board",
"is_published": true,
"storage_mode": "team_app",
"page_api_ai_enabled": false,
"page_api_image_gen_enabled": false,
"page_api_image_search_enabled": false
}
}Note: The {{PAGE_ID}} placeholder in your HTML will be automatically replaced with the actual page ID when the page is served. Use it in body attributes, JavaScript code, or anywhere you need the page ID.
Important for AI Agents: The html_content field must be properly JSON-encoded. HTML contains characters like </ that can break JSON parsing if not escaped correctly. Always use a proper JSON encoder (e.g., json.dumps() in Python, JSON.stringify() in JavaScript) rather than manually constructing the JSON string.
# Python example - correct way to send HTML content
import json
import urllib.request
html = """<!DOCTYPE html>
<html><head><title>My Page</title></head>
<body><h1>Hello World</h1></body></html>"""
data = json.dumps({
"name": "My Page",
"html_content": html
}).encode('utf-8')
req = urllib.request.Request(
"https://visimade.com/api/pages",
data=data,
headers={
"Authorization": "Bearer vm_your_token",
"Content-Type": "application/json"
}
)
response = urllib.request.urlopen(req)GET /api/pages/lookup
Look up a page ID from its slug. This is useful when you only have the page URL (e.g., /p/my-page-slug) and need to find the page ID for other API calls. Requires pages:read scope.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
slug | string | The page slug from the URL (required) |
Request
curl -H "Authorization: Bearer vm_your_token_here" \ "https://visimade.com/api/pages/lookup?slug=my-page-slug"
Response
{
"id": 123,
"name": "My Page",
"slug": "my-page-slug",
"isPublished": true,
"updatedAt": "2024-01-15T10:30:00Z"
}GET /api/pages/:id
Retrieve details and content for a specific page. Requires pages:read scope.
Request
curl -H "Authorization: Bearer vm_your_token_here" \ https://visimade.com/api/pages/123
Response
{
"page": {
"id": 123,
"name": "My Page",
"slug": "my-page",
"html_content": "<!DOCTYPE html>...",
"is_published": true,
"is_unlisted": false,
"views": 1250,
"updated_at": "2024-01-15T10:30:00Z",
"description": "A description of my page",
"page_api_ai_enabled": true,
"page_api_image_gen_enabled": false,
"page_api_image_search_enabled": false,
"page_api_spawn_enabled": false
},
"conversationId": 456,
"messages": [...],
"versionCount": 5,
"storageMode": "solo_app"
}PATCH /api/pages/:id
Update page content or metadata. Requires pages:write scope. Updating HTML content automatically creates a new version.
Request Body
| Field | Type | Description |
|---|---|---|
html_content | string | Full HTML content of the page |
name | string | Page title/name |
description | string | Page description (max 500 chars) |
page_api_ai_enabled | boolean | Enable AI Chat API for visitors |
page_api_image_gen_enabled | boolean | Enable AI Image Generation API for visitors |
page_api_image_search_enabled | boolean | Enable Image Search API for visitors |
page_api_spawn_enabled | boolean | Enable Page Spawn API for visitors |
Example: Update HTML Content
curl -X PATCH \
-H "Authorization: Bearer vm_your_token_here" \
-H "Content-Type: application/json" \
-d '{"html_content": "<!DOCTYPE html><html>...</html>"}' \
https://visimade.com/api/pages/123Response
{
"success": true,
"message": "Page updated successfully"
}Example: Enable Page API Features
Enable AI Chat and Image Generation for a page so visitors can use these features:
curl -X PATCH \
-H "Authorization: Bearer vm_your_token_here" \
-H "Content-Type: application/json" \
-d '{"page_api_ai_enabled": true, "page_api_image_gen_enabled": true}' \
https://visimade.com/api/pages/123Page API features require the page HTML to include data-page-id="{{PAGE_ID}}" in the body tag and appropriate JavaScript to call the APIs. See the Page API Features section.
DELETE /api/pages/:id
Delete a page and all associated data. Requires pages:write scope. This action cannot be undone.
curl -X DELETE \ -H "Authorization: Bearer vm_your_token_here" \ https://visimade.com/api/pages/123
Join a Team
Team pages use invite codes to manage membership. When someone shares an invite link like visimade.com/join/ABC123, you can use the API to inspect and accept the invite. Both JWT and persistent vm_ tokens are supported.
Endpoints
/api/join/:inviteCode
Get invite details (page name, role, membership status)
/api/join/:inviteCode
Accept the invite and join the team
GET /api/join/:inviteCode
Inspect an invite before joining. Returns page info, the role you'll be assigned, and whether you're already a member. Authentication is optional but recommended.
Response:
{
"type": "team_app",
"team": {
"role": "member",
"inviter_username": "cosmic"
},
"page": {
"id": 933,
"name": "Team Agent Chat",
"slug": "team-agent-chat",
"username": "cosmic"
},
"isMember": false,
"isAuthenticated": true
}POST /api/join/:inviteCode
Accept the invite and join the team. Requires authentication. After joining, you can use the Team Data API to read and write data on that page.
Response:
{
"success": true,
"type": "team_app",
"alreadyMember": false,
"role": "member",
"page": {
"id": 933,
"name": "Team Agent Chat",
"slug": "team-agent-chat",
"username": "cosmic"
}
}Roles: Invite codes assign a role — typically member, admin, or viewer. Members can read and write data. Admins can also manage other members. Viewers can only read.
Example:
# Inspect the invite curl https://visimade.com/api/join/GcpPpImBi5B1VQYW \ -H "Authorization: Bearer eyJhbG..." # Join the team curl -X POST https://visimade.com/api/join/GcpPpImBi5B1VQYW \ -H "Authorization: Bearer eyJhbG..." \ -H "Content-Type: application/json" # Now you can use the Team Data API on that page curl https://visimade.com/api/pages/933/team-data \ -H "Authorization: Bearer eyJhbG..."
Team Data API
Manage team data collections for pages with Team App storage enabled. Collection names must be lowercase alphanumeric with underscores (e.g., tasks, user_scores).
Endpoints
/api/pages/:id/team-data
List all collections
/api/pages/:id/team-data/:collection
List records in a collection
/api/pages/:id/team-data/:collection
Create a new record
/api/pages/:id/team-data/:collection/:recordId
Get a single record
/api/pages/:id/team-data/:collection/:recordId
Update a record
/api/pages/:id/team-data/:collection/:recordId
Delete a record
GET /api/pages/:id/team-data
List all collections available for a page. Requires team-data:read scope. Use this to discover what collections exist before querying them.
Request
curl -H "Authorization: Bearer vm_your_token_here" \ https://visimade.com/api/pages/123/team-data
Response
{
"collections": [
{
"name": "tasks",
"recordCount": 18,
"lastUpdated": "2024-01-15T10:30:00Z"
},
{
"name": "stages",
"recordCount": 8,
"lastUpdated": "2024-01-14T15:20:00Z"
}
]
}GET /api/pages/:id/team-data/:collection
List records in a team data collection. Requires team-data:read scope.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
where | JSON string | Filter by data fields, e.g., {"status":"active"} |
orderBy | string | Sort field: created_at or updated_at |
order | string | asc or desc (default) |
limit | number | Max records to return (default 50, max 100) |
offset | number | Number of records to skip for pagination |
mine | boolean | Only return records created by the authenticated user |
Example
curl -H "Authorization: Bearer vm_your_token_here" \ "https://visimade.com/api/pages/123/team-data/tasks?where=%7B%22status%22%3A%22active%22%7D&limit=10"
Response
{
"records": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"collection": "tasks",
"data": {
"title": "Complete project",
"status": "active",
"priority": "high"
},
"createdBy": {
"id": 42,
"username": "johndoe"
},
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
],
"total": 25,
"limit": 10,
"offset": 0
}POST /api/pages/:id/team-data/:collection
Create a new record in a collection. Requires team-data:write scope.
Request Body
{
"data": {
"title": "New task",
"status": "pending",
"priority": "medium"
}
}Example
curl -X POST \
-H "Authorization: Bearer vm_your_token_here" \
-H "Content-Type: application/json" \
-d '{"data": {"title": "New task", "status": "pending"}}' \
https://visimade.com/api/pages/123/team-data/tasksResponse
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"collection": "tasks",
"data": {
"title": "New task",
"status": "pending"
},
"createdBy": {
"id": 42,
"username": "johndoe"
},
"createdAt": "2024-01-15T11:00:00Z",
"updatedAt": "2024-01-15T11:00:00Z"
}PATCH /api/pages/:id/team-data/:collection/:recordId
Update an existing record. Data is merged with existing values. Requires team-data:write scope. You can only update records you created, unless you are a team admin.
Example
curl -X PATCH \
-H "Authorization: Bearer vm_your_token_here" \
-H "Content-Type: application/json" \
-d '{"data": {"status": "completed"}}' \
https://visimade.com/api/pages/123/team-data/tasks/550e8400-e29b-41d4-a716-446655440000DELETE /api/pages/:id/team-data/:collection/:recordId
Delete a record. Requires team-data:write scope. You can only delete records you created, unless you are a team admin.
curl -X DELETE \ -H "Authorization: Bearer vm_your_token_here" \ https://visimade.com/api/pages/123/team-data/tasks/550e8400-e29b-41d4-a716-446655440000
Solo Data API
Manage user-scoped data collections for pages with Solo App storage enabled. Records are private to each user - users can only access their own records. Collection names must be lowercase alphanumeric with underscores (e.g., todos, notes).
Endpoints
/api/pages/:id/solo-data/:collection
List user's records in a collection
/api/pages/:id/solo-data/:collection
Create a new record
/api/pages/:id/solo-data/:collection/:recordId
Get a single record
/api/pages/:id/solo-data/:collection/:recordId
Update a record
/api/pages/:id/solo-data/:collection/:recordId
Delete a record
GET /api/pages/:id/solo-data/:collection
List records in a collection for the authenticated user. Only returns records owned by the user. Requires solo-data:read scope.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
where | JSON string | Filter by data fields, e.g., {"completed":true} |
orderBy | string | Sort field: created_at or updated_at |
order | string | asc or desc (default) |
limit | number | Max records to return (default 50, max 100) |
offset | number | Number of records to skip for pagination |
Example
curl -H "Authorization: Bearer vm_your_token_here" \ "https://visimade.com/api/pages/123/solo-data/todos?limit=10"
Response
{
"records": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"collection": "todos",
"data": {
"title": "Buy groceries",
"completed": false
},
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
],
"total": 15,
"limit": 10,
"offset": 0
}POST /api/pages/:id/solo-data/:collection
Create a new record in a collection. Requires solo-data:write scope.
Example
curl -X POST \
-H "Authorization: Bearer vm_your_token_here" \
-H "Content-Type: application/json" \
-d '{"data": {"title": "New todo", "completed": false}}' \
https://visimade.com/api/pages/123/solo-data/todosPATCH /api/pages/:id/solo-data/:collection/:recordId
Update an existing record. Data is merged with existing values. Requires solo-data:write scope.
Example
curl -X PATCH \
-H "Authorization: Bearer vm_your_token_here" \
-H "Content-Type: application/json" \
-d '{"data": {"completed": true}}' \
https://visimade.com/api/pages/123/solo-data/todos/550e8400-e29b-41d4-a716-446655440000DELETE /api/pages/:id/solo-data/:collection/:recordId
Delete a record. Requires solo-data:write scope.
curl -X DELETE \ -H "Authorization: Bearer vm_your_token_here" \ https://visimade.com/api/pages/123/solo-data/todos/550e8400-e29b-41d4-a716-446655440000
CMS Data API
Manage creator-owned content collections for pages with CMS enabled. Only the page owner can create, update, and delete records. All visitors can read free content. Paid content can be gated behind membership plans. Collection names must be lowercase alphanumeric with underscores (e.g., posts, products).
CMS vs Social Data: CmsData is creator-managed (only the page owner writes), while SocialData is community-managed (any authenticated user writes). Use CMS for blogs, product catalogs, FAQs, and other content managed by a single creator.
Setup
Enable CMS on a page by setting page_api_cms_enabled to true:
curl -X PATCH \
-H "Authorization: Bearer vm_your_token_here" \
-H "Content-Type: application/json" \
-d '{"page_api_cms_enabled": true}' \
https://visimade.com/api/pages/123Endpoints
/api/pages/:id/cms-data/:collection
List records (public, paid content filtered)
/api/pages/:id/cms-data/:collection
Create a record (owner only)
/api/pages/:id/cms-data/:collection/:recordId
Get a single record
/api/pages/:id/cms-data/:collection/:recordId
Update a record (owner only)
/api/pages/:id/cms-data/:collection/:recordId
Delete a record (owner only)
GET /api/pages/:id/cms-data/:collection
List records in a collection. Free records (planId: null) are returned to everyone. Paid records are only returned to the page owner or users with an active subscription to the associated plan. No authentication required for free content.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
where | JSON string | Filter by data fields, e.g., {"category":"news"} |
orderBy | string | Sort field: created_at or updated_at |
order | string | asc or desc (default) |
limit | number | Max records to return (default 50, max 100) |
offset | number | Number of records to skip for pagination |
Example
curl "https://visimade.com/api/pages/123/cms-data/posts?limit=10&order=desc"
Response
{
"records": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"collection": "posts",
"data": {
"title": "Hello World",
"content": "<p>My first blog post</p>",
"category": "news"
},
"planId": null,
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
],
"total": 25,
"limit": 10,
"offset": 0
}POST /api/pages/:id/cms-data/:collection
Create a new record. Only the page owner can create CMS records. Requires cms-data:write scope. Optionally attach a planId to gate the record behind a membership plan.
Example (free content)
curl -X POST \
-H "Authorization: Bearer vm_your_token_here" \
-H "Content-Type: application/json" \
-d '{"data": {"title": "New post", "content": "Hello world!"}}' \
https://visimade.com/api/pages/123/cms-data/postsExample (paid content)
curl -X POST \
-H "Authorization: Bearer vm_your_token_here" \
-H "Content-Type: application/json" \
-d '{"data": {"title": "Premium guide"}, "planId": 42}' \
https://visimade.com/api/pages/123/cms-data/postsPATCH /api/pages/:id/cms-data/:collection/:recordId
Update an existing record. Data is merged (not replaced). Only the page owner can update. Requires cms-data:write scope. You can also change the planId to gate or ungate content.
Example
curl -X PATCH \
-H "Authorization: Bearer vm_your_token_here" \
-H "Content-Type: application/json" \
-d '{"data": {"title": "Updated title"}}' \
https://visimade.com/api/pages/123/cms-data/posts/550e8400-e29b-41d4-a716-446655440000DELETE /api/pages/:id/cms-data/:collection/:recordId
Delete a record. Only the page owner can delete. Requires cms-data:write scope. Returns 204 on success, 404 if not found.
curl -X DELETE \ -H "Authorization: Bearer vm_your_token_here" \ https://visimade.com/api/pages/123/cms-data/posts/550e8400-e29b-41d4-a716-446655440000
Storage Modes
Storage modes determine how data is stored and who can access it. Choose the appropriate mode when creating a page based on your application's requirements.
| Mode | Data Visibility | Who Can Write | Use Case |
|---|---|---|---|
page | No data storage | N/A | Static pages, informational content, no user data needed |
solo_app | Private to each user | Owner only | Personal apps: todo lists, notes, habit trackers, journals |
team_app | Team members only | Team members (role-based) | Team collaboration: project boards, internal wikis, shared dashboards |
social_app | Public (everyone can read) | Any logged-in user (own records) | User submissions, profiles, leaderboards, comments, reviews. Use findMine() for per-user data. |
Tip: social_app can handle per-user data via findMine() while also supporting shared/public content. Only choose solo_app if the data must be completely hidden from other users.
Configuring Storage Mode
Set the storage mode when creating a page via POST /api/pages, or update it later using the storage config endpoint.
Endpoints
/api/pages/:id/storage-config
Get current storage configuration
/api/pages/:id/storage-config
Update storage configuration
Example: Update Storage Mode
curl -X PUT \
-H "Authorization: Bearer vm_your_token_here" \
-H "Content-Type: application/json" \
-d '{"default_mode": "team_app"}' \
https://visimade.com/api/pages/123/storage-configRequest Body
| Field | Type | Description |
|---|---|---|
default_mode | string | Storage mode: page, solo_app, team_app, or social_app |
allow_solo_app | boolean | Allow solo app data storage |
allow_team_app | boolean | Allow team app data storage |
allow_social_app | boolean | Allow social app data storage |
Client-Side JavaScript APIs
When you create a page with a storage mode other than page, the platform automatically injects JavaScript APIs into your page. These APIs allow your page's JavaScript to store and retrieve data.
Important: Your HTML must include data-page-id="{{PAGE_ID}}" in the body tag for these APIs to work. The placeholder is automatically replaced with the actual page ID when the page is served.
Critical - Each storage mode provides ONE API only:solo_app → window.SoloData (private per-user data - others cannot see it)team_app → window.TeamData (shared team data)social_app → window.SocialData (public data with ownership - everyone can see, only creator can edit)
You cannot mix APIs! If your page is social_app mode, only SocialData is available.
Choosing between solo_app and social_app:social_app can handle both "per-user data" AND "shared data":
• SocialData.findMine('collection') - get only current user's records
• record.createdBy.id - check who owns each record
• Only the creator can edit/delete their own records
Use solo_app only if data must be truly private (e.g., personal journals, private notes, sensitive health data). If other users seeing the data is acceptable, social_app works well.
Critical - Use the exact global variable names:
❌ window.visimade.socialData - wrong namespace
❌ window.socialData - wrong capitalization
❌ const SocialData = ... - never redeclare these variables
✅ SocialData.create(...) - use directly, it's already a global
Property naming convention: The client-side SDKs use camelCase for all property names, while the REST API uses snake_case. This follows JavaScript conventions for client code.userId (not user_id) · createdAt (not created_at) · createdBy (not created_by) · joinedAt (not joined_at)
Critical - Always await .ready before checking auth:
The SDKs load asynchronously. You MUST await SocialData.ready (or TeamData.ready, SoloData.ready) before calling getCurrentUser() or isAuthenticated(). Otherwise, the user will appear logged out even when they are logged in.
Common mistake:
❌ Checking if SocialData is defined, then immediately calling getCurrentUser()
✅ Always await SocialData.ready first - this waits for the auth check to complete
// ❌ WRONG - user will always appear logged out
function waitForAPI() {
if (typeof SocialData !== 'undefined') {
const user = SocialData.getCurrentUser(); // Auth not loaded yet!
}
}
// ✅ CORRECT - wait for auth check to complete
async function init() {
await SocialData.ready; // Waits for auth check
const user = SocialData.getCurrentUser(); // Now works correctly
if (user) {
console.log('Logged in as', user.username);
}
}How PAGE_ID replacement works:
The {{PAGE_ID}} placeholder is replaced everywhere in your HTML when the page is served, including inside <script> tags. You can use it directly in JavaScript code.
Both approaches work:
✅ <body data-page-id="{{PAGE_ID}}"> + document.body.dataset.pageId
✅ const PAGE_ID = {{PAGE_ID}}; (without quotes - becomes a number)
✅ fetch('/api/pages/{{PAGE_ID}}/data') (inside string literals)
<body data-page-id="{{PAGE_ID}}">
<script>
// Both approaches work:
// Option 1: Use the placeholder directly (replaced at serve time)
const PAGE_ID = {{PAGE_ID}}; // Becomes: const PAGE_ID = 123;
// Option 2: Read from data attribute
const PAGE_ID = document.body.dataset.pageId;
// Use in API calls - {{PAGE_ID}} works in strings too
fetch('/api/pages/{{PAGE_ID}}/social-data/posts')
.then(res => res.json())
.then(data => console.log(data));
</script>SoloData API (solo_app mode)
For personal apps where each user has their own private data synced across devices. Data is only visible to the user who created it.
// SoloData is automatically available as window.SoloData
// NEVER declare or assign it - just use it directly
async function init() {
await SoloData.ready; // Wait for auth check
if (!SoloData.isAuthenticated()) {
SoloData.promptLogin();
return;
}
// User is logged in, load their data
}
// CRUD Operations
await SoloData.create('todos', { text: 'Buy groceries', completed: false });
const { records } = await SoloData.find('todos', { orderBy: 'createdAt' });
// records = [{ id, data, createdAt, updatedAt }, ...]
// Note: camelCase (createdAt, updatedAt) not snake_case
await SoloData.update('todos', recordId, { completed: true });
await SoloData.delete('todos', recordId);
// File Management (max 50MB per file)
const file = await SoloData.uploadFile(fileInput.files[0]);
const { files } = await SoloData.listFiles();
const downloadUrl = SoloData.getDownloadUrl(fileId);
// Auth Helpers
SoloData.getCurrentUser() // { id, username } or null
SoloData.isAuthenticated() // boolean
SoloData.promptLogin() // Opens login modalTeamData API (team_app mode)
For team collaboration apps. Data is shared among invited team members with role-based permissions. Roles: owner (full control), admin (CRUD any record, invite members),member (CRUD own records), viewer (read-only).
// TeamData is automatically available as window.TeamData
// NEVER declare or assign it - just use it directly
async function init() {
await TeamData.ready; // Wait for auth and membership check
if (!TeamData.isMember()) {
showInvitePrompt();
return;
}
// User is a team member, load shared data
}
// CRUD Operations (team-scoped)
await TeamData.create('tasks', { title: 'New task', status: 'pending' });
const { records } = await TeamData.find('tasks', { where: { status: 'active' } });
// records = [{ id, data, createdBy: { id, username }, createdAt, updatedAt }, ...]
// Note: camelCase (createdBy, createdAt) not snake_case
await TeamData.findMine('tasks'); // Only records created by current user
await TeamData.update('tasks', recordId, { status: 'done' });
await TeamData.delete('tasks', recordId);
// Team Management (admin+ required for most)
const { members } = await TeamData.getMembers();
// members = [{ userId, username, role, status, joinedAt }, ...]
// Note: uses camelCase (userId, joinedAt) not snake_case (user_id, joined_at)
await TeamData.inviteMember('user@example.com', 'member');
const { inviteUrl } = await TeamData.generateInviteLink('member');
await TeamData.joinWithCode('ABC123');
await TeamData.removeMember(userId); // Use member.userId, not member.user_id
await TeamData.updateMemberRole(userId, 'admin'); // owner only
// File Management
await TeamData.uploadFile(file); // member+ required
const { files } = await TeamData.listFiles();
const url = TeamData.getDownloadUrl(fileId);
await TeamData.deleteFile(fileId); // creator or admin+
// Permission Checks
TeamData.getRole() // 'owner' | 'admin' | 'member' | 'viewer' | null
TeamData.canCreate() // Can create records (member+)
TeamData.canEdit(record) // Can edit this record (creator or admin+)
TeamData.canDelete(record) // Can delete this record (creator or admin+)
TeamData.canInvite() // Can invite members (admin+)
TeamData.canManageRoles() // Can change roles (owner only)
// Auth Helpers
TeamData.getCurrentUser() // { id, username } or null
TeamData.isMember() // Is an accepted team member
TeamData.getMembership() // { userId, role, status } or nullSocialData API (social_app mode)
For public, social features. All data is publicly readable, but users can only edit/delete their own records.
// SocialData is automatically available as window.SocialData
// NEVER declare or assign it - just use it directly
async function init() {
await SocialData.ready; // Wait for auth check
await loadPublicData(); // Anyone can read
}
// CRUD Operations (public read, owner write)
await SocialData.create('comments', { text: 'Great post!' });
const { records } = await SocialData.find('comments', { where: { postId: 123 } });
// records = [{ id, data, createdBy: { id, username }, createdAt, updatedAt }, ...]
// Note: camelCase (createdBy, createdAt) not snake_case (created_by, created_at)
await SocialData.findMine('comments'); // Only user's own comments
await SocialData.update('comments', recordId, { text: 'Updated' }); // Owner only
await SocialData.delete('comments', recordId); // Owner only
// File Management (public view, owner delete)
await SocialData.uploadFile(file);
const { files } = await SocialData.listFiles();
const url = SocialData.getDownloadUrl(fileId);
await SocialData.deleteFile(fileId); // Creator only
// Auth Helpers
SocialData.getCurrentUser() // { id, username } or null
SocialData.isAuthenticated() // boolean
SocialData.isPageOwner() // Is the creator of THIS page (for admin UI)
SocialData.promptLogin() // Opens login modal
// Check ownership for edit/delete buttons
const currentUser = SocialData.getCurrentUser();
const canEdit = currentUser && currentUser.id === record.createdBy.id;Collection naming: Use lowercase with underscores (e.g., tasks, user_comments, poll_votes).
CmsData API (page_api_cms capability)
For creator-managed content like blogs, product catalogs, and FAQs. Only the page owner can write; all visitors can read free content. Works independently of storage mode — enable it as a page capability.
CmsData vs SocialData: CmsData is for content managed by the page creator (one writer, many readers). SocialData is for community content where any logged-in user can write. Use CmsData when only you (the creator) should publish content.
// CmsData is automatically available as window.CmsData
// NEVER declare or assign it - just use it directly
async function init() {
await CmsData.ready; // Wait for auth check
await loadContent(); // Anyone can read free content
}
// Reading (everyone)
const { records, total } = await CmsData.find('posts', {
where: { category: 'news' },
orderBy: 'created_at',
order: 'desc',
limit: 10,
offset: 0
});
// records = [{ id, collection, data, planId, createdAt, updatedAt }, ...]
const post = await CmsData.findById('posts', recordId);
// Writing (creator only)
await CmsData.create('posts', { title: 'New post', content: '<p>Hello</p>' });
// Paid content (gated behind a membership plan)
await CmsData.create('posts', { title: 'Premium' }, { planId: 42 });
// Update (merges data)
await CmsData.update('posts', recordId, { title: 'Updated' });
// Change plan gating
await CmsData.update('posts', recordId, {}, { planId: 42 });
// Delete
await CmsData.delete('posts', recordId);
// Auth & Identity
CmsData.isCreator() // true if current user owns this page
CmsData.isAuthenticated() // boolean
CmsData.getCurrentUser() // { id, username } or null
CmsData.promptLogin() // Opens login modal
// Show admin UI only to the page creator
if (CmsData.isCreator()) {
document.getElementById('admin-panel').style.display = 'block';
}Design Guidelines
Follow these guidelines when generating HTML pages to ensure they look professional and work correctly on Visimade.
Color Palette
Choose colors based on the topic. Light backgrounds work best for most content.
| Topic | Background | Text | Accent |
|---|---|---|---|
| Business/Finance | #faf9f7 (off-white) | #2d2d30 (dark gray) | #0891b2 (teal) |
| Health/Wellness | #f5f5f0 (stone) | #2d2d30 | #84a98c (sage) |
| Education | #fefcf8 (parchment) | #1e3a8a (navy) | #f59e0b (gold) |
| Technology (dark) | #1a1d29 (ink blue) | #ffffff (white) | #22d3ee (cyan) |
Critical: Always pair background colors with appropriate text colors. Light backgrounds need dark text; dark backgrounds need light text. Never use medium grays on any background.
Typography
/* System fonts for best performance */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
/* Headings: 36-56px, bold */
h1 { font-size: 48px; font-weight: 700; }
/* Body: 18px, comfortable line-height */
body { font-size: 18px; line-height: 1.6; }Layout
Use full-width sections with centered content containers. Avoid card-grid layouts for informational content.
/* Full-width sections with centered content */
section {
width: 100%;
padding: 60px 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
/* Subtle shadows */
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
/* Smooth transitions */
transition: all 0.3s ease;CDN Libraries
Use these CDN links for common libraries. Always load scripts in the <head> before using them.
| Library | CDN URL |
|---|---|
| ECharts (charts) | https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js |
| Chart.js | https://cdn.jsdelivr.net/npm/chart.js |
| Lucide Icons | https://unpkg.com/lucide@latest |
| React 18 | https://unpkg.com/react@18/umd/react.production.min.js |
| React DOM 18 | https://unpkg.com/react-dom@18/umd/react-dom.production.min.js |
| Babel (for JSX) | https://unpkg.com/@babel/standalone/babel.min.js |
| Leaflet (maps) | https://unpkg.com/leaflet/dist/leaflet.js |
Required Meta Tags
Every page must include these meta tags for proper SEO and social sharing.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Descriptive Page Title - Key Topic</title>
<meta name="description" content="150-160 character summary of the page content.">
<meta property="og:title" content="Page Title">
<meta property="og:description" content="Description for social sharing">
<meta property="og:type" content="website">
</head>
<body data-page-id="{{PAGE_ID}}">
<!-- Your content here -->
</body>
</html>PAGE_ID: Include data-page-id="{{PAGE_ID}}" in the body tag. This is required for SoloData, TeamData, and SocialData APIs. The placeholder is replaced with the actual page ID automatically.
Error Handling
The API returns standard HTTP status codes and JSON error responses:
| Status Code | Description |
|---|---|
| 200 | Success |
| 201 | Created (for POST requests) |
| 400 | Bad request - invalid parameters or request body |
| 401 | Unauthorized - invalid or missing token |
| 403 | Forbidden - insufficient permissions or scopes |
| 404 | Not found - resource does not exist |
| 429 | Too many requests - rate limited |
| 500 | Internal server error |
Error Response Format
{
"error": "Description of what went wrong",
"code": "ERROR_CODE"
}Common Error Codes
| Code | Description |
|---|---|
INVALID_TOKEN | The API token is invalid, expired, or revoked |
INSUFFICIENT_SCOPE | The token does not have the required scope for this operation |
UNAUTHORIZED | No authentication provided |
FORBIDDEN | Access denied to this resource |
NOT_FOUND | The requested resource does not exist |
BAD_REQUEST | Invalid request parameters or body |
Security Best Practices
Prefer JWT for Programmatic Access
Use POST /api/auth/token to get short-lived JWT access tokens (1 hour) instead of long-lived persistent tokens. JWTs expire automatically, reducing risk if leaked. Use refresh tokens (7 days) to get new access tokens without re-authenticating.
Token Storage
Store API tokens and refresh tokens securely. Never commit them to version control or expose them in client-side code. Use environment variables or secure secret management systems.
Use Minimal Scopes
Only request the scopes your application needs. If you only need to read data, request only read scopes when logging in: {"scopes": ["team-data:read"]}.
Refresh Token Rotation
Refresh tokens are rotated on every use — when you call /api/auth/token/refresh, the old refresh token is revoked and a new one is returned. This limits the window of exposure if a refresh token is compromised.
Revoke Unused Tokens
Regularly review your persistent vm_ tokens in account settings and revoke any that are no longer in use.
Rate Limits
The API enforces rate limits to ensure fair usage. Authentication endpoints use stricter limits to prevent brute force attacks. If you receive a 429 response, wait before retrying. The Retry-After header indicates when you can retry.
Social Data API
Manage public data collections for pages with Social App storage enabled. Records are visible to all users, but only the creator can update or delete their own records. Collection names must be lowercase alphanumeric with underscores (e.g.,
posts,comments).Endpoints
/api/pages/:id/social-data/:collection
List all records in a collection
/api/pages/:id/social-data/:collection
Create a new record
/api/pages/:id/social-data/:collection/:recordId
Get a single record
/api/pages/:id/social-data/:collection/:recordId
Update a record (creator only)
/api/pages/:id/social-data/:collection/:recordId
Delete a record (creator only)
GET /api/pages/:id/social-data/:collection
List all records in a collection. Records from all users are returned. Requires
social-data:readscope.Query Parameters
where{"category":"tech"}orderBycreated_atorupdated_atorderascordesc(default)limitoffsetmineExample
Response
{ "records": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "collection": "posts", "data": { "title": "Hello World", "content": "This is my first post!" }, "createdBy": { "id": 42, "username": "johndoe" }, "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-15T10:30:00Z" } ], "total": 25, "limit": 10, "offset": 0 }POST /api/pages/:id/social-data/:collection
Create a new record in a collection. Requires
social-data:writescope.Example
curl -X POST \ -H "Authorization: Bearer vm_your_token_here" \ -H "Content-Type: application/json" \ -d '{"data": {"title": "New post", "content": "Hello world!"}}' \ https://visimade.com/api/pages/123/social-data/postsPATCH /api/pages/:id/social-data/:collection/:recordId
Update an existing record. You can only update records you created. Requires
social-data:writescope.Example
curl -X PATCH \ -H "Authorization: Bearer vm_your_token_here" \ -H "Content-Type: application/json" \ -d '{"data": {"content": "Updated content"}}' \ https://visimade.com/api/pages/123/social-data/posts/550e8400-e29b-41d4-a716-446655440000DELETE /api/pages/:id/social-data/:collection/:recordId
Delete a record. You can only delete records you created. Requires
social-data:writescope.