Every API here is available over the APIs.io API and to AI agents over MCP.
MCP server
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
Tools for apis
7 MCP tools reach this
find_apisBrowse and filter every API in the catalog.
get_api_artifactsOne API's artifacts, grouped by type.
get_openapiThe primary OpenAPI for this API.
find_similar_apisAPIs that look like this one.
apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
resolveTurn a domain, URL or GitHub org into the provider it belongs to.
find_cohortsEvery scored population of providers in the catalog.
All 92 tools →
Call it yourself
curl for this page
This API
curl "https://apis.io/api/v1/apis/dotsy-ai:dotsy-ai-pages-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Get an API key
Free tier, no form to fill in. Signing in shares your email address with us — we
store it to create your key and to recognise you if you sign in with another
provider. See our Privacy Policy and
Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: dotsy Pages API
version: 1.0.0
description: 'Build, edit and publish dotsy sites over HTTP. The dashboard, the `dotsy` CLI and the MCP server all use this API.
Production base URL: `https://api.dotsy.ai`. The **Try it** console in this reference sends requests to the origin you are reading it on, so it needs no host setting.
## Authentication
Send `Authorization: Bearer ` on every request. Three kinds of token work:
- **Account API key** (`pga_…`). Create one in the dashboard or with `POST /api/v1/account/keys`. It acts as one account.
- **OAuth token.** MCP clients that sign in with OAuth send their opaque access token in the same header.
- **Guest token** (`pgg_…`). `POST /api/v1/anon/sites` returns one with a new site. It works on that one site only, and publish, rename, delete, domains, imports and asset writes need an account instead.
To choose the workspace a request acts in, send `X-Dotsy-Workspace: `. It applies to sign-in sessions and OAuth tokens. A `pga_` key is tied to one account and ignores it. A workspace you don''t belong to answers `403`.
## Errors
Failures answer with a JSON body `{ "error": "", "hint": "" }`. `error` is a stable machine code, so branch on it and not on the text. Validation failures add `issues`, the list of problems, and some errors add fields that belong to them, such as `jobId` or `keys`. Every error response in this reference uses the `ApiError` schema.
## Lists and paging
Each list operation that supports paging documents `limit` (1 to 1000) and `cursor`. Send `limit` to page. The response then carries `nextCursor`: a string while more results remain, and `null` on the last page. Pass it back unchanged as `cursor` to get the next page. Cursors are opaque, so don''t build or parse them.
## Sites, slugs and ids
Site operations live under `/api/v1/sites/{slug}`. `{slug}` takes the site''s slug or its id (a UUID, returned as `id`). A slug changes when the site is renamed, so keep the id if you need a handle that stays put.
- While a renamed, published site''s old slug stays reserved, it answers `301` for GET and HEAD and `308` for every other method. `Location` holds the same path under the current slug.
- A site id answers `307` with `Location` set to the path under the current slug.
**Follow redirects** (`curl -L`, and the default in `fetch`), then use the slug from `Location` from that point on. Slug-scoped API keys and guest tokens only work with the site''s current slug.
Content keys and asset paths in `{key}` and `{path}` can contain `/`. Percent-encode each segment. A `/` may be sent raw or as `%2F`.
## Rate limits
Writes (POST, PUT, PATCH and DELETE under `/api/v1/sites/…`) share a budget of 600 per minute per account. Raw asset uploads (`PUT …/assets-raw/…`) have their own budget of 3,000 per minute, and sending site invites has another of 30 per minute. Reads are not counted. Over budget, you get `429` with `error: "rate_limited"` and a `Retry-After` header in seconds.
## Long-running work
Imports, large renames and large changesets answer `202 Accepted` with a `job` object (`id`, `kind`, `status`). Poll `GET /api/v1/sites/{slug}/jobs/{id}` every few seconds until `status` is `succeeded` or `failed`. An operation that finishes right away returns its result and no job.'
servers:
- url: /
security:
- BearerAuth: []
tags:
- name: Pages
description: List a site's pages with their routes, titles and draft state.
paths:
/api/v1/sites/{slug}/pages:
get:
operationId: listPages
tags:
- Pages
summary: List a site's pages
description: One row per page, with its URL, layout, title, what it places (sections, menus, listings) and whether it has staged changes. A route that stands for many pages (a collection) appears as one row with a `pattern`. The rows come from an index, so no page bodies are read. Filter with `q` (path or title), `layout` or `noLayout`, `flag` (single chip), and page with `limit` and `cursor`. Pass `group=layout` for layout-key group order and `pv3.` cursors (default order keeps `pv2.`).
parameters:
- name: limit
in: query
required: false
description: Page size, 1 to 1000. Defaults to 1000; larger values are clamped. Follow `nextCursor` for the rest.
schema:
type: integer
minimum: 1
maximum: 1000
default: 1000
- name: cursor
in: query
required: false
description: Opaque value from the previous page's `nextCursor`.
schema:
type: string
- name: q
in: query
required: false
description: Only pages whose path or title contains this text (case-insensitive).
schema:
type: string
- name: layout
in: query
required: false
description: Only pages that use this layout, by file key (`layouts/default.html`) or name (`default`).
schema:
type: string
- name: noLayout
in: query
required: false
description: Only pages with no layout. Cannot be combined with `layout`.
schema:
type: boolean
- name: flag
in: query
required: false
description: Only pages matching this chip (combines with other filters).
schema:
type: string
enum:
- changes
- unpublished
- hidden
- test
- norule
- name: group
in: query
required: false
description: When `layout`, rows are ordered by layout file key (code unit), no-layout last, then path within each group. Cursors use the `pv3.` prefix.
schema:
type: string
enum:
- layout
- name: pagesLimit
in: query
required: false
description: Old name for `limit`. Use `limit`.
schema:
type: integer
deprecated: true
- name: pagesCursor
in: query
required: false
description: Old name for `cursor`. Use `cursor`.
schema:
type: string
deprecated: true
- name: pagesQ
in: query
required: false
description: Old name for `q`. Use `q`.
schema:
type: string
deprecated: true
- schema:
type: string
minLength: 1
example: acme-studio
required: true
description: Site slug or site id (UUID). An old slug answers 301/308 and a site id 307 to the canonical slug path.
name: slug
in: path
security:
- BearerAuth: []
- GuestBearerAuth: []
responses:
'200':
description: One page of rows.
content:
application/json:
schema:
$ref: '#/components/schemas/PagesList'
'400':
description: The `cursor` or a filter is not valid (`invalid_cursor`, `invalid_params`).
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: Missing or invalid credentials.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: The key or token cannot read this site.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: No such site (`site_not_found`).
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'409':
description: The site's storage is not ready yet (`backend_not_ready`). Retry shortly.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'503':
description: The page index could not be read (`outline_unavailable`). Retry shortly.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
components:
schemas:
PageRow:
type: object
properties:
path:
type: string
description: The URL path. For a page with no route, the path its file name serves.
example: /about
contentKey:
type:
- string
- 'null'
layout:
type:
- string
- 'null'
description: The layout's file key.
example: layouts/default.html
title:
type:
- string
- 'null'
description: The route title, else the page's `<title>`.
parts:
type:
- array
- 'null'
items:
$ref: '#/components/schemas/PagePart'
description: What the page places (sections, menus, listings). `null` until the page is indexed.
ownChrome:
type:
- boolean
- 'null'
description: The page carries its own header or footer. `null` until indexed.
noindex:
type: boolean
experiment:
type: boolean
description: The page has a draft or running A/B test.
hasChanges:
type: boolean
description: The page has a staged change.
published:
type: boolean
description: False for a page that was never published.
pattern:
type: object
properties:
count:
type: integer
collection:
type:
- string
- 'null'
required:
- count
- collection
description: A dynamic route that stands for several pages.
noRoute:
type: boolean
enum:
- true
description: The page exists as a file but no route points at it.
required:
- path
- contentKey
- layout
- title
- parts
- ownChrome
- noindex
- experiment
- hasChanges
- published
PagesList:
type: object
properties:
routes:
type: array
items:
$ref: '#/components/schemas/PageRow'
nextCursor:
type:
- string
- 'null'
description: Pass as `cursor` (with the same `limit`) for the next page; null on the last page.
routesListTotal:
type:
- integer
- 'null'
description: Rows that match the filter. `null` when the list is a partial window.
partial:
type: boolean
description: True when the list is a window of the site rather than all of it.
indexing:
type: object
properties:
pending:
type: integer
description: Pages whose structure is not indexed yet.
running:
type: boolean
description: Indexing is still running.
required:
- pending
- running
required:
- routes
- nextCursor
- routesListTotal
- partial
- indexing
LimitUpgrade:
type: object
properties:
limit:
type: string
description: 'What ran out: `sites`, `storage`, `domains`, `pages`, `abSites`, `siteDb`, or a feature name.'
used:
type:
- number
- 'null'
max:
type:
- number
- 'null'
incoming:
type: number
description: What this call would have added.
plan:
type: string
enum:
- free
- core
- studio
- agency
description: The workspace's plan today.
requiredPlan:
type: string
enum:
- free
- core
- studio
- agency
description: The cheapest workspace plan that lifts it. Absent on the top plan.
addOn:
type: string
enum:
- extraDomain
- siteDb
- abTest
description: Set when one add-on lifts it (paid workspaces only).
price:
type:
- object
- 'null'
properties:
amount:
type: number
currency:
type: string
enum:
- usd
interval:
type: string
enum:
- month
required:
- amount
- currency
- interval
description: Monthly price of `addOn`, else of `requiredPlan`.
scope:
type: string
enum:
- workspace
- site
url:
type: string
description: Opens the upgrade sheet. A signed link that expires; paying still needs an owner or admin.
canUpgrade:
type: boolean
description: 'false: the caller cannot pay — ask a workspace owner.'
retryable:
type: boolean
description: Retrying the same call can succeed without upgrading.
required:
- limit
- used
- max
- plan
- price
- scope
- url
- canUpgrade
- retryable
description: 'On plan-limit refusals: what lifts the limit and the link to do it.'
PagePart:
type: object
properties:
kind:
type: string
enum:
- section
- menu
- list
- slot
- chrome
description: '`section`: a shared fragment. `menu`: a navigation block. `list`: a collection listing. `slot`: where the page body goes. `chrome`: the page''s own header or footer.'
name:
type: string
dynamic:
type: boolean
enum:
- true
required:
- kind
ApiError:
type: object
properties:
error:
type: string
description: Stable machine-readable code, e.g. `invalid_body`.
example: invalid_body
hint:
type: string
description: What to do next, in plain words.
message:
type: string
field:
type: string
description: The request field the error is about, when there is one.
issues:
type: array
items: {}
description: Validation issues (zod issues, or gate issue strings/objects).
upgrade:
$ref: '#/components/schemas/LimitUpgrade'
stop:
type: string
description: 'On plan-limit refusals: the line an agent shows the user before it stops.'
fix:
type: object
properties:
tool:
type: string
enum:
- get_plan
args:
type: object
properties:
slug:
type: string
required:
- tool
- args
description: 'On plan-limit refusals: the one safe next call (MCP tool + arguments).'
requestId:
type: string
description: Correlation id for support (`x-dotsy-request-id`).
required:
- error
securitySchemes:
BearerAuth:
type: http
scheme: bearer
description: Account API key (pga_…). OAuth MCP sessions use the same header with an opaque token.
GuestBearerAuth:
type: http
scheme: bearer
description: Guest token (pgg_…) returned by POST /api/v1/anon/sites. Works only on that one site, and some operations (publish, rename, delete, domains, imports, asset writes) need an account instead.