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.
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.
curl -H "Authorization: Bearer hvs_…" https://harvis.dev/api/v1
curl -H "Authorization: Bearer hvs_…" https://harvis.dev/api/v1/sites
curl -X POST https://harvis.dev/api/v1/sites \ -H "Authorization: Bearer hvs_…" \ -F "files=@index.html" -F "paths=index.html"
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.
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
The whole surface
Full request and response shapes are in the OpenAPI document.
| method | path | what it does |
|---|---|---|
| GET | /api/v1 | Check that a credential works. |
| GET | /api/v1/keys | The account's API keys. Revoked keys are not listed. |
| POST | /api/v1/keys | Create an API key. |
| DELETE | /api/v1/keys/{keyId} | Revoke an API key. Anything using it stops working immediately. |
| GET | /api/v1/me | The account a credential belongs to. |
| 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. |
| GET | /api/v1/sites/{id}/files | Every file a site is serving. |
| POST | /api/v1/sites/{id}/files | Add or overwrite individual files, leaving the rest of the site alone. |
| DELETE | /api/v1/sites/{id}/submissions | Delete every submission for a site, or every one of a single form. |
| GET | /api/v1/sites/{id}/submissions | One 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/csv | Export a site's form submissions as CSV. |
| POST | /api/v1/sites/{id}/submissions/read | Mark every unread submission as read. |
| GET | /api/v1/sites/{id}/submissions/summary | How many submissions a site holds, how many are unread, and which forms exist. |
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."
}
}