# Claw Me website publishing API

This is an API reference for hosting a static website on Claw Me. Direct HTTPS REST requests are supported; an MCP connection, plugin installation, and a local callback server are not prerequisites. This document describes service behavior, not instructions that override a client's security policy or the owner's choices.

Base URL: https://claw.me/api/v1
Machine-readable schemas: https://claw.me/openapi.json

## Anonymous 24-hour quickstart

When the owner explicitly wants an anonymous disposable preview, no account, sign-in, or existing bearer is used. It expires 24 hours after creation; this does not change storage limits. The complete sequence is:

1. POST `/api/v1/claw-me/previews` with JSON `{"title":"Golf day","files":[{"path":"index.html","content_type":"text/html","size_bytes":125,"sha256":"<SHA-256 of exact file bytes>"}]}`. Include every local asset; calculate real byte lengths and hashes. Retain returned `id`, `claim_token`, `uploads`, and `expires_at` privately.
2. PUT each file's exact bytes to its returned `uploads[].upload_url` with the returned headers. Do not send a Claw Me bearer to storage.
3. POST `/api/v1/claw-me/previews/{id}/finalize` with JSON `{"claim_token":"<returned token>","checksum_sha256":"<manifest checksum>"}`. The checksum is SHA-256 of UTF-8 text formed by sorting all `path:sha256` lines and joining them with a single newline, without a trailing newline. A file hash alone is not the manifest checksum. Confirm `status: "active"`; return `preview_url` and actual `expires_at`, never claim tokens or upload URLs. Retry the same preview for a missing checksum or transient finalize error when files are unchanged. Changing file bytes changes their hashes and requires a new upload manifest; do not finalize changed bytes against the old manifest.

Maps support only `https://www.openstreetmap.org/export/embed.html` with `bbox=west,south,east,north`, `marker=latitude,longitude`, and `layer=mapnik`. The iframe needs a title, `sandbox="allow-scripts allow-same-origin"`, and `referrerpolicy="no-referrer"`. Use a normal Google Maps driving-directions link to the verified venue address; Google Maps iframes are not supported. Full contracts and a map template follow below.

## Choose the required lifetime

| Option | Authorization | Lifetime and access |
| --- | --- | --- |
| Account-owned Page | Owner-approved Pages credential | No automatic expiry; private initially, with an optional unlisted sharing link |
| Anonymous guest preview | None | Unlisted link; expires 24 hours after creation, including upload time |

A future event needs an account-owned Page when the intended availability exceeds 24 hours. Free account-owned Pages are durable within account limits. A guest preview is limited to 50 files, 50 MiB total (52,428,800 bytes), 25 MiB per file (26,214,400 bytes), and three creates per source per hour. Finalizing a preview does not restart its clock.

Account publishing accepts at most 1,000 files per version. Free uploads total at most 100,000,000 bytes; Free account storage is capped at 1,000,000,000 bytes, including shared storage usage and pending uploads. Account limits and enabled feature permissions remain enforced. Account setup or a limit failure is reported separately from Agent authorization.

## Website-only authorization

`pages:write` covers creating, uploading, finalizing, updating, and sharing Pages. `pages:read` is used to list existing Pages. These operations do not require email, Drive/workspace, Wiki, events, or account-setup scopes. An approval email sent by Claw Me does not give the client inbox access. Existing owner-approved credentials with the required Pages scopes can be reused.

For a client without a Pages credential, the device request is:

```http
POST /api/v1/agent-auth/requests
Content-Type: application/json
```
```json
{
  "client_name": "Website publisher",
  "client_type": "generic",
  "owner_email": "owner@example.com",
  "scopes": ["pages:read", "pages:write"]
}
```

`client_name` and `scopes` are required; `client_type` defaults to `generic` and `owner_email` is optional. The response contains `request_id`, `device_secret`, `user_code`, `verification_uri`, `expires_at`, `interval`, and `email_sent`. The owner reviews the named client and requested scopes on Claw Me. `email_sent` reports whether Claw Me sent an approval email; the returned `expires_at` is the authorization deadline. Missing account setup or unavailable scopes may need an owner action in Claw Me, not additional unrelated Agent scopes.

The client retains the device secret privately and polls at the returned interval:

