dotsy Guest API

Start a site without an account using a guest token, then claim it later.

Operations 2

POST /api/v1/anon/sites Start a draft site without an account #
POST /api/v1/anon/claim Move a guest site onto your account #

Work with this as data

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-guest-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 Specification

dotsy-ai-guest-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: dotsy Guest 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: Guest
  description: Start a site without an account using a guest token, then claim it later.
paths:
  /api/v1/anon/sites:
    post:
      operationId: startGuestSite
      tags:
      - Guest
      summary: Start a draft site without an account
      description: 'Creates a draft-only site and a guest token, with no sign-in and no API key. This is the one call that needs no credentials.


        The response carries a **guest token** (`pgg_…`). Send it as `Authorization: Bearer ` and it authenticates as `GuestBearerAuth` for that one site: you can read and edit its content, routes and brief, but only the operations that list `GuestBearerAuth` in their security accept it. Publishing, renaming, deleting, custom domains, imports, asset writes, creating other sites and managing API keys answer `403` `guest_requires_account`, and any other site answers `403` `forbidden_site_scope`.


        The site is deleted 72 hours after creation (`expiresAt`) unless you claim it with `POST /api/v1/anon/claim` from a signed-in account.


        Guest creation is rate limited across the whole platform. Over the limit you get `429` `rate_limited` with a `Retry-After` header.'
      security: []
      requestBody:
        required: false
        description: Optional title and purpose for the new site.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StartGuestSiteRequest'
            examples:
              titled:
                summary: With a title and a purpose
                value:
                  title: Maple Street Bakery
                  purpose: 'A one-page site for a neighbourhood bakery: opening hours, the menu and a contact form.'
              empty:
                summary: Nothing
                value: {}
      responses:
        '201':
          description: 'The guest site exists. Keep `guestToken`: it is not shown again.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartGuestSiteResponse'
        '400':
          description: 'The body is not valid (`invalid_body`): `title` or `purpose` is too long or not a string.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: Too many guest sites were created in the last hour (`rate_limited`). Retry after the delay in `Retry-After`.
          headers:
            Retry-After:
              description: Seconds to wait before trying again (3600).
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
  /api/v1/anon/claim:
    post:
      operationId: claimGuestSite
      tags:
      - Guest
      summary: Move a guest site onto your account
      description: 'Moves the guest site behind a guest token into your own workspace as a Free site, then revokes the guest token. After this the site is an ordinary site: publish it, rename it and connect domains.


        Call it with a signed-in user''s **OAuth access token** in `Authorization: Bearer …`. A `pga_` API key or a guest token in that header answers `401` `unauthorized`, because a claim belongs to a person. The guest token of the site you are claiming goes in the body. Claiming the same site again as the same user answers `200` with the same body.


        Each account can claim one guest site (`409` `guest_claim_limit`). A workspace that is already at its Free-site limit answers `403` `free_site_limit`.'
      security:
      - BearerAuth: []
      requestBody:
        required: true
        description: The guest token of the site to claim.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClaimGuestSiteRequest'
            example:
              guestToken: pgg_Zk3qR1m7vT0aXc9LwYb2NfHj5uPdE8sGiO4tAx6VnMo
      responses:
        '200':
          description: The site is now in your workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClaimGuestSiteResponse'
        '400':
          description: 'The body is not valid (`invalid_body`): `guestToken` is missing or too short.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: The bearer is not an OAuth access token (`unauthorized`), or the guest token in the body is unknown, expired or revoked (`invalid_guest_token`).
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/ApiError'
                - type: object
                  properties:
                    error:
                      type: string
                      enum:
                      - unauthorized
                      - invalid_guest_token
                      description: '`unauthorized`: the bearer is not an OAuth access token. `invalid_guest_token`: the guest token is unknown, expired, revoked or already used.'
        '403':
          description: Your workspace is at its Free-site limit (`free_site_limit`). Nothing changed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '409':
          description: The site can't be claimed. Nothing changed.
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/ApiError'
                - type: object
                  properties:
                    error:
                      type: string
                      enum:
                      - anonymous_site_claimed
                      - guest_claim_limit
                      - site_not_found
                      - slug_conflict
                      description: '`anonymous_site_claimed`: another account already claimed this site. `guest_claim_limit`: this account already claimed a guest site. `site_not_found` / `slug_conflict`: the site could not be moved into your workspace.'
        '500':
          description: Your workspace could not be provisioned (`org_provision_failed`). Retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
components:
  schemas:
    ClaimGuestSiteResponse:
      type: object
      properties:
        slug:
          type: string
          description: The claimed site's slug. It keeps its `anon…` slug.
          example: anon3f9c1b27a4
        status:
          type: string
          enum:
          - claimed
      required:
      - slug
      - status
    StartGuestSiteRequest:
      type: object
      properties:
        title:
          type: string
          maxLength: 200
          description: Site title, up to 200 characters.
          example: Maple Street Bakery
        purpose:
          type: string
          maxLength: 2000
          description: What the site is for, up to 2,000 characters. It seeds the site brief. When you leave it out, dotsy infers a placeholder from the title.
          example: 'A one-page site for a neighbourhood bakery: opening hours, the menu and a contact form.'
      description: Both fields are optional. An empty body (or `{}`) is fine.
    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.'
    GuidanceHint:
      type: object
      properties:
        missing:
          type: array
          items:
            type: string
            enum:
            - design
            - content
            - seo
        next:
          type: string
          enum:
          - 'Before your first change: load get_skill(''guidance'') and write the missing sections.'
      required:
      - missing
      - next
      description: Which rules & guidelines sections are still empty and the MCP skill to fill them (`get_skill('guidance')`).
    StartGuestSiteResponse:
      type: object
      properties:
        slug:
          type: string
          description: 'The new site''s slug: `anon` followed by ten letters and digits.'
          example: anon3f9c1b27a4
        guestToken:
          type: string
          description: 'The guest token (`pgg_…`). It is returned only here, so store it. Send it as `Authorization: Bearer <guestToken>`; it authenticates as `GuestBearerAuth` on this one site.'
          example: pgg_Zk3qR1m7vT0aXc9LwYb2NfHj5uPdE8sGiO4tAx6VnMo
        expiresAt:
          type: number
          description: When the guest site is deleted if nobody claims it, Unix ms. 72 hours after creation.
          example: 1790000000000
        draftOnly:
          type: boolean
          enum:
          - true
          description: 'Always true: a guest site can be edited as a draft but cannot be published until it is claimed.'
        guidance:
          $ref: '#/components/schemas/GuidanceHint'
      required:
      - slug
      - guestToken
      - expiresAt
      - draftOnly
    ClaimGuestSiteRequest:
      type: object
      properties:
        guestToken:
          type: string
          minLength: 20
          description: The `guestToken` that `POST /api/v1/anon/sites` returned for the site you want to keep.
          example: pgg_Zk3qR1m7vT0aXc9LwYb2NfHj5uPdE8sGiO4tAx6VnMo
      required:
      - guestToken
      description: The guest token of the site to claim.
    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.