http api

Everything the dashboard does, from your terminal

One account key. List your sites, deploy to them, rename them, read what your forms collected, delete them — over plain HTTP, from a script, a CI job, or an agent.

quickstart

Three commands

Create a key on your dashboard, check it works, and put a folder online. The key goes in an Authorization header on every call.

Check the key works
curl -H "Authorization: Bearer hvs_…" https://harvis.dev/api/v1
See what's on the account
curl -H "Authorization: Bearer hvs_…" https://harvis.dev/api/v1/sites
Publish a folder
curl -X POST https://harvis.dev/api/v1/sites \
  -H "Authorization: Bearer hvs_…" \
  -F "files=@index.html" -F "paths=index.html"
one contract

The web app and the API cannot drift apart

Every capability is declared once, in one file, and both the dashboard and this API are built from it. openapi.json is generated from that declaration, and the build fails if the two disagree. So this is not documentation that describes the product — it is the thing the product is made of.

  • One implementation per capability, called by the web app and by your script alike.
  • openapi.json is generated, never hand-written, and served at /openapi.json.
  • A build fails if a route, the contract and the document stop agreeing.
deploys

A key means the site is yours immediately

Deploying without an account still works and always will — you get a private claim link to open later. Send a key on the same call and there is nothing to claim: the site is on your account from the first byte, and it never expires.

zip -r site.zip . && curl -X POST https://harvis.dev/api/upload \
  -H "Authorization: Bearer hvs_…" \
  -H "Content-Type: application/zip" --data-binary @site.zip
endpoints

The whole surface

Full request and response shapes are in the OpenAPI document.

methodpathwhat it does
GET/api/v1Check that a credential works.
GET/api/v1/keysThe account's API keys. Revoked keys are not listed.
POST/api/v1/keysCreate an API key.
DELETE/api/v1/keys/{keyId}Revoke an API key. Anything using it stops working immediately.
GET/api/v1/meThe account a credential belongs to.
GET/api/v1/sitesList the account's sites, newest first.
POST/api/v1/sitesCreate 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}/deployReplace every file of a site with the uploaded set.
POST/api/v1/sites/{id}/deploy-tokenIssue a new deploy token for a site. The old one stops working immediately.
POST/api/v1/sites/{id}/deploy/zipReplace every file of a site from a zip archive sent as the raw request body.
GET/api/v1/sites/{id}/filesEvery file a site is serving.
POST/api/v1/sites/{id}/filesAdd or overwrite individual files, leaving the rest of the site alone.
DELETE/api/v1/sites/{id}/submissionsDelete every submission for a site, or every one of a single form.
GET/api/v1/sites/{id}/submissionsOne page of a site's form submissions, newest first.
DELETE/api/v1/sites/{id}/submissions/{submissionId}Delete one submission.
GET/api/v1/sites/{id}/submissions/{submissionId}One submission.
GET/api/v1/sites/{id}/submissions/csvExport a site's form submissions as CSV.
POST/api/v1/sites/{id}/submissions/readMark every unread submission as read.
GET/api/v1/sites/{id}/submissions/summaryHow many submissions a site holds, how many are unread, and which forms exist.
errors

Every failure has the same shape

Switch on the code, never on the message. The codes are stable; the prose is for whoever reads the log.

{
  "error": {
    "code": "subdomainTaken",
    "message": "That address is already taken. Try another."
  }
}