# ATTIM API and static hosting documentation

ATTIM accepts a manifest of static files, returns presigned upload targets, and serves a finalized version from a slug URL such as `https://<slug>.attim.link/`.

ATTIM base URL: `https://attim.link`

Human docs: `https://attim.link/docs`
Agent skill: `https://attim.link/skill.md`
LLM-readable docs: `https://attim.link/llms.txt`

## What ATTIM is

ATTIM is a static hosting service for generated artifacts, reports, demos, dashboards, microsites, and browser-only applications. A publish creates a site slug, a pending version, and one upload target per file. Finalizing promotes that version to the public slug URL.

Use ATTIM for static files. ATTIM does not run backend code, server-side rendering, scheduled jobs, databases, or long-running processes for hosted sites.

## Service boundaries

- Every hosted site requires a root `index.html`.
- Hosted files are public when a live slug URL is known.
- Anonymous sites are controlled by the one-time `claimToken` returned at create time.
- Claimed sites are controlled by the owner browser session or the account API bearer token.
- Site metadata contains title and description fields.
- Claimed sites can use public variables with placeholders such as `{{ vars.PRODUCT_NAME }}`.
- ATTIM does not provide per-site secrets, backend environment variables, custom domains, or password protection for hosted slug sites.

## Publish flow

The publish flow separates file registration from file upload. Create or update returns upload targets; finalize verifies storage objects and promotes the version.

### 1. Create an anonymous publish

Call `POST https://attim.link/api/publish` with the complete file manifest.

```json
{
  "files": [
    {
      "path": "index.html",
      "size": 1234,
      "contentType": "text/html; charset=utf-8"
    },
    {
      "path": "assets/app.js",
      "size": 2048,
      "contentType": "application/javascript",
      "hash": "optional-storage-etag-compatible-hash"
    }
  ],
  "ttlSeconds": 86400
}
```

### 2. Store the create response fields

Persist `slug`, `siteUrl`, `upload.versionId`, and `claimToken` before uploading. The `claimToken` is returned only by the create response.

```json
{
  "slug": "quiet-river-7328",
  "siteUrl": "https://quiet-river-7328.attim.link/",
  "upload": {
    "versionId": "1",
    "uploads": [
      {
        "path": "index.html",
        "method": "PUT",
        "url": "https://...",
        "headers": {
          "content-type": "text/html"
        }
      }
    ],
    "finalizeUrl": "/api/publish/quiet-river-7328/finalize",
    "expiresInSeconds": 3600
  },
  "claimToken": "returned-on-create",
  "expiresAt": "2026-05-09T00:00:00.000Z",
  "anonymous": true,
  "ownershipStatus": "anonymous"
}
```

### 3. Upload every file

Use each upload target exactly as returned. Preserve the returned HTTP method, URL, and headers.

### 4. Finalize the version

Call `POST /api/publish/:slug/finalize` with the returned version id. Anonymous sites must also provide the saved claim token.

```json
{
  "versionId": "1",
  "claimToken": "returned-on-create"
}
```

### 5. Update the same slug

Use `PUT /api/publish/:slug` to create a new pending version for an existing site. Upload the returned files and finalize the new version to keep the same slug URL.

## Agent handoff

A publish handoff is complete only when the user receives the fields needed to use and control the site after the agent run.

- Anonymous publish: return `slug`, `siteUrl`, finalize result, and `claimToken`.
- Claimed-site mutation: return `slug`, `siteUrl`, finalize or mutation result, and the auth context used.
- If the response contains `claimToken`, surface it explicitly instead of only summarizing the response.
- If tool output may truncate a raw API response, print the required handoff fields separately.

## Authentication

ATTIM uses three authorization mechanisms, depending on the resource being mutated.

- Anonymous site control uses the saved `claimToken`.
- Browser dashboard control uses the `attim_session` cookie and `X-ATTIM-CSRF` on non-GET mutations.
- Claimed-site API control uses `Authorization: Bearer <token>` with an account API token.

```http
Authorization: Bearer attim_uat_...
X-ATTIM-CSRF: <csrfToken-from-/api/auth/me>
X-ATTIM-Claim-Token: <anonymous-claim-token>
```

Browser-session mutations require `X-ATTIM-CSRF`. Retrieve it from `GET /api/auth/me`. Bearer-token requests do not require CSRF.

