VISIMADE

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 Hub

Authentication

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.

POST

/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.

POST

/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.

POST

/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:

ScopeDescription
pages:readRead page content and metadata
pages:writeUpdate page content and create versions
team-data:readRead team data collections and records
team-data:writeCreate, update, and delete team data records
solo-data:readRead solo data collections and records (user-scoped)
solo-data:writeCreate, update, and delete solo data records
social-data:readRead social data collections and records (public)
social-data:writeCreate, update, and delete social data records
cms-data:readRead CMS content collections and records
cms-data:writeCreate, 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
POST

/api/pages/:id/page-api-ai

Send messages to AI and get responses

Request Body
FieldTypeDescription
system_promptstringInstructions for the AI (required)
messagesarrayChat history: [{role: 'user', content: '...'}]
streambooleanWhether to stream the response (default: false)
maxTokensnumberMax 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
POST

/api/pages/:id/page-api-image-gen

Generate an AI image from a prompt

Request Body
FieldTypeDescription
promptstringDescription of the image to generate (required)
aspectRatiostringAspect 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
POST

/api/pages/:id/page-api-image-search

Search for images from Google

Request Body
FieldTypeDescription
querystringSearch query (required), e.g., "Microsoft logo"
countnumberNumber 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
POST

/api/pages

Create a new page with HTML content

GET

/api/pages/lookup?slug=:slug

Get page ID from slug

GET

/api/pages/:id

Get page details and content

PATCH

/api/pages/:id

Update page content or metadata

DELETE

