Sites

Publish a new site.

Two body shapes. application/json with an html string (or markdown) plus any of the optional fields below, the simplest for an agent writing its own HTTP call: curl -X POST https://api.stacktr.ee/sites -H "Content-Type: application/json" -d '{"html":"<!doctype html>..."}'. Or multipart/form-data with a file field (a single .html/.htm/.md document, or a .zip containing index.html at the root), the only shape for a zip, a PDF, an image or e2e ciphertext. Agents working for one person can add owner_email (that person's address, with their permission: on a page published without an account they are emailed the link that keeps it, and once more when someone else first opens it) and agent (the assistant's own name, shown to the owner). Anonymous publishing works with no Authorization header from a direct client connection: the site gets a 24h TTL, can be up to 10 MB, and is rate-limited per IP. An authenticated publish counts against the plan: Free allows 3 pages in total (a monotonic counter, so deleting a page does not free the slot) and expires every page after 7 days. Example: curl -X POST https://api.stacktr.ee/sites -F file=@page.html. An anonymous publish returns a claim_token: it adopts the page into an account (POST /sites/{idOrSlug}/claim) AND, until it is used for that, it is the page's keyless update credential on PUT /sites/{idOrSlug} (Authorization: Claim <claim_token>), so a curl-only agent can keep one URL current without an account. Store it with the page id; it is as sensitive as the page.

post/sites