Anonymous update, finalize, and delete accept the claim token as `X-ATTIM-Claim-Token` or as `claimToken` in the JSON body when the endpoint body supports it.

## Claim flow

Claiming attaches an anonymous site to an authenticated user account and removes anonymous expiry from that site.

### Dashboard claim

Use `POST /api/site-claims` from an authenticated browser session with `X-ATTIM-CSRF`.

```json
{
  "slug": "quiet-river-7328",
  "claimToken": "returned-on-create"
}
```

### Email-confirmed claim

Use `POST /api/publish/:slug/request-claim-link` with the saved claim token and owner email. The confirmation link is inspected by `GET /api/claim-links/inspect` and consumed by `POST /api/claim-links/consume`.

```json
{
  "claimToken": "returned-on-create",
  "email": "owner@example.com"
}
```

`POST /api/publish/:slug/claim` is a compatibility route for authenticated browser-session clients. Responses include `Deprecation: true`.

## Owned sites

Use `GET /api/publishes` with an authenticated browser session or account API bearer token to list sites owned by the authenticated user.

- `slug` and `siteUrl`
- `status`, `anonymous`, and `ownershipStatus`
- `metadata.title` and `metadata.description`
- `currentVersionId`, `versionCount`, `currentFileCount`, and `publicVariableCount`
- `createdAt`, `updatedAt`, and `expiresAt`

Owned site metadata is updated with `PATCH /api/publish/:slug/metadata`. Owned sites are deleted with `DELETE /api/publish/:slug`.

## Public variables

Public variables let owners customize claimed static sites without editing uploaded files. Add placeholders to text files, claim the site, then set values from `/variables` or the owner API.

```html
<h1>{{ vars.PRODUCT_NAME }}</h1>
<a href="{{ vars.CTA_URL }}">{{ vars.CTA_TEXT }}</a>
```

Variable names are uppercase letters, numbers, and underscores. Missing variables are left unchanged. Values are inserted literally into served files.

Public variables may be visible in page source, JavaScript, stylesheets, network responses, or browser devtools. Use them for names, links, colors, analytics IDs, browser-safe publishable keys, and public config. Do not store passwords, private API keys, or secret tokens.

```text
GET    /api/publish/:slug/variables
PUT    /api/publish/:slug/variables/:name
DELETE /api/publish/:slug/variables/:name
```

## Account API token

An account API token authenticates claimed-site API routes with `Authorization: Bearer <token>`. The token belongs to the signed-in account, not to a single site.

- Only one active account API token exists per account.
- Create and rotate responses include the raw token once.
- Rotation revokes the previous token before issuing the replacement.
- Revocation removes bearer access until a new token is created.
- Token management endpoints require a browser session; they are not managed by bearer token.

## Endpoint reference

```text
Publish and ownership
POST   /api/publish
GET    /api/publish/:slug
PUT    /api/publish/:slug
POST   /api/publish/:slug/finalize
PATCH  /api/publish/:slug/metadata
DELETE /api/publish/:slug
GET    /api/publishes
POST   /api/publish/:slug/request-claim-link
POST   /api/publish/:slug/claim
POST   /api/site-claims

Public variables
GET    /api/publish/:slug/variables
PUT    /api/publish/:slug/variables/:name
DELETE /api/publish/:slug/variables/:name

Browser and API auth
POST   /api/auth/request-code
POST   /api/auth/verify-code
GET    /api/auth/me
POST   /api/auth/logout
POST   /api/auth/magic-link/request
GET    /api/auth/magic-link
POST   /api/auth/magic-link/consume
GET    /api/claim-links/inspect
POST   /api/claim-links/consume

Account access
GET    /api/access/token
POST   /api/access/token
POST   /api/access/token/rotate
DELETE /api/access/token
POST   /api/support-requests

Serving and operations
GET    /_site/:slug
GET    /_site/:slug/*
GET    /health
GET    /health?include=r2
GET    /health?include=full
GET    /skill.md
GET    /llms.txt
```

## Publish contracts

### POST /api/publish

Creates an anonymous site and pending version. Authentication is not required.

- Body requires `files`.
- `ttlSeconds` is optional and controls anonymous expiry.
- Response includes `claimToken`, `siteUrl`, and upload targets.

