# 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 `