# htmlbin — Agent Protocol (markdown)

> The canonical, machine-readable form of this is at the same URL with
> `Accept: application/json`. This markdown variant is the same protocol
> rendered for humans previewing the API.

You are deploying an HTML artifact to htmlbin. Anyone with the URL can view it.
htmlbin is built for agents: the workflow below is the canonical, fastest path.

## TL;DR

1. POST /api/auth/start         → get a code + verification_url + poll_token
2. Print the URL + code to the human; ask them to verify (one-time)
3. GET /api/auth/poll?token=…   → poll until you receive an api_token
4. POST /api/drops (Bearer api_token) → upload HTML, receive a public URL

The api_token is shown exactly once. Store it for reuse on this machine.

## Naming + error conventions

- All request and response field names are **snake_case** (`raw_url`, `latest_version`, `created_at`, etc.).
- All 4xx/5xx responses share this shape:

```json
{
  "error": {
    "code": "html_too_large",
    "message": "HTML exceeds 2097152 bytes.",
    "details": { "max_bytes": 2097152 }
  }
}
```

Switch on `error.code`. `error.message` is for human display. `details` is optional.

## Step 1: Authenticate (one-time per agent install)

Check for an existing token first. The convention, in order of preference:
1. `./.htmlbin/token` (project-local — preferred; no permission prompt)
2. `HTMLBIN_TOKEN` environment variable
3. `~/.config/htmlbin/token` (machine-global fallback)

If you don't have a token, run the device-code flow:

```bash
START=$(curl -s -X POST https://htmlbin.dev/api/auth/start \
  -H "Content-Type: application/json" \
  -d '{"label":"claude-code"}')

CODE=$(echo "$START"        | jq -r .code)
URL=$(echo "$START"         | jq -r .verification_url)
POLL=$(echo "$START"        | jq -r .poll_token)
INTERVAL=$(echo "$START"    | jq -r .poll_interval)

echo ""
echo "  Open this URL and verify:"
echo "    $URL"
echo ""
echo "  Code:  $CODE"
echo ""

while true; do
  RESP=$(curl -s "https://htmlbin.dev/api/auth/poll?token=$POLL")
  STATUS=$(echo "$RESP" | jq -r .status)
  case "$STATUS" in
    pending)  sleep "$INTERVAL" ;;
    verified) export HTMLBIN_TOKEN=$(echo "$RESP" | jq -r .api_token); break ;;
    *)        echo "auth failed: $STATUS" >&2; exit 1 ;;
  esac
done

mkdir -p ./.htmlbin && echo "$HTMLBIN_TOKEN" > ./.htmlbin/token
chmod 600 ./.htmlbin/token
```

The verification URL drops the human onto a "Sign in with GitHub" page. We
ask for the `read:user` scope only (public username + id) and bind one
htmlbin account per GitHub identity, so quotas and existing drops stick
across devices. That click is the only human step.

## Step 2: Generate HTML

Author a complete, self-contained HTML document. The file is rendered in an
iframe, so it must look right standalone.

- All CSS in `<style>` (CDNs OK — Tailwind, Alpine, esm.sh, etc.)
- All JS in `<script>` (CDNs OK)
- No build step on our side — what you upload is what's served
- Up to 2 MB per file

## Step 3: Upload (creates v1)

```bash
cat > /tmp/htmlbin.html <<'HTMLEOF'
<!doctype html>
<html>...</html>
HTMLEOF

jq -n --arg title "My Prototype" \
       --arg description "What this is showing" \
       --rawfile html /tmp/htmlbin.html \
       '{title:$title, description:$description, html:$html}' \
| curl -s -X POST https://htmlbin.dev/api/drops \
    -H "Authorization: Bearer $HTMLBIN_TOKEN" \
    -H "Content-Type: application/json" \
    -d @-
```

Response (HTTP 201): the full `Drop` object:
```json
{
  "slug": "aB3xK7g",
  "title": "My Prototype",
  "description": "What this is showing",
  "url": "https://htmlbin.dev/p/aB3xK7g",
  "raw_url": "https://htmlbin.dev/p/aB3xK7g/raw",
  "locked": false,
  "latest_version": 1,
  "view_count": 0,
  "created_at": 0,
  "updated_at": 0
}
```

Print the `url` to the user.

## Iterating: PUT for new versions, PATCH for metadata

**`PUT /api/drops/<slug>`** mints a new version (html required):

```bash
jq -n --rawfile html /tmp/htmlbin.html \
       --arg context "Tweaked colors after user feedback" \
       '{html:$html, context:$context}' \
| curl -s -X PUT https://htmlbin.dev/api/drops/<slug> \
    -H "Authorization: Bearer $HTMLBIN_TOKEN" \
    -H "Content-Type: application/json" \
    -d @-
```

**`PATCH /api/drops/<slug>`** updates title/description without minting a version:

```bash
curl -s -X PATCH https://htmlbin.dev/api/drops/<slug> \
  -H "Authorization: Bearer $HTMLBIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Better title"}'
```

Including `html` in a PATCH returns `400 metadata_only_on_patch`.

Humans switch versions in the viewer with `?v=N`. Default = latest.

## Context (optional, opt-in)

The `context` field on POST/PUT lets you record the prompt, reasoning,
or thinking trace that produced the HTML. **It is opt-in and may be
sensitive — only include it if the human has agreed.** When present, the
viewer exposes it under a discreet "context" toggle.

## Listing your drops (paginated)

```bash
curl -s "https://htmlbin.dev/api/drops?page=1&pageSize=50&sortBy=updated_at&sortOrder=desc" \
  -H "Authorization: Bearer $HTMLBIN_TOKEN"
```

Response:
```json
{
  "data": [ /* Drop[] */ ],
  "pagination": {
    "page": 1, "page_size": 50, "total_items": 142, "total_pages": 3,
    "sort_by": "updated_at", "sort_order": "desc"
  }
}
```

## Delete

```bash
# Delete a single version (refused on the only remaining one) — returns the updated Drop
curl -s -X DELETE https://htmlbin.dev/api/drops/<slug>/v/<n> \
  -H "Authorization: Bearer $HTMLBIN_TOKEN"

# Delete the whole drop — returns 204 No Content
curl -s -X DELETE https://htmlbin.dev/api/drops/<slug> \
  -H "Authorization: Bearer $HTMLBIN_TOKEN"
```

## Passcode (soft gate)

Set, change, or remove a passcode via `POST /api/drops/<slug>/passcode` with
`{ "passcode": "..." }`. Pass `"passcode": ""` to remove. This is a soft
share gate — not encryption.

## Rate limiting

429 responses carry a `Retry-After` header and a
`details.retry_after_seconds` field. Back off accordingly.

Limits: 60 writes/min, 500 writes/day, 500 drops/account, 200 versions/drop, 2 MB / drop.

## Errors

All errors share `{ "error": { "code, message, details? } }`.
Switch on `error.code`. Common codes: `unauthorized`, `invalid_token`,
`rate_limited`, `daily_quota_exceeded`, `quota_exceeded`,
`html_too_large`, `html_required`, `title_required`, `forbidden`,
`not_found`, `version_not_found`, `metadata_only_on_patch`,
`last_version_cannot_be_deleted`, `token_required`.

That's the whole API. Build something good.