### GET /api/publish/:slug

Returns public site metadata and live-version availability. Authentication is not required.

```json
{
  "slug": "quiet-river-7328",
  "siteUrl": "https://quiet-river-7328.attim.link/",
  "status": "active",
  "anonymous": true,
  "ownershipStatus": "anonymous",
  "expiresAt": "2026-05-09T00:00:00.000Z",
  "metadata": {
    "title": null,
    "description": null
  },
  "live": {
    "available": true,
    "finalizedAt": "2026-05-08T00:00:00.000Z"
  }
}
```

### PUT /api/publish/:slug

Creates a pending version for an existing site. Anonymous sites require `claimToken`. Claimed sites require owner authentication.

```json
{
  "files": [
    {
      "path": "index.html",
      "size": 1234,
      "contentType": "text/html"
    }
  ],
  "claimToken": "required-for-anonymous-sites"
}
```

### POST /api/publish/:slug/finalize

Promotes a pending version after storage verification. Missing uploads, size mismatches, and hash mismatches reject finalization.

```json
{
  "success": true,
  "slug": "quiet-river-7328",
  "siteUrl": "https://quiet-river-7328.attim.link/",
  "previousVersionId": null,
  "currentVersionId": "1",
  "verifiedFiles": 2
}
```

### PATCH /api/publish/:slug/metadata

Updates claimed-site metadata. Requires owner authentication. Browser-session requests require `X-ATTIM-CSRF`.

```json
{
  "title": "My Site",
  "description": "Static report published with ATTIM."
}
```

### GET /api/publish/:slug/variables

Lists public variables for a claimed site. Requires owner authentication by browser session or account API bearer token.

```json
{
  "success": true,
  "slug": "quiet-river-7328",
  "count": 1,
  "variables": [
    {
      "name": "PRODUCT_NAME",
      "value": "Acme Analytics",
      "placeholder": "{{ vars.PRODUCT_NAME }}",
      "createdAt": "2026-05-14T00:00:00.000Z",
      "updatedAt": "2026-05-14T00:00:00.000Z"
    }
  ]
}
```

### PUT /api/publish/:slug/variables/:name

Creates or updates a public variable. Browser-session requests require `X-ATTIM-CSRF`.

```json
{
  "value": "Acme Analytics"
}
```

### DELETE /api/publish/:slug/variables/:name

Deletes a public variable. Browser-session requests require `X-ATTIM-CSRF`.

### DELETE /api/publish/:slug

Deletes a site and removes its live content. Anonymous sites require the saved claim token. Claimed sites require owner authentication.

## Auth contracts

### Auth-code API

`POST /api/auth/request-code` sends a code to an email address. `POST /api/auth/verify-code` validates the code and returns a session token in JSON.

```http
POST /api/auth/request-code
{
  "email": "owner@example.com"
}

POST /api/auth/verify-code
{
  "email": "owner@example.com",
  "code": "123456"
}
```

### Browser magic link

`POST /api/auth/magic-link/request` sends a sign-in link. `GET /api/auth/magic-link?token=...` inspects it. `POST /api/auth/magic-link/consume` creates the browser session cookie.

```json
{
  "token": "magic-link-token"
}
```

### Session state and logout

`GET /api/auth/me` returns authentication state and the CSRF token for browser-session mutations. `POST /api/auth/logout` revokes the browser session and requires `X-ATTIM-CSRF`.

```json
{
  "authenticated": true,
  "csrfToken": "csrf-token",
  "sessionId": "1",
  "expiresAt": "2026-06-07T00:00:00.000Z",
  "user": {
    "id": "1",
    "email": "owner@example.com"
  }
}
```

## Dashboard contracts

### POST /api/site-claims

Claims an anonymous site into the signed-in browser account. Requires browser session and `X-ATTIM-CSRF`.

```json
{
  "slug": "quiet-river-7328",
  "claimToken": "returned-on-create"
}
```

### Account token endpoints

These endpoints require a browser session. Non-GET requests require `X-ATTIM-CSRF`.

```text
GET    /api/access/token
POST   /api/access/token
POST   /api/access/token/rotate
DELETE /api/access/token
```