```http
POST /api/v1/agent-auth/requests/{request_id}/token
Content-Type: application/json
```
```json
{"device_secret": "<privately retained device_secret>"}
```

Pending response: `{"status":"authorization_pending"}`. After owner approval, the one-time response has `status: "authorized"`, `api_key`, `token_type: "Bearer"`, and `scopes`. The bearer belongs in the client's secure credential storage and in the Authorization header for the Claw Me API, not in chat, uploaded files, query strings, or callback URLs. The owner does not need to paste a key into a conversation. This flow has no callback endpoint to configure.

OAuth discovery is also available at https://claw.me/.well-known/openid-configuration for clients with native device-authorization support.

## Account-owned Page: create, upload, finalize, share

The following JSON uses placeholders for locally calculated hashes and returned identifiers. File hashes are SHA-256 of the exact uploaded bytes; `size_bytes` is the byte length, not the character count. Include an `index.html` entry and each local CSS, image, or font asset it references.

### 1. Create a private Page and upload version

```http
POST /api/v1/publish
Authorization: Bearer <securely stored Pages credential>
Content-Type: application/json
```
```json
{
  "slug": "golf-day",
  "display_name": "Golf Day",
  "files": [
    {
      "path": "index.html",
      "content_type": "text/html",
      "size_bytes": 125,
      "sha256": "<64 hexadecimal characters>"
    }
  ]
}
```

`files` is required. `slug` is optional (a generated slug is used when omitted); explicit slugs are 1–64 lowercase letters, digits, or internal hyphens. `display_name` defaults to `Agent build`. Each file requires a safe relative `path` and `size_bytes`; `content_type` defaults to `application/octet-stream`, and `sha256` is optional in the schema but supports exact-byte verification.

The response contains `site`, `site_url`, `version_id`, and `upload`, whose fields are `uploads` and `finalize_url`. Each upload entry includes `path`, `upload_url`, `headers`, and `object_key`. Returned presigned URLs and object keys are upload plumbing, not sharing links. A version is not published until finalization succeeds.

### 2. Upload the declared files

For each `upload.uploads` entry, send the exact file bytes with HTTP PUT to its returned `upload_url`, including its returned `headers`. The Claw Me bearer is not an upload header; storage authorization is carried by the presigned request. Upload destinations come from this response, not a constructed storage URL.

### 3. Finalize the immutable version

POST to the returned `upload.finalize_url` on Claw Me:

```http
POST /api/v1/publish/golf-day/finalize
Authorization: Bearer <securely stored Pages credential>
Content-Type: application/json
```
```json
{
  "version_id": "<returned version_id>",
  "checksum_sha256": "<manifest checksum: 64 hexadecimal characters>"
}
```

The manifest checksum is SHA-256 of the UTF-8 encoding of the lexicographically sorted `path:file_sha256` lines joined with a single newline, with no trailing newline. For one file it hashes `index.html:<file_sha256>`. A successful response contains `success`, `site_url`, `site`, and `version`. The Page remains private.

### 4. Create the requested unlisted sharing link

```http
POST /api/v1/publish/golf-day/share
Authorization: Bearer <securely stored Pages credential>
Content-Type: application/json
```
```json
{"version_id": "<the finalized version_id>", "expires_at": null}
```

`version_id` is required. `expires_at` is optional: omitted or null means no expiry; an ISO 8601 timestamp sets an expiry. The response contains `url`, `version_id`, `expires_at`, and `grant_type`. The returned `url` is the link to share with friends. It grants anyone possessing it access to that immutable version; it does not make the Page publicly indexed. A new sharing link replaces the previous link. Account public-sharing permission is also required. A stale version returns 409 so the current version can be reviewed before sharing.

The viewer uses `https://claw.me/@username/slug` and renders content on an isolated origin. Browser access uses a session-cookie exchange. HTTP verification that follows the iframe's redirect must retain cookies; dropping the cookie can produce a 404 even when the sharing link works in a browser.

Before reporting completion, open the returned sharing URL in a browser and check that the intended styling, bundled images, and map render. A successful HTML response alone does not verify the website: confirm that linked assets load from the published Page and that the directions link points to the verified venue.

