Publish a stoop site over HTTP with an API key, from a server-side app, agent, or CI job that has no folder and no terminal. Use when code you are writing needs to publish or revise a web page at runtime — a bot posting a report, a job publishing a dashboard. If you have the files on disk and a terminal, use `stoop` instead.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add mislavjc/stoop --skill stoop-api --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Stoop Api?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mislavjc-stoop-api)More formats (shields.io, HTML) on the badges page.
---
name: stoop-api
description: "Publish a stoop site over HTTP with an API key, from a server-side app, agent, or CI job that has no folder and no terminal. Use when code you are writing needs to publish or revise a web page at runtime — a bot posting a report, a job publishing a dashboard. If you have the files on disk and a terminal, use `stoop` instead."
---
# Publishing a stoop site over HTTP
One authenticated POST publishes a static site and returns its URL. No
browser session, no zip, no filesystem, no terminal — for code that needs to
put a page on the web while it runs.
Writing any `platform.*` call? Load the `stoop-platform` skill first — it
is the full SDK surface, and guessing at this API produces a page that looks
right and silently loses data.
## First: should this be a page at all?
| surface | fits | example |
|---|---|---|
| a short reply | a sentence, a number, a yes or no | "all three deploys are green" |
| a long document | prose read top to bottom, or edited afterwards | a postmortem, a spec |
| a page | anything whose **layout** carries meaning | a dashboard, a sortable table, a chart, a comparison grid |
The test is whether the layout carries meaning. Publish because reading the
thing as text would lose something — not because the answer felt important.
A paragraph you are proud of is still a paragraph; send it as a reply.
## The call
```sh
curl -X POST https://stoop.run/api/sites \
-H "authorization: Bearer $STOOP_API_KEY" \
-H "content-type: application/json" \
-d '{"name":"deploy-health","files":{"index.html":"<h1>hi</h1>"}}'
```
The response carries the site `url`, and `findings` (read them — see
below). Limits: 50 files, 25 MB total.
### The key
The user mints it once with `npx @stoop/cli keys create <name>`, and it is
shown only at creation. **Ask them to run it** — like `login`, it needs
their account, so do not run it yourself. Read the secret from the
environment; never hold it in source.
A key writes only inside its own namespace: a key named `release-bot`
reaches `release-bot-*` and nothing else, so the call above lands at
`release-bot-deploy-health.stoop.run`.
### Never start the name with your key's prefix
The namespace is added for you. So do not send `release-bot-deploy-health`
even when the human says "call it release-bot-deploy-health" — you would be
asking for `release-bot-release-bot-deploy-health`. The server strips one
redundant prefix, so this is no longer a broken URL, but it is still the
difference between the name you meant and one you did not.
### One name per recurring thing, forever
`name` is an upsert: the same name always maps to the same URL, so each
publish revises the page people already bookmarked instead of stranding it.
Name the page for what it **is**, not when it was made — `deploy-health`,
never `deploy-health-jul-30`. A new name for the same thing every week is
how you end up with eleven dead dashboards and nobody knowing which is
current. Date the content, not the name.
### Send every file, every time
`files` replaces the site: anything you omit is **deleted**. "Just
updating the CSS" means sending the HTML too.
So never publish a revision from memory. If you no longer hold what you
published — a later session, a fresh process — read the site back first
(below) and patch what you read. An approximation publishes just as cleanly
as the real thing and silently drops whatever you failed to recall, over a
page someone may already be reading.
### `keep` the files you are not editing
Naming a file in `keep` carries it over untouched instead of resending it:
```json
{"name": "deploy-health",
"files": {"index.html": "<h1>…</h1>"},
"keep": ["logo.png", "styles.css"]}
```
This is the answer to a file you cannot hold — a 2 MB image has no useful
representation in your context, and its bytes are already on the server.
Keeping one is free; resending it means downloading it first. Kept files are
still part of the site, so they count toward the 50-file and 25 MB limits.
Every name must be in the site as it stands right now. One that isn't is a
`400` naming it, never a quiet publish without it — so a `keep` list you
carried over from an older session fails loudly instead of deleting the file
it was meant to protect. Read the site back if you are unsure what it holds.
### Files may be text or binary
A file is either a plain string (UTF-8 text, the common case) or an object
saying which it is — the same shape a read-back returns, so what you read
can be published again unchanged:
```json
{"index.html": "<h1>hi</h1>",
"logo.png": {"encoding": "base64", "content": "iVBORw0KGgo..."}}
```
### Visibility is a decision, not a default
Sites published this way are private by default — only members of the key's
organization can open them.
- Internal data (metrics, customer names, anything out of a private system):
leave it private.
- "Make it shareable", "send this to the customer", a link going into a
public channel: pass `"visibility": "public"`. Private is the *wrong*
answer here — to anyone outside the org a private site is a sign-in wall
they cannot pass.
- Unsure: private, and say so in your reply so the human can ask for public.
### Retention
Sites expire 7 days after their last deploy, and every deploy pushes that
out — a page you keep revising never expires under a live link. The org can
make them permanent with the "Keep sites" switch on the dashboard's API keys
screen.
## Read the findings
Every publish runs the same static checks `stoop check` runs and returns
them as `findings`, empty when the site is clean:
```json
{ "code": "unknown-sdk-member", "severity": "warning",
"path": "index.html", "subject": "platform.db.query",
"message": "platform.db.query does not exist",
"hint": "available: platform.db.collection(name)" }
```
`message` and `hint` are prose and may be reworded at any time. If your
code decides anything — gate on errors, ignore a class of warning, check
whether last publish's problem is gone — branch on `code` and `subject`,
never on the sentence. `code` is one of `no-root-index`,
`reserved-path`, `sdk-not-loaded`, `unknown-sdk-member`,
`dangling-ref`; `code` + `path` + `subject` together identify a
finding across publishes.
Findings never block the publish — the site is live either way. They exist to
catch the SDK method you half-remembered, which is the most common reason a
page published this way looks right and does nothing. Read them, fix the
page, publish again over the same name.
Send `"dryRun": true` to validate without publishing: same findings, no
site created, nothing deployed.
## Did it work?
To confirm the page is up, fetch the URL you published — **without following
redirects**:
```sh
curl -s -o /dev/null -w "%{http_code}" --max-redirs 0 <url>
```
- `200` — published, public.
- `401`, or a `302` to `/login` — published, **private**, working.
This is success, not a broken page. Both carry
`x-stoop-auth-required: 1`.
- `404` — the publish did not land.
`fetch()` follows redirects by default, so a naive check of a private site
returns `200` carrying the *dashboard's login page* — which reads as "the
site published broken" when nothing is wrong. Pass
`redirect: "manual"`, or compare the final URL against the one you
published.
## What have I already published?
```sh
curl -H "authorization: Bearer $STOOP_API_KEY" https://stoop.run/api/sites
```
Your key's namespace only, newest deploy first. Across sessions this is the
only way to know whether a name is already taken by one of your own earlier
pages — check before publishing something "new".
## Read a site back
```sh
curl -H "authorization: Bearer $STOOP_API_KEY" \
https://stoop.run/api/sites/<name>
```
Returns the site's current `files` — every file, exactly as served — plus
its `lastDeployedAt`. One address per site, and it takes the same `name`
you publish under; the full slug from the response (`release-bot-notes`)
works too, so whichever one you kept is the right one.
This is what makes revising safe. Read, change the one file you mean to
change, send the whole map back:
```js
const site = await (await fetch(url, { headers })).json();
site.files["index.html"] = { encoding: "utf8", content: nextHtml };
// every other file goes back untouched — including any binary one
await publish({ name: "notes", files: site.files });
```
Hand back the `files` map whole. Filtering it to "just the text ones"
deletes the rest, because a publish still replaces the entire site. `keep`
is the cheaper half of the same rule: read to find out what is there, then
name the untouched files instead of shipping them back.
Limits mirror the publish limits — 50 files, 25 MB — because a site you
cannot re-publish is not one this can usefully hand you. A larger site was
deployed from a folder with the CLI and has to be revised the same way.
## Publishing only if nobody beat you to it
Reading a site and publishing it back is a read-modify-write, and two of them
overlapping lose the earlier edit with a `200` on both sides. Send the
`lastDeployedAt` you read as an `If-Match` to make the publish conditional:
```sh
curl -X POST https://stoop.run/api/sites \
-H "authorization: Bearer $STOOP_API_KEY" \
-H "if-match: 2026-07-30T09:12:44.108Z" \
-H "content-type: application/json" -d @site.json
```
`412` means somebody published in between: read the site again, re-apply
your change to what you just read, and retry. Do not retry without the
re-read — that is the lost update you were avoiding.
Worth it whenever a page has more than one writer — a schedule and a person,
or two of your own runs. Skip it and the last publish wins.
## Removing a site
```sh
curl -X DELETE -H "authorization: Bearer $STOOP_API_KEY" \
https://stoop.run/api/sites/<name>
```
`204`, and the address 404s from then on. Same address as the read, and it
takes the `name` or the full slug. A key deletes only inside its own
prefix; anything else is a `403`. `If-Match` works here too, if you want
the delete to refuse a site that changed since you looked at it.
Deletion takes the whole site and cannot be undone: the files, anything
visitors uploaded, and the site's database. To change what a page *says*,
publish over the same name instead — that keeps the URL people already have.
So a site you published to try something out, or under a name you regretted
the moment you saw it, is not permanent. Delete it rather than leaving it in
the namespace for the next session to puzzle over.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!