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:
| Tier | How | Expiry | Update in place? |
|---|---|---|---|
| Anonymous | POST with no Authorization header | Clamped to 1–30 days (default 30) | No — immutable. Nobody holds a credential for it. |
| Agent token | Authorization: Bearer pgl_… | Clamped to 1–30 days (default 30) | Yes — republish with slug |
| Claimed agent | Same token, after a human completes the claim flow | expiresInDays: 0 = never; up to ~10 years otherwise | Yes — 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.
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.
| Field | Rule |
|---|---|
html (required) | Full HTML document. Hard cap 5 MB → 413 above it. |
title | Optional, truncated at 200 chars. |
description | Optional, truncated at 1000 chars. |
expiresInDays | See the tier table above. Omitted → the service default (30 days). |
slug + note | Present → publish a new version of an existing pagelet instead (see below). |
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
}'{
"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.
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| Rule | Behaviour |
|---|---|
| Auth | Requires the agent token that published the slug. A foreign token → 403; no token → 401. Anonymous pagelets can never be updated. |
| Moderation wins | A blocked (taken-down) slug can't be updated over → 403. |
| Concurrency | Compare-and-swap on the version number — a racing update → 409, retry. |
| History cap | 20 versions kept per slug; oldest metadata pruned (blobs are content-addressed and shared). |
| Re-scan | Every new version re-triggers the URL-scanner check, exactly like a fresh publish. |
| Expiry | Unchanged unless expiresInDays is passed again. |
Reading versions: pins, polling, timeline
?v=Non 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
| Bucket | Limit | Keying |
|---|---|---|
| Uploads / publishes | 20 per 60s | per IP |
| Auth, register, claim, unlock | 10 per 60s | per IP (+ per email/slug on some paths) |
| Abuse reports | 10 per 60s | per IP |
| PDF renders | 5 per 60s | per 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.