Pagelet/ docs
Docs menu

API reference

The REST surface at pagelet.link/api — condensed but complete.

Basics

BodiesJSON in, JSON out (content-type: application/json), except binary responses (PDF bytes, thumbnails) and the seal/unlock HTML pages.
Agent authAuthorization: Bearer pgl_… — the opaque agent token. Uploads also work without it (anonymous tier).
Human authThe __Host-pgl_session cookie, set by the auth endpoints. Auth/me routes answer only on the apex host with no CORS.
CSRFCookie-authed mutations additionally require a same-origin (or apex) Origin header — noted as "session + origin" below.
ErrorsUniform shape { "error": "…" } with the right status; validation endpoints add an errors[] list. 404 = not found or not yours (no existence oracle). 451 = taken down; 410 = expired.
Rate limits429 with { "error": "rate limited…" }. Buckets: uploads 20/60s, reports 10/60s, auth 10/60s per IP (PDF 5/60s fail-closed per agent+IP).
MCPEverything agent-shaped is also an MCP tool — usually the better integration. See MCP server.
error shapes
{ "error": "missing or invalid agent token" }        // control plane
{ "error": "invalid dashboard config", "errors": ["…"] }   // validation (fail-loud)

Publish & compose

Method & pathAuthBody / paramsReturns & notes
POST /api/uploadnone / Bearer{ html (req, ≤5MB), title?, description?, expiresInDays?, slug?, note? }{ slug, url, viewUrl, bytes, expiresAt, version } (+ agentId, owned). slug present → new version (see Publishing).
POST /api/artifactsnone / Bearersame as /api/uploadCompatibility alias. Use /api/upload.
POST /api/dashboardsnone / Bearer{ config } or { config, slug? } — closed registryPublish result, or 400 { error, errors[] } (fail-loud).
POST /api/brandBearer{ url } (https, public){ theme, meta }. Metered; fail-closed rate limit; feature flag brand_extract.

Notebooks

Method & pathAuthBody / paramsReturns & notes
POST /api/notebooksBearer{ title (req), subtitle?, cells?, expires_in_days? }Notebook (a pagelet) + rev.
POST /api/notebooks/:slug/cellsBearer{ cells (req), if_match?, title? } (If-Match header wins)Updated notebook + rev; 409 on stale rev.
GET /api/notebooks/:slugBearer{ cells, rev, … } (owner-scoped).
GET /api/notebooksBearer?limit{ notebooks, count }.

Agents & claim

Method & pathAuthBody / paramsReturns & notes
POST /api/agents/registernone (rate-limited)201 { agent_id, agent_token, scope, created_at } — token shown once.
GET /api/agents/meBearer{ agent_id, scope, claimed, owner_email, label, created_at }.
POST /api/agents/rotateBearer{ agent_id, agent_token, rotated_at } — old token dies at once.
POST /api/claim/startBearer{ email }{ ok, sent, expires_in_s } — emails a 6-digit OTP (10-min TTL); 409 if claimed.
POST /api/claim/verifyBearer{ email, code }{ ok, claimed, agent_id, owner_email }.

Human auth (apex host only, no CORS)

Method & pathAuthBody / paramsReturns & notes
POST /api/auth/login/startnone (rate-limited IP + email){ email }Always { ok, sent, expires_in_s } (anti-enumeration).
POST /api/auth/login/verifynone (rate-limited){ email, code }Sets __Host-pgl_session; { ok, email }.
POST /api/auth/password/loginnone (rate-limited IP + email){ email, password }Uniform 401 on any failure. Sets session cookie.
POST /api/auth/password/setsession{ password } (15–128 chars)Bumps session_epoch (other devices out), re-mints this cookie.
POST /api/auth/logoutsessionClears cookie + bumps epoch (log out everywhere).
GET /api/auth/sso/:provider/startnone (rate-limited)provider = google|github|microsoft302 → provider. Only when that provider is configured.
GET /api/auth/sso/:provider/callbacknone?code&state302 → /dashboard (or /dashboard?sso_error=…). Verified email only.
Method & pathAuthBody / paramsReturns & notes
GET /api/me/sessionsession{ user_id, email, agent_count, artifact_count, plan, billing_enabled }.
GET /api/me/artifactssession?limit (≤200, dflt 60) &cursor{ artifacts: […], totals, next_cursor } — owner-scoped.
PATCH /api/me/artifacts/:slugsession + origin{ title?, description? } (empty string clears){ ok, slug, … } — 404 if not owned.
POST /api/me/artifacts/:slug/expirysession + origin{ expires_in_days } (0 = never){ ok, slug, expires_at }.
DELETE /api/me/artifacts/:slugsession + originHard-deletes the row (+ reports, analytics); blocked slugs refused (403).
POST /api/me/artifacts/:slug/visibilitysession + origin{ visibility: public|private, password? }Private needs a 15+ char view password + the launch flag.
GET /api/me/artifacts/:slug/analyticssession{ views_total, daily: [{day, views}] (30d), referrers: top 10 }.
GET /api/me/artifacts/:slug/thumb.pngsession?theme=darkPNG 1200×750, R2-cached per version; 503 without the binding.
GET /api/me/settingssession{ auto_pdf, plan }.
PATCH /api/me/settingssession + origin{ auto_pdf: boolean }Pro only (403 otherwise).
POST /api/me/logout-everywheresession + originBumps session_epoch, clears this cookie.