/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
FieldTypeRequiredDescription
namestringYesPage title (used to generate slug)
html_contentstringYesComplete HTML document including DOCTYPE
descriptionstringNoPage description (max 500 characters)
storage_modestringNoData storage mode: page, solo_app, team_app, or social_app (default: page)
page_api_ai_enabledbooleanNoEnable AI Chat API for visitors (default: false)
page_api_image_gen_enabledbooleanNoEnable AI Image Generation for visitors (default: false)
page_api_image_search_enabledbooleanNoEnable Image Search for visitors (default: false)
is_publishedbooleanNoPublish 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/pages
Response
{
  "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
ParameterTypeDescription
slugstringThe 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
FieldTypeDescription
html_contentstringFull HTML content of the page
namestringPage title/name
descriptionstringPage description (max 500 chars)
page_api_ai_enabledbooleanEnable AI Chat API for visitors
page_api_image_gen_enabledbooleanEnable AI Image Generation API for visitors
page_api_image_search_enabledbooleanEnable Image Search API for visitors
page_api_spawn_enabledbooleanEnable 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/123
Response
{
  "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/123

Page 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
GET

/api/join/:inviteCode

Get invite details (page name, role, membership status)

POST

/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
GET

/api/pages/:id/team-data

List all collections

GET

/api/pages/:id/team-data/:collection

List records in a collection

POST

/api/pages/:id/team-data/:collection

Create a new record

GET

/api/pages/:id/team-data/:collection/:recordId

Get a single record

PATCH

/api/pages/:id/team-data/:collection/:recordId

Update a record

DELETE

/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
ParameterTypeDescription
whereJSON stringFilter by data fields, e.g., {"status":"active"}
orderBystringSort field: created_at or updated_at
orderstringasc or desc (default)
limitnumberMax records to return (default 50, max 100)
offsetnumberNumber of records to skip for pagination
minebooleanOnly 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/tasks
Response
{
  "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-446655440000

DELETE /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
GET

/api/pages/:id/solo-data/:collection

List user's records in a collection

POST

/api/pages/:id/solo-data/:collection

Create a new record

GET

/api/pages/:id/solo-data/:collection/:recordId

Get a single record

PATCH

/api/pages/:id/solo-data/:collection/:recordId

Update a record

DELETE

/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
ParameterTypeDescription
whereJSON stringFilter by data fields, e.g., {"completed":true}
orderBystringSort field: created_at or updated_at
orderstringasc or desc (default)
limitnumberMax records to return (default 50, max 100)
offsetnumberNumber 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/todos

PATCH /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-446655440000

DELETE /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

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
GET

/api/pages/:id/social-data/:collection

List all records in a collection

POST

/api/pages/:id/social-data/:collection

Create a new record

GET

/api/pages/:id/social-data/:collection/:recordId

Get a single record

PATCH

/api/pages/:id/social-data/:collection/:recordId

Update a record (creator only)

DELETE

/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:read scope.

Query Parameters
ParameterTypeDescription
whereJSON stringFilter by data fields, e.g., {"category":"tech"}
orderBystringSort field: created_at or updated_at
orderstringasc or desc (default)
limitnumberMax records to return (default 50, max 100)
offsetnumberNumber of records to skip for pagination
minebooleanOnly return records created by the authenticated user
Example
curl -H "Authorization: Bearer vm_your_token_here" \
  "https://visimade.com/api/pages/123/social-data/posts?limit=10"
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:write scope.

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/posts

PATCH /api/pages/:id/social-data/:collection/:recordId

Update an existing record. You can only update records you created. Requires social-data:write scope.

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-446655440000

DELETE /api/pages/:id/social-data/:collection/:recordId

Delete a record. You can only delete records you created. Requires social-data:write scope.

curl -X DELETE \
  -H "Authorization: Bearer vm_your_token_here" \
  https://visimade.com/api/pages/123/social-data/posts/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/123

Endpoints
GET

/api/pages/:id/cms-data/:collection

List records (public, paid content filtered)

POST

/api/pages/:id/cms-data/:collection

Create a record (owner only)

GET

/api/pages/:id/cms-data/:collection/:recordId

Get a single record

PATCH

/api/pages/:id/cms-data/:collection/:recordId

Update a record (owner only)

DELETE

/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
ParameterTypeDescription
whereJSON stringFilter by data fields, e.g., {"category":"news"}
orderBystringSort field: created_at or updated_at
orderstringasc or desc (default)
limitnumberMax records to return (default 50, max 100)
offsetnumberNumber 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/posts
Example (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/posts

PATCH /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-446655440000

DELETE /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.

ModeData VisibilityWho Can WriteUse Case
pageNo data storageN/AStatic pages, informational content, no user data needed
solo_appPrivate to each userOwner onlyPersonal apps: todo lists, notes, habit trackers, journals
team_appTeam members onlyTeam members (role-based)Team collaboration: project boards, internal wikis, shared dashboards
social_appPublic (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
GET

/api/pages/:id/storage-config

Get current storage configuration

PUT

/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-config
Request Body
FieldTypeDescription
default_modestringStorage mode: page, solo_app, team_app, or social_app
allow_solo_appbooleanAllow solo app data storage
allow_team_appbooleanAllow team app data storage
allow_social_appbooleanAllow 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_appwindow.SoloData (private per-user data - others cannot see it)
team_appwindow.TeamData (shared team data)
social_appwindow.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 modal

TeamData 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 null

SocialData 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.

TopicBackgroundTextAccent
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.

LibraryCDN URL
ECharts (charts)https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js
Chart.jshttps://cdn.jsdelivr.net/npm/chart.js
Lucide Iconshttps://unpkg.com/lucide@latest
React 18https://unpkg.com/react@18/umd/react.production.min.js
React DOM 18https://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 CodeDescription
200Success
201Created (for POST requests)
400Bad request - invalid parameters or request body
401Unauthorized - invalid or missing token
403Forbidden - insufficient permissions or scopes
404Not found - resource does not exist
429Too many requests - rate limited
500Internal server error
Error Response Format
{
  "error": "Description of what went wrong",
  "code": "ERROR_CODE"
}
Common Error Codes
CodeDescription
INVALID_TOKENThe API token is invalid, expired, or revoked
INSUFFICIENT_SCOPEThe token does not have the required scope for this operation
UNAUTHORIZEDNo authentication provided
FORBIDDENAccess denied to this resource
NOT_FOUNDThe requested resource does not exist
BAD_REQUESTInvalid 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.

Developer Documentation - Visimade