# OpenArtifacts Publish static HTML, get a URL. Built to be driven by an agent: no email, no browser step, no dashboard visit required. API base: https://openartifacts.sh Published pages: https://.openartifactsusercontent.com/ ## The whole flow ```bash # 1. Get a token (no account needed). Save token AND claim_url. curl -sX POST https://openartifacts.sh/v1/signup # 2. Publish. Reusing a slug publishes a new version at the same URL. curl -sX POST https://openartifacts.sh/v1/publish \ -H "Authorization: Bearer $OA_TOKEN" \ -F slug=my-page \ --form-string html='

hello

' # 3. Multi-file: each part's filename is its path inside the site. curl -sX POST https://openartifacts.sh/v1/publish \ -H "Authorization: Bearer $OA_TOKEN" \ -F slug=my-site \ -F 'files=@index.html;filename=index.html' \ -F 'files=@app.css;filename=css/app.css' ``` The publish response is {url, host, artifact_id, version, account}. Give the human the url - that is the point of the whole exercise. ## Rules an agent needs - Token format oa_sk_<32>, sent as Authorization: Bearer . Shown once. - slug matches ^[a-z0-9-]{3,40}$. Same slug = new version, same URL. - Limits: 10 MB and 50 files per publish. - A fresh account is provisional: it works immediately, and it and every page on it expire 30 MINUTES after signup unless a human opens claim_url. Claiming only works before then - after that the account is gone and the answer is a fresh POST /v1/signup. Show claim_url to the human the moment you get it. - Every error is {code, message, hint}. The hint names the next call. Follow it. ## Endpoints POST /v1/signup no auth -> account + token + claim_url POST /v1/publish token -> publish or version an artifact GET /v1/artifacts token -> list GET /v1/artifacts/:slug token -> detail + version history PATCH /v1/artifacts/:slug token -> {"visibility":"unlisted"} ($0.50/month) DELETE /v1/artifacts/:slug token -> stop serving POST /v1/artifacts/:slug/versions token -> new version (?activate=false to stage) POST /v1/artifacts/:slug/activate token -> {"version": n} rollback / roll forward GET /v1/account token -> quota, credit, claim_url PATCH /v1/account session -> {"allow_agent_join":false} (owner) GET /v1/usage token -> this month: publishes, storage, views, spend POST /v1/account/claim-link token -> rotate the claim code POST /v1/account/join no auth -> {"code"} redeem an invite -> token on that account GET /v1/account/members token -> who publishes here: members + active tokens DELETE /v1/account/members/:user_id session -> remove a member, revoke their tokens (admin+) POST /v1/account/invites session -> create an invite (admin+) GET /v1/account/invites session -> list invites (admin+) DELETE /v1/account/invites/:code session -> revoke an invite (admin+) GET /invite/:code browser -> the page a human opens to join GET /v1/tokens token -> list tokens POST /v1/tokens token -> mint a token (shown once) DELETE /v1/tokens/:id token -> revoke POST /v1/credits token -> buy credit (402 handshake) GET /v1/status no auth -> liveness POST /v1/reports no auth -> {"host","reason"} abuse report POST /mcp token -> MCP streamable HTTP endpoint GET /skill , /skill/reference.md no auth -> Claude skill package GET /openapi.json no auth -> machine-readable API The team routes take either the dashboard session cookie or a token with the account:manage scope. What a session may do is decided by its role on the account: member (publish, mint own tokens), admin (+ invites, members, tokens), owner (+ billing and the agent-join switch). ## Joining a shared account A human may hand you an invite code instead of a token. Redeem it once: ```bash curl -sX POST https://openartifacts.sh/v1/account/join \ -H "Content-Type: application/json" \ -d '{"code":""}' ``` -> 201 {token, account:{handle}, scopes, hint}. Save that token the way you would a signup token; it is shown once. That token belongs to SOMEONE ELSE'S account. Publishes on it draw that account's free allowance and bill its balance, and every member can see the pages it publishes. Name the account handle to the human every time you publish with it. A 403 on join means the owner turned agent joining off; ask the human for a token minted on the dashboard instead. Invites also expire, get revoked, and run out of uses - the error body says which. ## Money Free publishes are month-keyed, and which allowance you get depends on status: - provisional account: 5 free publishes for its whole (30-minute) life. - claimed account: 100 free publishes per UTC calendar month, resetting on the 1st. Everything past that is metered: - publish beyond the allowance ..... $0.005 each - private or unlisted artifact ..... $0.50 per artifact per month, recurring - storage .......................... $0.05 per GB-month - traffic .......................... $1.00 per million page views, first 1,000,000/month free The three recurring charges are applied by a monthly metering pass, not at request time, so a balance can go negative. While it is negative, a publish answers 402 asking you to top up. GET /v1/usage shows the month so far. When you are short, the API answers 402 with {code:"payment_required", amount_usd, accepts, hint}. An entry in accepts carries "instructions" only when that retry really settles here. Two ways to pay, both on the "dev" scheme in local dev: ```bash # 1. inline: resend the 402'd request with the header accepts[0] names curl -sX POST https://openartifacts.sh/v1/publish \ -H "Authorization: Bearer $OA_TOKEN" \ -H "X-PAYMENT: dev:0.005" \ -F slug=my-page --form-string html='

hi

' # 2. in bulk: buy credit once, then retry with no X-PAYMENT header curl -sX POST https://openartifacts.sh/v1/credits \ -H "Authorization: Bearer $OA_TOKEN" \ -H "Content-Type: application/json" \ -H "X-PAYMENT: dev:5" \ -d '{"amount_usd": 5}' ``` Top-ups are $1-100 (a real card rail starts at $5; the dev rail takes any of it). An inline X-PAYMENT amount must equal amount_usd exactly, so read amount_usd from the 402 rather than from the price list: on a balance the metering pass drove negative it is the arrears plus this request, which is the sum that clears it. Never spend on a human's behalf without telling them what it costs first. ## Moderation Every publish is scanned. A form asking for a seed phrase, a private key, or a card number with its CVV is blocked and its URL serves a 451 page instead of the content. A password field next to a brand lure is flagged "review": it keeps serving, and a human decides. Status is visible in GET /v1/artifacts/:slug.