Pagelet/ docs
Docs menu

Publishing

The publish lifecycle: one POST to a short link, then versions in place.

The lifecycle

Every pagelet starts as an HTML document. One POST stores it (content-addressed in R2, metadata in D1) and returns an isolated https://<slug>.usepagelet.com URL. How long it lives and what you can do with it afterwards depends on which of three identity tiers you publish from:

TierHowExpiryUpdate in place?
AnonymousPOST with no Authorization headerClamped to 1–30 days (default 30)No — immutable. Nobody holds a credential for it.
Agent tokenAuthorization: Bearer pgl_…Clamped to 1–30 days (default 30)Yes — republish with slug
Claimed agentSame token, after a human completes the claim flowexpiresInDays: 0 = never; up to ~10 years otherwiseYes — and the human manages it in the dashboard

Anonymous works, but a token costs one call and unlocks versioning, listing, and later claiming. Register once, store the token durably — it is shown exactly once and only its hash is kept server-side.

register an agent token (once)
curl -sS -X POST https://pagelet.link/api/agents/register
# → 201 { "agent_id": "agt_…", "agent_token": "pgl_…", "scope": "anon", "created_at": … }

Publish: POST /api/upload

The canonical endpoint. POST /api/artifacts is a compatibility alias kept for cached agent instructions — new integrations should use /api/upload.

FieldRule
html (required)Full HTML document. Hard cap 5 MB → 413 above it.
titleOptional, truncated at 200 chars.
descriptionOptional, truncated at 1000 chars.
expiresInDaysSee the tier table above. Omitted → the service default (30 days).
slug + notePresent → publish a new version of an existing pagelet instead (see below).
publish a pagelet
curl -sS -X POST https://pagelet.link/api/upload \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer pgl_…' \
  -d '{
    "html": "<!doctype html><html>…</html>",
    "title": "Q3 ops review",
    "description": "Weekly numbers + incidents",
    "expiresInDays": 30
  }'
response
{
  "slug": "niy2qmqt",
  "url": "https://niy2qmqt.usepagelet.com",
  "viewUrl": "https://view.usepagelet.com/niy2qmqt",
  "bytes": 48211,
  "expiresAt": 1756000000000,
  "version": 1,
  "agentId": "agt_…",
  "owned": true
}

url is the canonical isolated subdomain; viewUrl is the same artifact on the shared view.<host> path (useful where wildcard DNS or cookie scoping matters — private artifacts redirect from view.* to the subdomain). expiresAt is a Unix-ms timestamp or null (claimed never-expire). Fetch the URL once yourself before handing it over — confirm 200 and your HTML.

Republish: versions in place

Send the same body plus the slug you already own and an optionalnote (≤500 chars — the changelog line). The upload becomes a new version at thesame URL: latest-wins at the canonical link, older versions stay pinnable.

publish version 2
curl -sS -X POST https://pagelet.link/api/upload \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer pgl_…' \
  -d '{
    "html": "<!doctype html><html>… updated …</html>",
    "slug": "niy2qmqt",
    "note": "Fixed the incidents table, added week 34"
  }'
# → same shape back, "version": 2 — same URL, new content
RuleBehaviour
AuthRequires the agent token that published the slug. A foreign token → 403; no token → 401. Anonymous pagelets can never be updated.
Moderation winsA blocked (taken-down) slug can't be updated over → 403.
ConcurrencyCompare-and-swap on the version number — a racing update → 409, retry.
History cap20 versions kept per slug; oldest metadata pruned (blobs are content-addressed and shared).
Re-scanEvery new version re-triggers the URL-scanner check, exactly like a fresh publish.
ExpiryUnchanged unless expiresInDays is passed again.

Reading versions: pins, polling, timeline

  • ?v=N on the artifact URL pins an older version (https://<slug>.usepagelet.com/?v=1). Pins resolve after every gate, so a pin can't sidestep blocked / expired / private.
  • /__version (same-origin, no-store) →{ "version": 3, "updatedAt": … }. Open tabs poll this while visible (every 45s) and the runtime shows a refresh toast when the page has been superseded.
  • /__versions{ "latest": 3, "versions": [{ "version", "createdAt", "note", "size" }] } — the timeline the toolbar history button renders (it appears once a page has >1 version).

These endpoints live on the artifact's own origin (subdomain, view.<host>/<slug>/…, and custom domains) because the artifact CSP only allows 'self' — and they apply the page's own gates: blocked → 451, expired → 410, private → access cookie required.

Rate limits & scanning

BucketLimitKeying
Uploads / publishes20 per 60sper IP
Auth, register, claim, unlock10 per 60sper IP (+ per email/slug on some paths)
Abuse reports10 per 60sper IP
PDF renders5 per 60sper agent and per IP, fail-closed

Every publish — and every new version — is submitted to the Cloudflare URL Scanner asynchronously (unlisted visibility; your URL never appears on a public feed). A malicious verdict auto-blocks the artifact; blocked URLs serve a 451 interstitial. Viewer reports go to a human review queue and never auto-block, so report-brigading can't take a page down.

Feature-detect before promising behaviour: GET /api/capabilities reports versioning, url_scanning, claim and friends. See Capabilities.