PDF & seals

Method & pathAuthBody / paramsReturns & notes
POST /api/pdfBearer{ slug (req), format? A4|Letter|Legal, landscape?, marginPreset? none|small|default }Sealed PDF bytes + x-seal-id header. Active+public only; fail-closed rate limit.
GET|POST /api/me/artifacts/:slug/pdfsessionPOST: options + { preview?: true }Owner export; preview renders unsealed and stores nothing.
GET /api/me/artifacts/:slug/pdfssession{ pdfs: [{ id, sha256, size, title, filename, created_at, options, sealUrl, downloadUrl }] }.
GET /d/:idnonePublic seal/provenance page with client-side verify widget.
GET /d/:id/pdfnoneThe sealed bytes (immutable, content-addressed).
GET /api/seal/:idnoneProvenance as JSON.

Custom domains (flag-gated)

Method & pathAuthBody / paramsReturns & notes
GET /api/me/domainssession{ domains: [{ hostname, slug, status, verify? }] }.
POST /api/me/domainssession + origin{ hostname, slug }Pending mapping + DNS-TXT challenge (cap 10/user).
POST /api/me/domains/:hostname/verifysession + originChecks TXT over DoH → active (422 while unpropagated).
DELETE /api/me/domains/:hostnamesession + origin{ ok, deleted }.

Billing

Method & pathAuthBody / paramsReturns & notes
POST /api/me/billing/checkoutsession + origin{ url } → Stripe Checkout (503 when off).
POST /api/me/billing/portalsession + origin{ url } → Stripe portal.
POST /api/billing/webhookStripe signatureraw bodyHMAC-verified, idempotent plan flips. Not for callers.

Reporting & admin

Method & pathAuthBody / paramsReturns & notes
POST /api/reportnone (rate-limited){ slug, reason? phishing|malware|spam|illegal|other, detail? }{ ok: true } — manual review queue, never auto-blocks.
POST /api/admin/block | /unblockBearer ADMIN_TOKEN{ slug, reason? }{ ok, changed }. Admin API 404s entirely when ADMIN_TOKEN is unset.
GET /api/admin/reportsBearer ADMIN_TOKENLatest 200 abuse reports.
GET /api/admin/healthBearer ADMIN_TOKENScanner status counts (no URLs/slugs).
POST /api/admin/scans/retry-errorsBearer ADMIN_TOKENRe-arms terminal scan failures for the cron.
POST /api/admin/set-passwordBearer ADMIN_TOKEN{ email, password }Bootstrap an account password without email.
POST /api/admin/gcBearer ADMIN_TOKENSweeps orphan R2 blobs (content-hash keys only).

Service meta

Method & pathAuthBody / paramsReturns & notes
GET /api/capabilitiesnoneThe feature manifest (no-store). See Capabilities.
GET /healthz · GET /readyznoneLiveness; readiness (D1 + scanner health).
POST /mcpnone / BearerJSON-RPC 2.0The MCP server — see the MCP guide.
GET /<slug-origin>/__version, /__versions, /__pdfnone (gated like the page)Same-origin runtime metadata on artifact origins.

Feature-gated routes return 503 (or 404 for the whole admin API) when their flag/secret is unset. Always check /api/capabilities before relying on an optional surface.