hataw.
← home

Docs

Hataw is one HTTP API. There's no SDK and no CLI. The fastest path is one call to /quick with your file inline. For larger sites use the manifest flow below.

Quick publish (one call)

The 90% case: send file bodies inline, get back a live URL. Up to 20 files, 5 MiB total per request.

curl -X POST https://hataw.dev/api/v1/publish/quick \
  -H 'content-type: application/json' \
  -d '{"files":[{"path":"index.html","encoding":"utf8","body":"<h1>hello</h1>"}]}'
# → { "siteUrl": "https://<slug>.hataw.dev/", "claimToken": "hct_...", ... }

File bodies default to base64. Use encoding:"utf8" for plain text. contentType is optional and inferred from the file extension.

Classic flow (manifest + uploads)

For sites bigger than the quick-publish caps, declare a manifest, PUT each file, then finalize. Append ?autoFinalize=1 to the last PUT to skip the finalize call entirely.

# 1. create the site (returns slug, uploads[], finalizeUrl)
RESP=$(curl -s -X POST https://hataw.dev/api/v1/publish \
  -H 'content-type: application/json' \
  -d '{"files":[{"path":"index.html","size":14}]}')

SLUG=$(echo "$RESP" | jq -r .slug)
UPLOAD=$(echo "$RESP" | jq -r '.uploads[0].uploadUrl')

# 2. upload the file body and auto-finalize on the last one
curl -X PUT "$UPLOAD&autoFinalize=1" \
  -H 'content-type: text/html' --data-binary '<h1>hello</h1>'
# → { "ok": true, "finalized": true, "siteUrl": "https://<slug>.hataw.dev/", ... }

API

POST /api/v1/publish/quick

One-call publish. Body: { files: [...], unlisted?: boolean }. Up to 20 files / 5 MiB. Returns { slug, siteUrl, deleteToken, claimToken? }.

POST /api/v1/publish

Create a new site from a manifest. Body: { files: [...], unlisted?: boolean }. Returns { slug, versionId, siteUrl, uploads[], finalizeUrl, deleteToken }.

PUT /api/v1/publish/upload/:slug/:path?v=:version&autoFinalize=1

Upload a single file body. Pass autoFinalize=1 on the last file to finalize implicitly.

POST /api/v1/publish/:slug/finalize

Lock the version in and make it live. Only needed if you didn't pass autoFinalize on the last upload.

GET /api/v1/publish/:slug

Get status for a site: pending | live | expired | not_found.

DELETE /api/v1/sites/:slug

Delete a site using its deleteToken. Pass as Authorization: Bearer hdt_... — irreversible.

PATCH /api/v1/sites/:slug

Toggle unlisted. Body: { unlisted: true|false }. Auth: same deleteToken.

Delete & unlisted

Every publish returns a deleteToken. Save it — it's the only way to take the site down or toggle visibility without an account.

Pass "unlisted": true in the publish body to hide from /fyp, /explore, and the sitemap. The site still works at its URL. Toggle later with PATCH /api/v1/sites/:slug.

URLs & expiry

Every site gets a subdomain: https://<slug>.your-host/. Sites are deleted 30 days after finalize. In dev, subdomains work automatically via *.localhost.

Telling an agent about Hataw

You don't need to paste these docs into your agent's prompt. Just tell it “publish this to hataw.dev” — any agent with web access will fetch hataw.dev/llms.txt (a concise agent-readable quickstart) or hataw.dev/llms-full.txt (the full API reference) and figure out the rest. There's also a full OpenAPI 3.1 spec at hataw.dev/openapi.json for tool ecosystems that want structured API introspection.

Thumbnails & social unfurls (automatic)

You don't have to do anything for your site to look good in /fyp, /explore, the owner dashboard, or when someone pastes your URL into Facebook, LinkedIn, X, iMessage, Discord, or Slack. Two things happen at finalize time:

  1. Headless Chromium takes a screenshot of your site and saves a 1200×750 JPEG at hataw.dev/api/v1/thumb/<slug>. Used as the thumbnail everywhere Hataw lists sites.
  2. OpenGraph + Twitter Card meta tags are auto-injected into the served HTML of your index.html. The injected tags point at the thumbnail above and use your extracted <title> + <meta name="description">. The bucket bytes are unchanged — this is a serving-layer rewrite, not a stored modification.

A plain <title> still helps. Hataw extracts it at finalize time and uses it as the primary label in the feed and as the injected og:title. If you skip it, sites just show the slug as a fallback and the publish response carries warning: "no_title".

Customize your social preview

The auto-injection has per-tag opt-out: if your HTML already contains <meta property="og:title">, Hataw leaves that one tag alone and only fills in the ones you didn't set. So you can override any subset — or the whole thing — by including your own tags. Here's the full template if you want complete control:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Your page title</title>
  <meta name="description" content="One-sentence description.">

  <!-- Open Graph (overrides auto-injection if present) -->
  <meta property="og:type" content="website">
  <meta property="og:title" content="Your page title">
  <meta property="og:description" content="One-sentence description.">
  <meta property="og:image" content="https://<slug>.hataw.dev/my-custom-preview.png">
  <meta property="og:url" content="https://<slug>.hataw.dev/">

  <!-- Twitter / X -->
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:title" content="Your page title">
  <meta name="twitter:description" content="One-sentence description.">
  <meta name="twitter:image" content="https://<slug>.hataw.dev/my-custom-preview.png">
</head>
<body>
  <!-- your content -->
</body>
</html>

Tip: if you want a custom og:image, upload it in the same publish (e.g. a 1200×630 PNG) and reference it by its absolute subdomain URL. Some crawlers reject relative paths.