# AnyRouter Artifacts: a guide for AI agents

Artifacts are pages hosted on AnyRouter. You publish one file or a small folder and get a stable link at `https://anyrouter.dev/a/<id>`. Publishing again makes a new numbered version at the same link. Use them to share a report, a dashboard, a demo, a diagram or a note you just built.

- Gallery: https://anyrouter.dev/artifacts
- API reference: https://docs.anyrouter.dev/api-reference/artifacts
- Your artifacts: https://anyrouter.dev/dashboard/artifacts

There are three ways to publish. Use the REST API for any size and for folders, the MCP tools when you work inside an MCP client, and the `anyr` CLI from a terminal.

## Auth

All calls use `Authorization: Bearer $ANYROUTER_API_KEY`.

- An inference key (`sk-ar-v1-...`) works for every artifact call, unless its endpoint allow-list leaves out Artifacts.
- A management key (`ak_...`) needs `read:artifacts` to list and read, and `write:artifacts` to publish, change or delete.
- Get a key at https://anyrouter.dev/dashboard/keys. Keep it in `ANYROUTER_API_KEY`, never in source.

```bash
export ANYROUTER_API_KEY="sk-ar-v1-your-key"
API=https://anyrouter.dev/api/v1
```

## Visibility

Every artifact is `private` unless you choose otherwise.

| Value | Who can open it | Listed in the gallery | Indexed by search engines |
|---|---|---|---|
| `private` | Only the owner | No | No |
| `unlisted` | Anyone with the link | No | No |
| `public` | Anyone with the link | Yes | Yes |

An account can make artifacts `public` 24 hours after sign-up, and up to 5 a day. Use `unlisted` to share a link before then. Do not make an artifact `public` unless the user asked for it.

## Kinds

The entry is the file shown at the link. Its extension decides how it renders.

| Entry extension | Kind | Viewers see |
|---|---|---|
| `.html` | Page | The page itself, in a sandboxed frame. |
| `.md` | Markdown | Formatted text. Raw HTML in the Markdown is dropped. |
| `.jsx`, `.tsx` | React | One component with a default export, rendered in the viewer's browser. Tailwind classes work. |
| `.svg` | Image | The SVG as an image. Scripts in it do not run. |
| `.mmd`, `.mermaid` | Diagram | A Mermaid diagram. |
| `.py` `.ts` `.sh` `.sql` `.go` `.rs` `.yaml` and other source files | Code | The file with syntax highlighting. |

`.js`, `.mjs`, `.css`, `.json`, `.csv` and `.txt` are never the entry. They are files a page loads. A folder with `index.html` and `app.js` opens `index.html`.

How the entry is picked:

1. `entry_path` always wins.
2. Otherwise the only `.html` or `.md` file opens. With several, `index.html` wins, then `index.md`.
3. A folder with no `.html` or `.md` file opens its only React, SVG, Mermaid or source file. A single React component wins over SVG and source files beside it. Otherwise `index.tsx`, `index.jsx`, `index.svg` and `index.mmd` win in that order.
4. Anything else returns `400 entry_ambiguous` or `400 no_entry`. Set `entry_path` to fix it.

A React entry is one file up to 256 KB. It may import only `react`, `lucide-react`, `recharts`, `d3`, `lodash`, `papaparse`, `mathjs`, `three`, `chart.js`, `xlsx`, the shadcn-style `@/components/ui/{alert,badge,button,card,input,label,progress,separator,switch,tabs,textarea}` and `@/lib/utils`. Any other import returns `422 unsupported_import`. A missing default export returns `422 invalid_component`.

## Publish one file (REST)

```bash
curl -sS $API/artifacts \
  -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Hello","visibility":"unlisted","files":{"index.html":{"content":"<h1>Hi</h1>"}}}'
```

Response, `201`:

```json
{ "id": "k3Vx9TqLm2Ab7Zcd", "version": 1, "url": "https://anyrouter.dev/a/k3Vx9TqLm2Ab7Zcd", "visibility": "unlisted" }
```

Request fields:

| Field | Notes |
|---|---|
| `files` | Map of path to `{ "content": "...", "encoding": "utf8" \| "base64" }`. `encoding` defaults to `utf8`. Use `base64` for images and fonts. |
| `title` | Up to 120 characters. Defaults to the page's `<title>` or first heading. |
| `description` | Up to 300 characters. |
| `visibility` | `private` (default), `unlisted` or `public`. |
| `entry_path` | The file to show at the link. |
| `label` | A note for this version, up to 60 characters. |
| `show_author` | `true` shows your profile name on the page. Defaults to `false`. |

An inline publish carries up to 5 MB in total. Use the folder flow for more.

A React component:

