API reference
Base URL
https://htmldrop.app/api/v1
Every endpoint below is relative to this base URL.
Authentication
Programmatic clients (scripts, CI, the MCP server) authenticate with a bearer API token:
Authorization: Bearer hsk_live_...
Create a token in the dashboard at /dashboard/settings under API tokens. The full token is shown once at creation time — store it somewhere safe.
Interactive OAuth clients (agents, third-party integrations) can instead
use OAuth 2.1: the authorization server's metadata is published at
/.well-known/oauth-authorization-server
so a compliant client can discover the authorize/token endpoints and
register itself without any hardcoded URLs.
The dashboard's own session cookie also satisfies requireAuth on these
routes, but a bearer token is what you want for anything outside a
browser.
Verified email required. Most authenticated endpoints require the
account's email to be verified — an unverified session gets back 403 {"error":"email_not_verified"}. The exceptions, scoped to let a brand-new
account get one site live before confirming, are POST /sites, GET /sites, GET /sites/{id}, DELETE /sites/{id}, POST /sites/{id}/upload, and POST /sites/{id}/upload-bundle. In practice this
mostly matters for dashboard sessions — API tokens can only be minted by an
already-verified account, so any request authenticated with Bearer hsk_live_… has already cleared that gate.
Create a site
POST /sites
Body is JSON. Both fields are optional — omit slug to get a random one,
omit name to leave it blank.
curl -X POST https://htmldrop.app/api/v1/sites \
-H "Authorization: Bearer hsk_live_..." \
-H "Content-Type: application/json" \
-d '{"slug": "my-project", "name": "My Project"}'
Returns 201 with the created site:
{
"id": "st_...",
"slug": "my-project",
"name": "My Project",
"access_mode": "public",
"has_password": false,
"branding_enabled": true,
...
}
A site has no content until you upload to it — creating it just reserves the slug.
Upload a single file
POST /sites/{id}/upload
Multipart form with one file field: an .html/.htm file served as-is,
or a .md file rendered to HTML. Every successful upload creates a new
version and immediately promotes it live.
curl -X POST https://htmldrop.app/api/v1/sites/st_.../upload \
-H "Authorization: Bearer hsk_live_..." \
-F "file=@./index.html"
Returns 201:
{
"version_id": "v_...",
"version_number": 2,
"byte_size": 4213,
"file_count": 1
}
Upload a multi-file bundle
POST /sites/{id}/upload-bundle
Multipart form for anything bigger than one file — a folder or a .zip.
There are two accepted shapes:
- Repeated
filesfields (one per file) with a parallelpathsfield giving each file's relative path (e.g.paths=index.html,paths=css/style.css) — this is what the dashboard's folder-picker sends, and multipart clients that can't preserve directory structure in the filename should send it too. - A single
filefield whose filename ends in.zip— htmldrop extracts it server-side and deploys the contents.
Either way, index.html must exist at the root of the bundle. Like a
single-file upload, this creates a new version and promotes it live.
# repeated files + paths
curl -X POST https://htmldrop.app/api/v1/sites/st_.../upload-bundle \
-H "Authorization: Bearer hsk_live_..." \
-F "files=@./dist/index.html" -F "paths=index.html" \
-F "files=@./dist/assets/app.css" -F "paths=assets/app.css" \
-F "files=@./dist/assets/app.js" -F "paths=assets/app.js"
# or a single zip
curl -X POST https://htmldrop.app/api/v1/sites/st_.../upload-bundle \
-H "Authorization: Bearer hsk_live_..." \
-F "file=@./site.zip"
Returns 201 with the same shape as a single-file upload (version_id,
version_number, byte_size, file_count).
List and get sites
GET /sites
GET /sites/{id}
curl https://htmldrop.app/api/v1/sites \
-H "Authorization: Bearer hsk_live_..."
curl https://htmldrop.app/api/v1/sites/st_... \
-H "Authorization: Bearer hsk_live_..."
GET /sites returns a JSON array of site objects; GET /sites/{id}
returns a single one, in the same shape POST /sites returns.
Delete a site
DELETE /sites/{id}
curl -X DELETE https://htmldrop.app/api/v1/sites/st_... \
-H "Authorization: Bearer hsk_live_..."
Returns 204 with no body.
The authenticated account
GET /me
curl https://htmldrop.app/api/v1/me \
-H "Authorization: Bearer hsk_live_..."
Returns the signed-in user and tenant: email, verification status, plan, role, and the org memberships available to switch between.
Plan limits
Caps are enforced on site creation, upload, and storage. Anonymous (no-account) drops are capped at 2 MB per file.
| Plan | Sites | Max upload | Storage |
|---|---|---|---|
| Free | 3 | 10 MB | 50 MB |
| Starter ($3/mo) | 10 | 25 MB | 250 MB |
| Plus ($8/mo) | 25 | 100 MB | 2 GB |
| Pro ($16/mo) | 100 | 250 MB | 5 GB |
| Business ($49/mo) | 100 | 500 MB | 20 GB |
Exceeding a cap on create or upload returns 402 with {"error": "plan_limit"} (site count) or {"error": "plan_storage_limit"} (storage),
each with a kind field naming what was hit.
Errors
Non-2xx responses are JSON: {"error": "<code>"}, sometimes with extra
fields for context. Common codes:
| Status | Code | Meaning |
|---|---|---|
| 400 | slug_invalid |
Slug failed validation |
| 400 | missing_file / missing_files |
Upload had no file(s) attached |
| 402 | plan_limit |
Site-count or feature cap reached for your plan |
| 402 | plan_storage_limit |
Storage cap reached for your plan |
| 403 | email_not_verified |
Account email isn't verified yet |
| 404 | not_found |
Site (or other resource) doesn't exist for your tenant |
| 409 | slug_taken |
Requested slug is already in use |
| 413 | file_too_large / bundle_too_large |
Upload exceeds your plan's per-upload cap |
| 429 | rate_limited |
Too many requests; see Retry-After below |
| 451 | quarantined |
Upload tripped an abuse heuristic |
429 responses carry a Retry-After header (seconds) telling you how long
to wait before retrying.