Docs

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:

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.