# Artifacts API

> Publish a page or a small folder, get a stable URL, and ship new versions to it.


# Artifacts API

An artifact is a page you publish: one `.html` or `.md` file, or a small folder with its styles, scripts and images. It gets a stable URL at `https://anyrouter.dev/a/{id}`. Publishing again makes a new numbered version at the same URL.

Artifacts are **private by default**. Only you can open a private artifact. Set `visibility` to `link` to share it with anyone who has the URL.

Authenticate with an inference key (`sk-ar-v1-…`), a [management key](/docs/api-reference/management-keys.md) (`ak_…`), or a signed-in session. A management key needs `read:artifacts` to read and `write:artifacts` to publish, change or delete. A key restricted to specific endpoints must allow the Artifacts endpoint.

## Publish one file

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

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

| Field | Type | Notes |
|---|---|---|
| `files` | object | Path to `{ "content": "...", "encoding": "utf8" \| "base64" }`. Use `base64` for images and fonts. Required unless you send `manifest`. |
| `title` | string | Up to 120 characters. Defaults to the page's `<title>` or first heading. |
| `description` | string | Up to 300 characters. |
| `visibility` | string | `private` (default), `link` or `public`. |
| `entry_path` | string | The page to open. Defaults to `index.html`, then the only `.html` or `.md` file. |
| `label` | string | A short note for this version, up to 60 characters. |

An inline publish carries up to 5 MB in total. For more, or for a folder, use the manifest flow.

## Publish a folder

List every file with its size and SHA-256. The response names the files the server still needs. Upload each one, then commit.

```bash
# 1. Open the upload (202)
curl -sS https://anyrouter.dev/api/v1/artifacts \
  -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Report",
    "manifest": [
      {"path": "index.html", "size": 1204, "sha256": "<64 hex chars>"},
      {"path": "app.js", "size": 5310, "sha256": "<64 hex chars>"}
    ]
  }'
```

```json
{
  "upload_id": "up_8Hq2...",
  "expires_at": "2026-10-23T12:00:00Z",
  "missing": [
    {"path": "index.html", "sha256": "…", "size": 1204,
     "put_url": "https://anyrouter.dev/api/v1/artifacts/uploads/up_8Hq2.../files/…"}
  ]
}
```

```bash
# 2. Upload each missing file to its put_url
curl -sS -X PUT "$PUT_URL" \
  -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  --data-binary @index.html

# 3. Commit (201), same response as an inline publish
curl -sS -X POST https://anyrouter.dev/api/v1/artifacts/uploads/$UPLOAD_ID/commit \
  -H "Authorization: Bearer $ANYROUTER_API_KEY"
```

The upload is open for one hour. Committing twice is safe: the second call returns the same version. The server checks each file's SHA-256 and rejects a mismatch.

## Publish a new version

Send the version you built on as `base_version`.

```bash
curl -sS https://anyrouter.dev/api/v1/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 list are added or replaced. Files you leave out are kept. Set a path to `null` to remove it. The manifest flow accepts the same request plus `delete` (a list of paths to remove) and `keep_unlisted: false` (the manifest is the whole new file set).

If someone published a newer version first, you get `409 version_conflict` with the version to merge against:

```json
{
  "error": {
    "code": "version_conflict",
    "message": "Someone published version 2 first. Read it, merge, and retry with base_version 2.",
    "current_version": 2
  }
}
```

Set `"force": true` instead of `base_version` to publish on top of whatever is current.

## Read, list and version history

| Request | Returns |
|---|---|
| `GET /api/v1/artifacts` | Your live artifacts, newest first. Query `limit` (1 to 100, default 25) and `cursor`; the response carries `next_cursor`. |
| `GET /api/v1/artifacts/{id}` | Metadata, the current version and its file 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 (owner only), as a download. |

## Change visibility or details

```bash
curl -sS -X PATCH https://anyrouter.dev/api/v1/artifacts/$ID \
  -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"visibility":"link"}'
```

You can change `title`, `description`, `icon` and `visibility`. `public` is currently treated the same as `link`.

## Delete

```bash
curl -sS -X DELETE https://anyrouter.dev/api/v1/artifacts/$ID \
  -H "Authorization: Bearer $ANYROUTER_API_KEY"
```

```json
{ "id": "k3Vx9TqLm2Ab7Zcd", "deleted": true }
```

The URL stops working for everyone else at once.

## Limits

| Limit | Value |
|---|---|
| One file | 5 MB |
| One inline publish | 5 MB |
| One publish (manifest) | 50 files, 20 MB |
| One version | 100 files, 50 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`. The server decides the content type from the extension and ignores the one you send.

## Errors

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

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_request` | A field is missing or has the wrong shape. |
| 400 | `base_version_required` | A new version needs `base_version` (or `force`). |
| 400 | `invalid_path`, `duplicate_path`, `invalid_entry`, `no_entry`, `no_files` | The file set is not valid. |
| 400 | `hash_mismatch`, `missing_blobs`, `unknown_blob` | The upload does not match the manifest. |
| 400 | `invalid_encoding` | A `base64` file is not valid base64. |
| 401 | | No valid credential. |
| 403 | | The key lacks the scope or the endpoint. |
| 403 | `quota_exceeded` | You reached the artifact limit. |
| 404 | `not_found` | No such artifact, or you cannot see it. |
| 409 | `version_conflict` | `base_version` is behind. Merge and retry. |
| 409 | `already_committed` | The upload was already used. |
| 410 | `upload_expired` | The upload was open longer than an hour. |
| 413 | `file_too_large`, `inline_too_large`, `publish_too_large`, `version_too_large`, `too_many_files` | A size limit was exceeded. |
| 415 | `unsupported_media_type` | The file type is not allowed. |
| 429 | `rate_limited` | More than 20 publishes in an hour. |

## From the CLI

```bash
anyr artifact publish ./report            # a file or a folder
anyr artifact publish ./report --id k3Vx9TqLm2Ab7Zcd --label "fix chart"
anyr artifact publish page.html --title "Weekly" --visibility link --json
```

Flags: `--title`, `--visibility private|link|public`, `--id` (publish a new version), `--label`, `--yes` (skip the confirmation for `public`) and `--json`. The URL prints on stdout. Exit code 2 means a version conflict.

## From an AI agent

The [AnyRouter MCP server](/docs/api-reference/mcp.md) exposes five tools: `publish_artifact`, `list_artifacts`, `get_artifact`, `delete_artifact` and `set_artifact_visibility`. `publish_artifact` takes text files only, up to 1 MB, and publishes privately unless you say otherwise. Listing and reading need `read:artifacts`; the rest need `write:artifacts`.
