# artifact-bin Upload a self-contained HTML artifact, get back a **public link** to share with your user. ## Auth — get a token (three ways, in order of preference) Every `/api/artifacts` call needs a bearer token: ``` Authorization: Bearer mx_... ``` 1. **Saved config** — check `~/.config/artifact-bin/config.json` for `{ "url", "token" }` (written by `artifactbin login`). If it exists, use it: publishes are attached to your user's account automatically. 2. **Your user gave you one** — use it. 3. **No token? Mint an anonymous one** — zero setup: ``` POST https://artifactbin.dev/api/tokens/anonymous → 201 { "id", "token": "mx_..." } ``` Save it for the session and reuse it for every call. Anonymous artifacts work fully but belong to nobody — **tell your user**: *"to keep these under your account, log in at https://artifactbin.dev and claim token `mx_...`"* (they paste it in the Claim box on the dashboard). Claiming attaches everything the token already published, past and future. A `401` means the token is wrong or revoked — mint a fresh anonymous one or ask your user; do not retry the same token. ## Endpoints ### Create an artifact ``` POST https://artifactbin.dev/api/artifacts { "title": "Optional title", "description": "Optional", "html": "..." } → 201 { "id": "art_...", "slug": "...", "url": "https://artifactbin.dev/a/", "version": 1 } ``` `html` here is one of six content fields — `markup | markdown | html | dataset | viz | image`; every endpoint takes exactly ONE of them (see **Content tiers** below). Give `url` to your user — that's the deliverable. Example: ```bash jq -n --rawfile html artifact.html '{"title":"My page","html":$html}' \ | curl -sS -X POST https://artifactbin.dev/api/artifacts \ -H "Authorization: Bearer $ARTIFACT_TOKEN" -H 'Content-Type: application/json' \ --data-binary @- ``` ### Update an artifact (the link never changes) ``` PUT https://artifactbin.dev/api/artifacts/ { "html": "...", "title": "optional new title" } → 200 { "id", "slug", "url", "version": } ``` Full replacement — send the complete new content, not a diff. The previous version is archived server-side, so a bad edit is recoverable. Omitted `title`/`description` keep their current values. Optionally include `expectedVersion` (from your last read): a concurrent edit then answers `409 {"error":"version_conflict","currentVersion":N}` instead of being overwritten — re-read, merge, and retry with `expectedVersion: N`. ### Edit part of a document (preferred for `markup`) ``` POST https://artifactbin.dev/api/artifacts//edits { "edit_id": "", "old_string": "exact text to replace", "new_string": "replacement" } → 200 { "id", "version", "edit_id": "", "markup", ... } ``` Like editing a file: `old_string` must appear EXACTLY ONCE in the version named by `edit_id`. Prefer this over PUT for targeted changes — it is smaller, and a HUMAN MAY BE EDITING THE SAME PAGE LIVE while you work. `edit_id` is an opaque random string returned by every create/read/edit. Never invent one: it is how the server knows which version you actually read. Concurrency is per NODE, not per document, so most edits just apply: | Result | Meaning | What to do | |---|---|---| | `200` | Applied — even if someone else edited a DIFFERENT part meanwhile | Use the returned `edit_id` for your next edit | | `409 doc_changed` | Someone changed the SAME part you were changing | The response carries the current `edit_id` + `source` — re-anchor on those and retry | | `409 stale_edit_id` | That `edit_id` is unknown (too old, or never read) | `GET` the artifact and start from its `edit_id` | | `400 bad_diff` | `old_string` matched zero times or more than once | Re-read and pick a longer, unique anchor | You may also set `title`, `theme`, or `colorMode` in the same request (with or without a text change). Those are document-level and never conflict. ### Read one back (before editing) ``` GET https://artifactbin.dev/api/artifacts/ → 200 { "id", "slug", "url", "title", "description", "format", "html", "version", ... } ``` ### List your artifacts ``` GET https://artifactbin.dev/api/artifacts → 200 { "artifacts": [ { "id", "slug", "url", "title", "version", "updated_at", ... } ] } ``` ### Version history & revert (undo a bad edit) Every `PUT` archives the previous state. To roll back: ``` GET https://artifactbin.dev/api/artifacts//versions → 200 { "versions": [ { "version", "title", "created_at" } ] } POST https://artifactbin.dev/api/artifacts//revert { "version": 1 } → 200 { "id", "slug", "url", "version": } ``` A revert creates a NEW version (the pre-revert state is archived too), so reverts are themselves undoable and the link never changes. ### Delete an artifact ``` DELETE https://artifactbin.dev/api/artifacts/ → 200 { "ok": true } ``` Permanent: the public link dies and version history is erased. Confirm with your user before deleting anything they shared. ### Screenshot / export as an image (no auth, curlable) ``` GET https://artifactbin.dev/a//export → image/png of the fully rendered page GET https://artifactbin.dev/a//export?format=jpg → image/jpeg ``` Rendered on demand in a server-side headless browser — full page at 1200px wide, repeat fetches cached until the artifact changes, nothing stored. Use it to eyeball your own output or hand your user a static image: ```bash curl -sS -o report.png "https://artifactbin.dev/a//export" ``` Share pages also carry `og:image` pointing at this URL, so links pasted into Slack and the like unfurl with a live preview. A `503 render_unavailable` means this deployment has no headless browser installed — the HTML link still works. ## Errors | Status | Meaning | What to do | |---|---|---| | 400 | `invalid_json` / `one_of_markdown_html_markup` / `invalid_jsx` / `invalid_refs` / `unknown_theme` | Fix the request body — `details` names each problem with its span | | 401 | `unauthorized` | Token wrong/revoked — ask your user, don't retry | | 403 | `quota_exceeded` | This token is at its artifact cap — delete something or use another token | | 404 | `not_found` | No artifact with that id belongs to your token | | 409 | `version_conflict` | Your `expectedVersion` is stale — re-read, merge, retry with `currentVersion` | | 409 | `doc_changed` | Someone edited the SAME node — re-anchor on the returned `edit_id`/`source` and retry | | 409 | `stale_edit_id` | That `edit_id` is unknown — `GET` the artifact and use its `edit_id` | | 400 | `bad_diff` | `old_string` matched zero times or more than once — pick a unique anchor | | 409 | `has_dependents` | Other documents reference this artifact — re-send DELETE with `?force=true` to break them knowingly | | 413 | `too_large` | Shrink the content (max 2,000,000 bytes) | | 429 | `rate_limited` | Back off and retry after a minute | ## MCP server (same API, tool-shaped) `https://artifactbin.dev/mcp` — a Streamable HTTP MCP server speaking this exact API (`create_artifact`, `update_artifact`, `edit_artifact`, `get_artifact`, `list_artifacts`, `list_versions`, `get_version`, `revert_artifact`, `delete_artifact`). Auth is the same bearer token via the `Authorization` header: ``` claude mcp add --transport http artifact-bin https://artifactbin.dev/mcp \ --header "Authorization: Bearer mx_..." ``` ## Content tiers — pick ONE content field per request 1. **`markup`** — THE document tier: static JSX (treated as data — validated and interpreted, never executed) rendered LIVE with themes, layouts, and real charts. Use it for anything worth designing (slide decks, dashboards, scrollytelling, reports, data stories, etc.): ``` POST https://artifactbin.dev/api/artifacts { "markup": "

Q3

...
", "theme": "nocturne", "template": "editorial", "colorMode": "light" } ``` - Vocabulary: plain HTML tags + ~60 kit components (Card, Tabs, Badge, SlideDeck/Slide, Grid/GridItem, …) + live data embeds (`` charts/tables, ``, `` filters) — **read https://artifactbin.dev/docs/markup for the full reference before authoring.** - Style with Tailwind classes via `className` (no `