```json
{
  "success": true,
  "token": "attim_uat_returned-on-create-or-rotate",
  "tokenState": {
    "active": true,
    "token": {
      "id": "1",
      "createdAt": "2026-05-08T00:00:00.000Z",
      "lastUsedAt": null
    }
  }
}
```

### POST /api/support-requests

Sends a signed-in support request to the configured ATTIM support inbox. Requires browser session and `X-ATTIM-CSRF`.

```json
{
  "subject": "Issue with my site",
  "message": "Tell us what happened.",
  "siteSlug": "quiet-river-7328"
}
```

## Serving behavior

Finalized sites are served through host-header routing on slug subdomains and through internal base-domain routes.

- Public slug URL pattern: `https://<slug>.attim.link/`
- Internal route pattern: `/_site/:slug` and `/_site/:slug/*`
- `/` resolves to `index.html`.
- Paths ending in `/` resolve to that directory's `index.html`.
- HTML responses use `Cache-Control: public, max-age=60, stale-while-revalidate=300`.
- Non-HTML file responses use `Cache-Control: public, max-age=31536000, immutable`.
- Text, JSON, JavaScript, XML, and SVG files containing public variable placeholders use the short HTML cache behavior.
- Unknown slug/file responses use a short-lived 404 cache.

## Limits and validation

- Maximum files per publish: `200`.
- Maximum total publish size: `20 MiB`.
- `ttlSeconds` minimum: `60`.
- `ttlSeconds` maximum: `604800` seconds.
- `ttlSeconds` default: `86400` seconds.
- Auth codes, magic links, and claim links expire after `600` seconds by default.
- Browser sessions expire after `30` days by default.
- Public variables: `100` per site, names up to `64` characters, values up to `10000` characters.

### File validation

- Root `index.html` is required.
- Paths must be relative and traversal-safe.
- Duplicate normalized file paths are rejected.
- Content types are normalized by removing parameters after `;`.
- Supported content type families: `text/*`, `image/*`, `font/*`, `audio/*`, and `video/*`.
- Supported application content types include JSON, JavaScript, WASM, XML, PDF, octet-stream, manifest JSON, JSON-LD, and Microsoft font objects.

## Errors and rate limits

Error responses use JSON and usually include an `error` string plus a machine-readable `code`.

```json
{
  "error": "Rate limit exceeded for publish",
  "code": "ATTIM_RATE_LIMITED",
  "action": "publish",
  "retryAfterSeconds": 12
}
```

- Publish rate limit: `20` requests per minute per IP.
- Finalize rate limit: `40` requests per minute per IP.
- Public variable mutation rate limit: `60` requests per minute per IP.
- Auth request rate limit: `5` requests per minute per IP/email pair.
- Auth verify rate limit: `10` requests per minute per IP/email pair.
- Rate-limited responses include `Retry-After` and `retryAfterSeconds`.

### Common error codes

```text
ATTIM_RATE_LIMITED
ATTIM_AUTH_REQUIRED
ATTIM_CSRF_REQUIRED
ATTIM_CLAIM_TOKEN_REQUIRED
ATTIM_INVALID_CLAIM_TOKEN
ATTIM_CLAIM_TOKEN_EXPIRED
ATTIM_ALREADY_CLAIMED
ATTIM_OWNER_REQUIRED
ATTIM_OWNER_AUTH_REQUIRED
ATTIM_VARIABLE_NAME_INVALID
ATTIM_VARIABLE_VALUE_TOO_LARGE
ATTIM_VARIABLE_LIMIT_EXCEEDED
ATTIM_VARIABLE_NOT_FOUND
ATTIM_MISSING_INDEX_HTML
ATTIM_DUPLICATE_PATH
ATTIM_INVALID_PATH
ATTIM_UNSUPPORTED_CONTENT_TYPE
ATTIM_MISSING_UPLOADS
ATTIM_SITE_DELETED
ATTIM_MAGIC_LINK_INVALID
ATTIM_MAGIC_LINK_USED
ATTIM_MAGIC_LINK_EXPIRED
ATTIM_CLAIM_LINK_INVALID
ATTIM_CLAIM_LINK_USED
ATTIM_CLAIM_LINK_EXPIRED
```

## Related links

- Human docs: https://attim.link/docs
- Agent skill: https://attim.link/skill.md
- FAQ: https://attim.link/faq