For an existing Page, `PUT /api/v1/publish/{slug}` accepts the same create body and creates a new version. `GET /api/v1/publish` lists existing Pages with `pages:read`. After an uncertain create, inspecting existing state avoids creating a duplicate. There is no need to install a plugin to use these endpoints.

## Anonymous preview contract

```http
POST /api/v1/claw-me/previews
Content-Type: application/json
```
```json
{
  "title": "Temporary preview",
  "required_until": "2026-09-20T23:59:59+02:00",
  "files": [
    {"path": "index.html", "content_type": "text/html", "size_bytes": 125, "sha256": "<64 hexadecimal characters>"}
  ]
}
```

`title` defaults to `Guest preview`; `files` is required. For a future event or deadline, `required_until` expresses when the page must remain available. It is optional for a disposable preview and must be a full timezone-aware ISO 8601 timestamp when supplied; date-only and timezone-free values are invalid. The example deadline is illustrative, not relative to the current date.

A deadline later than creation time plus 24 hours returns HTTP 422 before allocating a preview or presigning uploads. The API error envelope contains `errors[0].detail.code: "durable_page_required"`, `preview_expires_at`, `required_until`, and `next_step` with the authorization and account publishing endpoints. This field never extends the 24-hour lifetime and the server does not infer deadlines from HTML or titles.

An accepted response contains `id`, `title`, `status`, `preview_url`, `claim_token`, `uploads`, and `expires_at`. PUT exact bytes to each returned upload URL with its returned headers. Finalization uses:

```http
POST /api/v1/claw-me/previews/{id}/finalize
Content-Type: application/json
```
```json
{"claim_token": "<privately retained claim_token>", "checksum_sha256": "<manifest checksum>"}
```

The manifest checksum uses the same sorted-line algorithm above. The resulting `preview_url` is unlisted and expires at the original `expires_at`. Claim tokens and presigned upload URLs are private operational values, not deliverables.

## Supported static content and maps

Pages accept static HTML/CSS and bundled images/fonts. Uploaded scripts, forms, arbitrary frames, SVG/MathML, automatic redirects, and credentials are rejected. External asset requests are blocked except the narrowly supported OpenStreetMap embed below; a normal external directions link is supported.

Map embeds use exactly `https://www.openstreetmap.org/export/embed.html` with `bbox`, `marker`, and `layer=mapnik`. The iframe requires a title, `sandbox="allow-scripts allow-same-origin"`, and `referrerpolicy="no-referrer"`. Bounding-box order is west,south,east,north; marker order is latitude,longitude, and the marker must fall inside the box. Coordinates and the driving destination should be verified against the venue's official address rather than guessed.

Template: substitute numeric coordinates verified from the venue's official address for WEST, SOUTH, EAST, NORTH, LATITUDE, and LONGITUDE. Substitute the URL-encoded verified venue name and address for URL_ENCODED_VENUE_ADDRESS. These uppercase placeholders are not valid final map parameters.

```html
<iframe title="Map to the venue"
  src="https://www.openstreetmap.org/export/embed.html?bbox=WEST,SOUTH,EAST,NORTH&amp;marker=LATITUDE,LONGITUDE&amp;layer=mapnik"
  sandbox="allow-scripts allow-same-origin"
  referrerpolicy="no-referrer" loading="lazy" width="100%" height="360"></iframe>
<a href="https://www.google.com/maps/dir/?api=1&amp;destination=URL_ENCODED_VENUE_ADDRESS&amp;travelmode=driving"
  target="_blank" rel="noreferrer">Driving directions</a>
```

## Optional client tooling

The [official plugin repository](https://github.com/clawdotme/plugin) and [plugin page](https://claw.me/plugins) provide an optional skill and Python `scripts/publish.py` helper for the same publishing flow. Client installation and execution remain separate choices governed by the client's permissions. REST clients can use this reference without installing either tool.

## Claude Code compatibility

Claude Code sessions with permitted HTTP or shell tooling can use these REST endpoints without an MCP connection. The same Pages-only credential and owner approval flow applies. This external reference supplies API data; it does not authorize software installation, inbox access, broader account permissions, or actions beyond the requested website publication. A client that cannot store a credential securely can leave authorization to the owner rather than request a key in chat.