```bash
curl -sS $API/artifacts \
  -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Counter","files":{"App.tsx":{"content":"import { useState } from \"react\"\nexport default function App() {\n  const [n, setN] = useState(0)\n  return <button className=\"p-4\" onClick={() => setN(n + 1)}>Clicked {n}</button>\n}"}}}'
```

## Publish a folder (REST)

Send a manifest with each file's path, size and SHA-256. The server answers with the files it still needs. Upload each one, then commit.

```bash
cd ./report

# 1. Build the manifest.
MANIFEST=$(find . -type f ! -path '*/.*' | sed 's|^\./||' | sort | while read -r f; do
  jq -n --arg path "$f" --argjson size "$(wc -c < "$f")" \
    --arg sha256 "$(sha256sum "$f" | cut -d' ' -f1)" \
    '{path: $path, size: $size, sha256: $sha256}'
done | jq -s .)

# 2. Open the upload. Response 202: { upload_id, expires_at, missing: [{ path, sha256, size, put_url }] }
OPEN=$(curl -sS $API/artifacts \
  -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --argjson manifest "$MANIFEST" '{title: "Report", manifest: $manifest}')")
UPLOAD_ID=$(echo "$OPEN" | jq -r .upload_id)

# 3. Upload each missing file to its put_url.
echo "$OPEN" | jq -r '.missing[] | [.path, .put_url] | @tsv' | while IFS=$'\t' read -r path url; do
  curl -sS -X PUT "$url" -H "Authorization: Bearer $ANYROUTER_API_KEY" --data-binary @"$path"
done

# 4. Commit. Response 201, the same shape as an inline publish.
curl -sS -X POST $API/artifacts/uploads/$UPLOAD_ID/commit \
  -H "Authorization: Bearer $ANYROUTER_API_KEY"
```

On macOS use `shasum -a 256` in place of `sha256sum`. The upload stays open for one hour. Committing twice is safe and returns the same version. The server checks every file's SHA-256 and size.

## Publish a new version (REST)

Send the version you built on as `base_version`.

```bash
curl -sS $API/artifacts/$ID/versions \
  -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"base_version":1,"label":"fix chart","files":{"index.html":{"content":"<h1>Hi again</h1>"}}}'
```

- Files you send are added or replaced. Files you leave out are kept. A path set to `null` is removed.
- The folder flow works here too. Send `manifest` to this URL in place of `files`, then upload and commit as above. Add `delete` (a list of paths) to remove files, or `keep_unlisted: false` to make the manifest the whole new file set.
- If someone published first, you get `409 version_conflict` with `current_version`. Read that version, merge your change, and retry with the new `base_version`.
- `"force": true` in place of `base_version` publishes on top of whatever is current. Use it only when the user wants to overwrite.

## Change visibility or details (REST)

```bash
curl -sS -X PATCH $API/artifacts/$ID \
  -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"visibility":"unlisted"}'
```

PATCH accepts `title`, `description`, `icon`, `visibility` and `show_author`.

## List and read (REST)

| Request | Returns |
|---|---|
| `GET /api/v1/artifacts` | Your live artifacts, newest first: `{ "data": [...], "next_cursor": ... }`. Query `limit` (1 to 100, default 25) and `cursor`. |
| `GET /api/v1/artifacts/{id}` | Metadata, the current version and its `files` list. `?version=n` reads an older version (owner only). |
| `GET /api/v1/artifacts/{id}/versions` | The version history. |
| `GET /api/v1/artifacts/{id}/files/{path}` | The source of one file, as a download (owner only). |
| `GET /api/v1/artifacts/{id}/stats?days=30` | Daily views and visitors (owner only). |
| `GET /api/v1/artifacts/gallery` | Public artifacts, newest first. No auth. Add `sort=popular` for the most viewed. |

```bash
curl -sS "$API/artifacts?limit=10" -H "Authorization: Bearer $ANYROUTER_API_KEY"
```

## Download your artifact's files (REST)

Read the file list, then fetch each file. This works for artifacts you own.

```bash
mkdir -p "$ID" && cd "$ID"
curl -sS $API/artifacts/$ID -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  | jq -r '.files[].path' | while read -r path; do
    mkdir -p "$(dirname "$path")"
    curl -sS "$API/artifacts/$ID/files/$path" \
      -H "Authorization: Bearer $ANYROUTER_API_KEY" -o "$path"
  done
```

To copy the folder into a new artifact, publish the downloaded folder with the folder flow.

## Delete (REST)

```bash
curl -sS -X DELETE $API/artifacts/$ID -H "Authorization: Bearer $ANYROUTER_API_KEY"
```

Response: `{ "id": "...", "deleted": true }`. The link stops working for everyone else at once. Delete only when the user asks.

## MCP tools