Request

  • Base URL: https://api.stacktr.ee
  • URL: https://api.stacktr.ee/sites
  • Auth: one of:
    • HTTP bearer
    • OAuth 2 (authorization code, authorization URL https://api.stacktr.ee/oauth/authorize, token URL https://api.stacktr.ee/oauth/token, scopes: sites:write)
    • none

Headers

Idempotency-Keystring

Makes a retry safe. Any unique string (a UUID is ideal), 1 to 255 visible ASCII characters. The same key with the same body inside 24 hours returns the ORIGINAL page (same id, same url, same tokens) with Idempotent-Replay: true, publishes nothing new, and spends no second page against the Free lifetime cap. Time-relative fields (ttl_seconds, and expires_at if it has since been changed) are recomputed on the replay rather than repeated from the original response. The same key with a DIFFERENT body is refused with 422 rather than being served the old page. Two requests carrying one key cannot both publish: the loser gets 409 idempotency_key_in_progress and should retry. Keys are scoped to the caller, so yours cannot collide with anyone else's — EXCEPT on an anonymous publish, where there is no account and the scope is the network address: the key must be unguessable there (send a UUID) or it is refused with 400 idempotency_key_too_weak. If the page a key produced has since been deleted or burned, the retry is 409 idempotent_page_gone, never a 201 for a dead link. Every response echoes Idempotency-Key and an Idempotency-Status of created, replayed, in_progress, mismatch, gone or unsupported.

Request body

htmlstring

The document, as a string. Required unless markdown is sent.

markdownstring

A Markdown document instead of HTML; rendered as a page.

filenamestring

Optional, default index.html (index.md for markdown).

owner_emailstring email

The person the page is for, as the publishing agent knows them. On a page published without an account, Stacktree emails them the link that keeps it, and once more when someone else first opens it (named open, or a second viewer). Ignored on an account publish. Send only with their permission.

agentstring

The publishing assistant's own name, a plain slug (muse, instinct, poke, dots, grok). Shown to the owner; analytics only.

passwordstring
recipientstring

Who the page is for, by name; the response carries their named link.

pii_check'warn' | 'block' | 'off'

Pre-flight scan for email addresses, card numbers, social security numbers, API keys and access tokens. "warn" (the default) publishes and says what it found in pii_warning (and the X-Stacktree-Pii-Warning header). "block" refuses with 422 pii_detected and publishes nothing; the MCP tools default to block. "off" skips the scan.

Response

Published. Also returned verbatim on an idempotent replay, with Idempotent-Replay: true. A page published without an account also carries claim_token, keep_url, and next.status (how to ask whether it has been opened); with owner_email, an owner_email object saying whether the keep link was sent.

idstring required

Stable site identifier.

slugstring nullable

Public subdomain slug, if the site is public.

visibility'unlisted' | 'public' | 'burn' required

Privacy mode.

urlstring uri

Canonical URL to view the site.

created_atinteger required

Unix epoch seconds.

updated_atinteger

Unix epoch seconds.

expires_atinteger nullable

Unix epoch seconds; null = no expiry. Clamped to the plan ceiling at write time (Free: 7 days, anonymous: 24 hours), so this can come back sooner than you asked for.

expires_at_isostring nullable

The same instant as expires_at, RFC 3339 UTC, second granularity (e.g. "2026-09-08T12:00:00Z"). Present so a client can show a deadline without reconstructing it from the epoch.

ttl_secondsinteger nullable

Seconds from now until the page stops serving. Null when it does not expire; 0 when it is already out of time. Never negative.

expiry_clampedboolean

True when the lifetime THIS request asked for was longer than the plan allows and expires_at is the shortened reality. Always present, false included. False on reads, which ask for no lifetime. Asking for a page that never expires on a capped plan is refused (409 expiry_clamped) rather than clamped, unless accept_clamp is sent.

expiry_ceiling_hoursinteger nullable

The longest lifetime this plan allows a page, in hours: 24 anonymous, 168 on Free, null on plans with no ceiling and on the paid one-off rails.

expiry_source'request' | 'plan_ceiling' | 'plan_default' | 'stored'

What decided the date: request (the caller named it), plan_ceiling (the plan cap did), plan_default (the plan default did), or stored (this call only read the row).

byte_sizeinteger

Total content size in bytes.

metrics_lockedboolean

True when the plan has no viewer numbers (anonymous and Free). The three viewer fields below are then null; there is no number to reveal, blur, or approximate.

openedboolean

Whether anyone has opened the page. Returned on every plan, including when metrics_locked is true, so "someone read this" survives the lock.

badgedboolean

Whether THIS page carries the "Made with Stacktree" footer as served on its stacktr.ee URL. Returned by GET /sites/{idOrSlug} only. A fact about the page, not about the plan: pages published before 2026-08-03, generated client portals and end-to-end encrypted pages carry no badge even on Free, and a remove_badge entitlement lifts it off one page or a whole account. Never assume it from the plan; anything offering to remove the badge should read this.

view_countinteger nullable

Raw view count. Null when metrics_locked.

unique_viewersinteger nullable

Distinct human viewers over the last 30 days. Null when metrics_locked.

last_viewed_atinteger nullable

Unix epoch seconds of the most recent open. Null when metrics_locked, or when never opened.

client_urlstring

Present on publish when the space has a connected hostname and the page got a space_path: the page’s address on the client’s own domain (e.g. https://acme.theiragency.com/june-report). Prefer handing this link to the user.

claim_tokenstring nullable

Anonymous publishes only. Adopts the page into an account (POST /sites/{idOrSlug}/claim) and, until then, is its keyless update credential. As sensitive as the page.

claim_urlstring uri

Anonymous publishes only. The dashboard claim screen for this page: sign in, and the page moves into that account at the same address.

keep_urlstring uri

Anonymous publishes only. Give this to the person you published for: it opens a page that says when this page stops working and offers both ways to keep it (a free account, or the link by email); opening it keeps nothing until they choose one. It also marks their browser so the page itself shows them a Keep button. It carries the claim token, so it is as sensitive as claim_url: share url, never this.

deleted_atinteger nullable

Unix epoch seconds the page stopped serving; null when it is live. Non-null means the URL is dead for everyone holding it.

delete_reasonstring nullable

Why it stopped serving: expired, owner (someone deleted it), abuse (a takedown), or merged (folded into another page; see merged_into). null while the page is live. abuse and merged are never restorable: POST /sites/{idOrSlug}/restore refuses them, so do not offer a restore control for either.

restorable_untilinteger nullable

Unix epoch seconds. A real deadline, not a hint: the hourly cleanup destroys the content at that moment and restore returns 404 from then on.

Changes

No recorded changes to this operation across all 1 revision of this API.