# 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
POST /auth/register no auth -> {"username","password"} human sign-up -> oa_sess cookie
POST /auth/login no auth -> {"username","password"} -> oa_sess cookie
POST /auth/logout session -> clear the cookie
GET /auth/me session -> the signed-in user + the accounts they own
GET /signin browser -> the sign-in / create-account page
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)
POST /v1/credits/checkout session -> {"amount_usd"} card top-up -> {url} (humans)
POST /webhooks/stripe stripe -> signed callback; credits the account
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).
## Humans sign in with a password; you do not
/auth/register and /auth/login exist for the PERSON, not for you. They set the
oa_sess cookie a browser carries, which is what claiming an account and
accepting an invite need. Never ask a human for their password and never send
one yourself: your credential is the bearer token, and the human's credential
stays in their browser. When a human has to sign in, give them the link -
https://openartifacts.sh/signin?next= - and let them type it there.
## 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.
### The card is the human's, not yours
POST /v1/credits/checkout ($5-500) mints a Stripe Checkout Session and returns
{url}. It takes the oa_sess cookie only: a bearer token gets 403, because a
hosted card page needs a browser and a person. When a human needs to add money
by card, send them to https://openartifacts.sh/credits and let them press the button there. Credit
lands when Stripe calls POST /webhooks/stripe back, so the balance may take a
moment after the redirect - re-read GET /v1/account rather than assuming. If the
deployment has no Stripe keys the endpoint answers 501 not_configured and the
402 rails above are still the way to pay.
## 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. Where a classifier is configured it reads the page
too and the stricter verdict wins, so those rules are a floor, not the whole
list: do not publish a page that imitates someone else's sign-in. Status is
visible in GET /v1/artifacts/:slug.