Connect `https://anyrouter.dev/api/v1/mcp?tools=artifacts` with `Authorization: Bearer $ANYROUTER_API_KEY` to get only the artifact tools. The full MCP server at `https://anyrouter.dev/api/v1/mcp` includes them too.

| Tool | Input | Does |
|---|---|---|
| `publish_artifact` | `files` (path to text content, `null` removes a path), `title`, `visibility`, `entry_path`, `label`, and `id` plus `base_version` for a new version | Publishes and returns the link. |
| `list_artifacts` | `limit` (default 20) | Lists your live artifacts. |
| `get_artifact` | `id` | Metadata, current version and file list. |
| `set_artifact_visibility` | `id`, `visibility` | Sets `private`, `unlisted` or `public`. |
| `delete_artifact` | `id` | Deletes the artifact. |

`publish_artifact` takes plain text content, not `{ "content": ... }` objects, up to 1 MB in total. For images, fonts or larger folders, use the REST folder flow.

```json
{
  "name": "publish_artifact",
  "arguments": {
    "title": "Weekly report",
    "visibility": "unlisted",
    "files": { "index.html": "<h1>Week 43</h1>", "style.css": "h1 { color: teal }" }
  }
}
```

## The `anyr` CLI

```bash
curl -fsSL https://anyrouter.dev/setup.sh | bash
anyr auth login

anyr artifact publish ./report                       # a file or a folder; prints the link
anyr artifact publish page.html --title "Weekly" --visibility unlisted --json
anyr artifact publish ./report --id k3Vx9TqLm2Ab7Zcd --label "fix chart"   # new version
anyr artifact ls                                     # your artifacts (--limit, --cursor, --all)
anyr artifact visibility k3Vx9TqLm2Ab7Zcd unlisted
anyr artifact visibility k3Vx9TqLm2Ab7Zcd public --yes
anyr artifact rm k3Vx9TqLm2Ab7Zcd --yes
```

- `--visibility` takes `private` (default), `unlisted` or `public`. `--show-author` and `--no-show-author` work on `publish` and `visibility`.
- `--yes` skips the confirmation for `public` and `rm`. `--json` prints the server response.
- A single file is sent as UTF-8 text. Put binary files such as images in a folder and publish the folder.
- A folder publish skips dotfiles and `node_modules`. The CLI allows up to 50 files, 20 MB in total and 5 MB per file.
- With `--id`, the CLI reads the current version and builds on it. A conflict exits with code 2.
- Exit codes: 0 ok, 1 error, 2 usage error or version conflict, 3 not found, 4 auth error.
- The CLI has no download command. Use the REST download recipe above.

## Limits

| Limit | Value |
|---|---|
| One asset file (image, font, script) | 25 MB |
| One entry or source file (`html`, `md`, `svg`, `mmd`, source code) | 5 MB |
| One React or Mermaid entry | 256 KB |
| One inline REST publish | 5 MB |
| One MCP publish | 1 MB of text |
| One publish or version | 200 files, 100 MB |
| Artifacts per account | 100 |
| Stored size per account (current versions) | 200 MB |
| Publishes per hour | 20 |
| Path length | 200 characters |

Allowed file types: `html`, `md`, `css`, `js`, `mjs`, `json`, `txt`, `csv`, `png`, `jpg`, `jpeg`, `gif`, `webp`, `avif`, `svg`, `woff2`, `ico`, `mmd`, `mermaid`, `jsx`, `tsx` and the source extensions above. The server sets the content type from the extension.

## Errors

Errors look like `{ "error": { "code": "...", "message": "..." } }`.

| Status | Codes | What to do |
|---|---|---|
| 400 | `invalid_request`, `invalid_path`, `entry_ambiguous`, `no_entry`, `base_version_required` | Fix the request. Set `entry_path` for an ambiguous folder. |
| 403 | `quota_exceeded`, `account_too_new`, `listing_blocked` | Delete old artifacts, or use `unlisted`. |
| 404 | `not_found` | No such artifact, or it is private to someone else. |
| 409 | `version_conflict` | Read `current_version`, merge, retry. |
| 413 | `inline_too_large`, `file_too_large`, `publish_too_large`, `too_large` | Use the folder flow, or make the file smaller. |
| 415 | `unsupported_media_type` | The file type is not allowed. |
| 422 | `invalid_component`, `unsupported_import`, `invalid_svg` | Fix the entry file. |
| 429 | `rate_limited` | Wait, then retry. |

## Links

- Gallery: https://anyrouter.dev/artifacts
- API reference: https://docs.anyrouter.dev/api-reference/artifacts
- MCP server: https://docs.anyrouter.dev/api-reference/mcp
- CLI: https://docs.anyrouter.dev/cli
- Agent guide for the whole gateway: https://anyrouter.dev/agents.md
- Site overview for LLMs: https://anyrouter.dev/llms.txt
