Harvis Sites API

Creating, deploying and managing hosted sites.

GitHub OpenAPI

Operations 9

GET /api/v1/sites List the account's sites, newest first #
POST /api/v1/sites Create a site from a folder of files #
DELETE /api/v1/sites/{id} Delete a site, its files and its form submissions #
GET /api/v1/sites/{id} One site #
PATCH /api/v1/sites/{id} Rename a site, change its web address, or both #
POST /api/v1/sites/{id}/deploy Replace every file of a site with the uploaded set #
POST /api/v1/sites/{id}/deploy-token Issue a new deploy token for a site. The old one stops working immediately #
POST /api/v1/sites/{id}/deploy/zip Replace every file of a site from a zip archive sent as the raw request body #
POST /api/v1/sites/import Create a site by downloading a single page from a known artifact URL #

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/harvis-dev:harvis-dev-sites-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

harvis-dev-sites-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: harvis.dev Sites API
  version: '1'
  description: Everything an account can control, over HTTP. This document is generated from lib/api/contract.ts, which is also what the dashboard's server actions call — so what the harvis CLI can do and what the web app can do are the same list by construction. Do not edit this file by hand; run `bun run api:generate`.
servers:
- url: https://harvis.dev
tags:
- name: Sites
  description: Creating, deploying and managing hosted sites.
paths:
  /api/v1/sites:
    get:
      operationId: sites.list
      summary: List the account's sites, newest first
      tags:
      - Sites
      security:
      - apiKey: []
      - session: []
      responses:
        '200':
          description: List the account's sites, newest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteList'
        '401':
          description: '`unauthorized`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: '`suspended`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
    post:
      operationId: sites.create
      summary: Create a site from a folder of files
      description: The site belongs to the account that authorized the call — there is no claim link and no 24-hour expiry, unlike an anonymous deploy to /api/upload. A single shared top-level folder is stripped, junk files (.DS_Store, __MACOSX…) are dropped, and the homepage is chosen as the root index.html if there is one. 50 MB and 500 files.
      tags:
      - Sites
      security:
      - apiKey: []
      - session: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CreateSiteInput'
      responses:
        '201':
          description: Create a site from a folder of files.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSiteResult'
        '400':
          description: '`invalidRequest`, `noFiles`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: '`unauthorized`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: '`suspended`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '413':
          description: '`tooLarge`, `tooManyFiles`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
  /api/v1/sites/{id}:
    delete:
      operationId: sites.delete
      summary: Delete a site, its files and its form submissions
      description: The address is released and the site stops resolving. This cannot be undone.
      tags:
      - Sites
      security:
      - apiKey: []
      - session: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Delete a site, its files and its form submissions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkResult'
        '401':
          description: '`unauthorized`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: '`suspended`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          description: '`siteNotFound`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
    get:
      operationId: sites.get
      summary: One site
      tags:
      - Sites
      security:
      - apiKey: []
      - session: []
      parameters:
      - name: id
        in: path
        required: true
        description: The site's id, as returned by sites.list.
        schema:
          type: string
      responses:
        '200':
          description: One site.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteResult'
        '401':
          description: '`unauthorized`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: '`suspended`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          description: '`siteNotFound`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
    patch:
      operationId: sites.update
      summary: Rename a site, change its web address, or both
      description: Absent fields are left alone. A subdomain is unique across the whole service, not just this account, so a taken one is refused rather than queued.
      tags:
      - Sites
      security:
      - apiKey: []
      - session: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSiteInput'
      responses:
        '200':
          description: Rename a site, change its web address, or both.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteResult'
        '400':
          description: '`invalidRequest`, `nameRequired`, `subdomainInvalid`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: '`unauthorized`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: '`suspended`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          description: '`siteNotFound`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '409':
          description: '`subdomainTaken`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
  /api/v1/sites/{id}/deploy:
    post:
      operationId: sites.deploy
      summary: Replace every file of a site with the uploaded set
      description: 'The site''s whole file list is replaced: paths absent from the upload are deleted from storage. Form submissions belong to the site, not to the deploy, and survive. The web address does not change. 50 MB and 500 files.'
      tags:
      - Sites
      security:
      - apiKey: []
      - session: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/DeploySiteInput'
      responses:
        '200':
          description: Replace every file of a site with the uploaded set.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeployResult'
        '400':
          description: '`invalidRequest`, `noFiles`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: '`unauthorized`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: '`suspended`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          description: '`siteNotFound`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '413':
          description: '`tooLarge`, `tooManyFiles`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
  /api/v1/sites/{id}/deploy-token:
    post:
      operationId: sites.rotateToken
      summary: Issue a new deploy token for a site. The old one stops working immediately
      description: A deploy token is scoped to one site and can only redeploy it — that is what the GitHub Action and `harvis link` use. An account key does strictly more; this exists for handing a single site to something you would rather not trust with the account.
      tags:
      - Sites
      security:
      - apiKey: []
      - session: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Issue a new deploy token for a site. The old one stops working immediately.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeployTokenResult'
        '401':
          description: '`unauthorized`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: '`suspended`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          description: '`siteNotFound`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
  /api/v1/sites/{id}/deploy/zip:
    post:
      operationId: sites.deployZip
      summary: Replace every file of a site from a zip archive sent as the raw request body
      description: 'The same operation as sites.deploy, for callers that would rather not assemble a multipart body: `curl --data-binary @site.zip -H ''Content-Type: application/zip''`. A single shared top-level folder inside the archive is stripped.'
      tags:
      - Sites
      security:
      - apiKey: []
      - session: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: name
        in: query
        required: false
        schema:
          type: string
          maxLength: 120
      requestBody:
        required: true
        description: The archive itself, as the raw body.
        content:
          application/zip:
            schema:
              type: string
              format: binary
      responses:
        '200':
          description: Replace every file of a site from a zip archive sent as the raw request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeployResult'
        '400':
          description: '`invalidRequest`, `noFiles`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: '`unauthorized`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: '`suspended`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          description: '`siteNotFound`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '413':
          description: '`tooLarge`, `tooManyFiles`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
  /api/v1/sites/import:
    post:
      operationId: sites.import
      summary: Create a site by downloading a single page from a known artifact URL
      description: 'The shortcut past copy-and-paste for a page an AI chat built: send the artifact''s public address and the fetched HTML becomes the site''s index.html. Only hosts that serve an artifact as a plain document can be fetched — see lib/artifact-sources.ts for the list — and a chat''s own share link is refused with the reason, because it renders the conversation rather than the page inside it. One file, 5 MB; use sites.create for anything with assets.'
      tags:
      - Sites
      security:
      - apiKey: []
      - session: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportSiteInput'
      responses:
        '201':
          description: Create a site by downloading a single page from a known artifact URL.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSiteResult'
        '400':
          description: '`importFailed`, `importUnsupported`, `invalidRequest`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: '`unauthorized`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: '`suspended`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '413':
          description: '`tooLarge`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
