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