components:
  schemas:
    SiteList:
      type: object
      properties:
        sites:
          type: array
          items:
            $ref: '#/components/schemas/Site'
      required:
      - sites
      additionalProperties: false
    CreateSiteResult:
      type: object
      properties:
        site:
          $ref: '#/components/schemas/Site'
        deployToken:
          description: Scoped to this one site. Shown here so a CI job can be given it without the account key.
          type: string
      required:
      - site
      - deployToken
      additionalProperties: false
    Site:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        subdomain:
          type: string
        url:
          description: Where the site is served, e.g. https://sunny-otter-42.harvis.page.
          type: string
        entrypoint:
          description: The file served at the site's root.
          type: string
        fileCount:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        totalSize:
          description: Bytes stored, summed over every file.
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        createdAt:
          format: date-time
          type: string
        updatedAt:
          format: date-time
          type: string
      required:
      - id
      - name
      - subdomain
      - url
      - entrypoint
      - fileCount
      - totalSize
      - createdAt
      - updatedAt
      additionalProperties: false
    UpdateSiteInput:
      type: object
      properties:
        name:
          description: A label for the dashboard; not shown to visitors.
          type: string
          maxLength: 120
        subdomain:
          description: 5–63 lowercase letters, numbers or hyphens. The site moves to it immediately.
          type: string
      additionalProperties: false
    ApiError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              description: A stable machine code — see docs/api.md for the full list.
              type: string
            message:
              description: English prose, for a human reading a log.
              type: string
          required:
          - code
          - message
          additionalProperties: false
      required:
      - error
      additionalProperties: false
    DeployTokenResult:
      type: object
      properties:
        deployToken:
          type: string
      required:
      - deployToken
      additionalProperties: false
    DeployResult:
      type: object
      properties:
        site:
          $ref: '#/components/schemas/Site'
        replaced:
          description: How many files the site now serves.
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
      required:
      - site
      - replaced
      additionalProperties: false
    ImportSiteResult:
      type: object
      properties:
        site:
          $ref: '#/components/schemas/Site'
        deployToken:
          type: string
      required:
      - site
      - deployToken
      additionalProperties: false
    ImportSiteInput:
      type: object
      properties:
        url:
          description: The artifact's public https address, e.g. https://claude.ai/public/artifacts/<id>.
          type: string
          maxLength: 2048
        name:
          description: Defaults to the imported page's <title>.
          type: string
          maxLength: 120
      required:
      - url
      additionalProperties: false
    DeploySiteInput:
      type: object
      properties:
        files:
          type: array
          items:
            type: string
            format: binary
            contentEncoding: binary
        paths:
          description: One per file, same order — the path relative to the site root.
          type: array
          items:
            type: string
        name:
          description: Renames the site as well, if given.
          type: string
          maxLength: 120
      required:
      - files
      - paths
      additionalProperties: false
    CreateSiteInput:
      type: object
      properties:
        files:
          description: One part per file of the site.
          type: array
          items:
            type: string
            format: binary
            contentEncoding: binary
        paths:
          description: One part per file, in the same order — the file's path relative to the site root. Required because multipart strips directory separators from a file name.
          type: array
          items:
            type: string
        name:
          description: Defaults to the folder name, then to the homepage's <title>.
          type: string
          maxLength: 120
      required:
      - files
      - paths
      additionalProperties: false
    SiteResult:
      type: object
      properties:
        site:
          $ref: '#/components/schemas/Site'
      required:
      - site
      additionalProperties: false
    OkResult:
      type: object
      properties:
        ok:
          type: boolean
          const: true
      required:
      - ok
      additionalProperties: false
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: hvs_…
      description: An account key, created at https://harvis.dev/dashboard/keys and shown once.
    session:
      type: apiKey
      in: cookie
      name: better-auth.session_token
      description: The dashboard's own sign-in cookie. Listed because the browser calls these same endpoints; a CLI or a server should use apiKey instead.