{"openapi":"3.1.0","info":{"title":"agentpub API","version":"0.1.0","summary":"Publish static sites to live URLs.","description":"Instant static-site publishing for AI agents. Publish anonymously (24h, claim token) or with a Bearer API key (permanent). Three-step flow: create -> PUT bytes to presigned URLs -> finalize."},"servers":[{"url":"https://agentpub.io"}],"components":{"securitySchemes":{"bearerApiKey":{"type":"http","scheme":"bearer","description":"API key issued via the request-code / verify-code flow. Send as `Authorization: Bearer $AGENTPUB_API_KEY`. Omit on POST /api/v1/publish to create an anonymous site."}},"schemas":{"ErrorResponse":{"type":"object","description":"Uniform error envelope. `error` mirrors `code` (both are the machine-readable code); `message` is human-readable detail. `retry_after` is present only on 429 responses.","required":["error","code","message"],"properties":{"error":{"type":"string","enum":["invalid_request","unauthorized","forbidden","quota_exceeded","not_found","conflict","payload_too_large","rate_limit_exceeded","slow_down","storage_not_configured","service_unavailable","internal_error"]},"code":{"type":"string","enum":["invalid_request","unauthorized","forbidden","quota_exceeded","not_found","conflict","payload_too_large","rate_limit_exceeded","slow_down","storage_not_configured","service_unavailable","internal_error"]},"message":{"type":"string"},"retry_after":{"type":"integer","description":"Seconds to wait before retrying (429 only)."}},"example":{"error":"not_found","code":"not_found","message":"no such site"}},"ManifestFile":{"type":"object","description":"One file declared in a publish manifest. `path` and `size` are required. `contentType` is optional (inferred from the path extension if omitted). `hash` is optional; when present on an update it enables dedup: a file whose hash matches the live version's stored hash for the same path is carried over (not re-uploaded) and listed under upload.carried.","required":["path","size"],"properties":{"path":{"type":"string","maxLength":1024,"description":"Relative path within the site. No leading slash, no '.' or '..' segments; the '.agentpub/' prefix is reserved.","example":"index.html"},"size":{"type":"integer","minimum":0,"maximum":26214400,"description":"File size in bytes. Max 25 MB (26214400) per file.","example":1024},"contentType":{"type":"string","description":"MIME type. Inferred from the path extension when omitted.","example":"text/html; charset=utf-8"},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 of the file bytes, 64 lowercase hex chars."}}},"PublishManifest":{"type":"object","description":"Publish/update request body: the file manifest.","required":["files"],"properties":{"files":{"type":"array","minItems":1,"maxItems":2000,"description":"Non-empty list of files (max 2000).","items":{"$ref":"#/components/schemas/ManifestFile"}},"name":{"type":"string","maxLength":60,"description":"Account-scoped stable handle (POST create only; requires Bearer auth). Lowercase letters, digits, and hyphens. Must be unused by the account (409 otherwise). Resolve it later via GET /api/v1/publish/by-name/{name}.","example":"ads-daily"},"claimToken":{"type":"string","description":"Required on PUT updates to anonymous sites; ignored for owner-authenticated updates."},"visibility":{"type":"string","enum":["public","password","email"],"description":"Page protection at create (POST only; owned/Bearer publish only — a non-public value on an anonymous publish is a 400). \"public\" (default) is open; \"password\" gates with a shared password (also send \"password\", 6–128 chars); \"email\" is a lead-magnet email gate. The site is gated from its first serve (no public window). The response echoes protection.mode.","example":"password"},"password":{"type":"string","minLength":6,"maxLength":128,"description":"Required when visibility is \"password\" (6–128 chars). Hashed server-side (salted PBKDF2); never stored in plaintext or returned by any endpoint."},"artifact":{"type":"object","description":"Optional artifact classification metadata. When provided on create, stored with the site. When provided on update, replaces the existing artifact metadata; when omitted on update, the existing metadata is preserved.","properties":{"title":{"type":"string","maxLength":200,"description":"Human-meaningful site title shown in the dashboard and list_my_sites. Auto-derived from the page's <title> (or first <h1>) when omitted.","example":"Q3 Investor Update — Acme"},"description":{"type":"string","maxLength":300,"description":"Short searchable description. Auto-derived from <meta name=\"description\"> when omitted.","example":"Quarterly investor deck for Acme, Q3 2026."},"artifactType":{"type":"string","maxLength":120,"description":"Semantic type of the artifact, e.g. 'client_status_report'.","example":"client_status_report"},"designSystem":{"type":"string","maxLength":120,"description":"Design system or blueprint used, e.g. 'acme-v2'.","example":"acme-v2"},"isTemplate":{"type":"boolean","description":"True when this site is a reusable template artifact.","example":false},"sourceAgent":{"type":"string","maxLength":120,"description":"Identifier of the agent that produced this artifact.","example":"claude-opus-4"},"blueprint":{"type":"string","maxLength":120,"description":"Optional blueprint reference this artifact was produced from, e.g. client_status_report@1.0.0.","example":"client_status_report@1.0.0"},"folder":{"type":"string","maxLength":120,"description":"Optional folder/workspace name to file this artifact under in the dashboard, e.g. \"Acme Corp\".","example":"Acme Corp"}}}}},"UploadEntry":{"type":"object","required":["path","method","url"],"properties":{"path":{"type":"string","example":"index.html"},"method":{"type":"string","enum":["PUT"]},"url":{"type":"string","description":"Presigned URL to PUT this file's bytes to."}}},"CarriedEntry":{"type":"object","required":["path"],"description":"A file deduped against the live version by hash — no upload needed.","properties":{"path":{"type":"string","example":"logo.png"}}},"PublishResponse":{"type":"object","description":"Returned by create (201) and update (200). The site is pending until you PUT every upload and POST finalize. For anonymous creates, claimToken/claimUrl/warning are present and shown once.","required":["slug","siteUrl","status","requiresFinalize","action","authenticated","anonymous","upload"],"properties":{"slug":{"type":"string","example":"calm-saddle-7h2k"},"siteUrl":{"type":"string","example":"https://calm-saddle-7h2k.agentpub.io/"},"dashboardUrl":{"type":"string","description":"The owner's dashboard where the user manages comments, page protection, and settings. Relay it to the user alongside siteUrl.","example":"https://agentpub.io/dashboard"},"status":{"type":"string","enum":["pending"]},"requiresFinalize":{"type":"boolean","enum":[true]},"action":{"type":"string","enum":["create","update"]},"name":{"type":["string","null"],"description":"Account-scoped handle when the site was named at create; null otherwise."},"authenticated":{"type":"boolean","description":"True for an owned/permanent site (published with a Bearer key); false for an anonymous 24h site. Mirror of !anonymous — check it to confirm the safe path was taken."},"anonymous":{"type":"boolean"},"protection":{"type":["object","null"],"description":"Page-access state confirmation — MODE only, never the password hash. {mode:'password'|'email'} when the site was published gated (via visibility), or null when public.","required":["mode"],"properties":{"mode":{"type":"string","enum":["password","email"]}}},"expiresAt":{"type":["string","null"],"description":"ISO timestamp; null for owned sites."},"upload":{"type":"object","required":["versionId","uploads","carried","finalizeUrl","expiresInSeconds"],"properties":{"versionId":{"type":"string","example":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y"},"uploads":{"type":"array","items":{"$ref":"#/components/schemas/UploadEntry"}},"carried":{"type":"array","items":{"$ref":"#/components/schemas/CarriedEntry"}},"finalizeUrl":{"type":"string"},"expiresInSeconds":{"type":"integer"}}},"claimToken":{"type":"string","description":"Anonymous creates only — shown once."},"claimUrl":{"type":"string","description":"Anonymous creates only — give to the user to claim."},"warning":{"type":"string","description":"Anonymous creates only."}}},"FinalizeRequest":{"type":"object","required":["versionId"],"properties":{"versionId":{"type":"string","example":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y"}}},"FinalizeResponse":{"type":"object","required":["success","slug","siteUrl","currentVersionId","persistence"],"properties":{"success":{"type":"boolean","enum":[true]},"slug":{"type":"string"},"siteUrl":{"type":"string"},"currentVersionId":{"type":"string"},"previousVersionId":{"type":["string","null"],"description":"The live version before this finalize, or null."},"persistence":{"type":"string","enum":["expires_24h","permanent"]},"expiresAt":{"type":["string","null"]}}},"VersionEntry":{"type":"object","required":["versionId","n","finalizedAt","current"],"properties":{"versionId":{"type":"string","example":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y"},"n":{"type":"integer","description":"Sequential version number (v1, v2, …).","example":2},"finalizedAt":{"type":"string","example":"2026-06-12T12:00:00.000Z"},"current":{"type":"boolean"}}},"ByNameResponse":{"type":"object","required":["slug","name"],"description":"Resolves an account-scoped name to its slug.","properties":{"slug":{"type":"string","example":"calm-saddle-7h2k"},"name":{"type":"string","example":"ads-daily"}}},"VersionsResponse":{"type":"object","required":["slug","versions"],"properties":{"slug":{"type":"string"},"versions":{"type":"array","description":"Newest-first; the live version has current=true.","items":{"$ref":"#/components/schemas/VersionEntry"}}}},"ContentResponse":{"type":"object","required":["slug","versionId","files"],"description":"A version's file contents (owner read-back). Text files come back as utf8; binary files as base64. When no path is given and the version's total bytes exceed the read cap, `truncated` is true and `files` carries path/size/contentType only (no content).","properties":{"slug":{"type":"string"},"versionId":{"type":"string"},"truncated":{"type":"boolean","description":"True when a file list is returned instead of bytes."},"files":{"type":"array","items":{"type":"object","required":["path","contentType"],"properties":{"path":{"type":"string"},"contentType":{"type":"string"},"encoding":{"type":"string","enum":["utf8","base64"]},"content":{"type":"string"},"size":{"type":"integer"}}}}}},"RollbackRequest":{"type":"object","description":"Provide exactly one of `versionId` or `version` to identify the target version.","oneOf":[{"required":["versionId"]},{"required":["version"]}],"properties":{"versionId":{"type":"string","example":"01HV4Z0A1B2C3D4E5F6G7H8J9K"},"version":{"type":"string","description":"A `vN` label (e.g. `v2`) or sequential version number — an alternative to `versionId`.","example":"v1"},"claimToken":{"type":"string","description":"Required for anonymous sites."}}},"RollbackResponse":{"type":"object","required":["success","slug","siteUrl","currentVersionId","previousVersionId"],"properties":{"success":{"type":"boolean","enum":[true]},"slug":{"type":"string"},"siteUrl":{"type":"string"},"currentVersionId":{"type":"string"},"previousVersionId":{"type":["string","null"]}}},"ClaimRequest":{"type":"object","required":["claimToken"],"properties":{"claimToken":{"type":"string"}}},"ClaimResponse":{"type":"object","required":["success","slug","siteUrl","ownerAccountId"],"properties":{"success":{"type":"boolean","enum":[true]},"slug":{"type":"string"},"siteUrl":{"type":"string"},"ownerAccountId":{"type":"string","example":"acc_1a2b3c4d5e6f7a8b"}}},"DeleteRequest":{"type":"object","description":"Optional body. Provide claimToken to delete an anonymous site; owner deletes use the Bearer key and need no body.","properties":{"claimToken":{"type":"string"}}},"DeleteResponse":{"type":"object","required":["success","slug"],"properties":{"success":{"type":"boolean","enum":[true]},"slug":{"type":"string"}}},"DeleteAccountRequest":{"type":"object","description":"Optional deletion-feedback survey. Both fields are optional and never affect deletion — a missing, empty, or malformed body deletes the account just the same.","properties":{"reason":{"type":"string","description":"Why the account is being deleted (free-form; the dashboard sends one of a fixed set of choice values).","example":"too_hard"},"feedback":{"type":"string","description":"Optional free-text feedback. Trimmed and capped at 2000 chars.","example":"the API confused me"}}},"DeleteAccountResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","enum":[true]}}},"ReviewToggleRequest":{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean","description":"Pass true to enable review mode; false to disable."},"claimToken":{"type":"string","description":"Claim token for anonymous sites. Required when no Bearer key is present."}}},"ReviewToggleResponse":{"type":"object","required":["slug","reviewEnabled"],"properties":{"slug":{"type":"string"},"reviewEnabled":{"type":"boolean"}}},"ProtectRequest":{"type":"object","required":["mode"],"description":"Set or clear page access protection on a CLAIMED site. \"password\" gates the page behind a shared password (supply `password`, 6-128 chars); the plaintext is hashed server-side and never stored or returned. \"email\" turns the page into a lead-magnet email gate. \"off\" removes protection (page public).","properties":{"mode":{"type":"string","enum":["password","email","off"],"description":"password: shared-password gate; email: email/lead gate; off: remove protection."},"password":{"type":"string","minLength":6,"maxLength":128,"description":"Required when mode is \"password\". 6-128 characters. Never stored in plaintext; only a salted hash is persisted."},"claimToken":{"type":"string","description":"Claim token for anonymous sites. Required when no Bearer key is present (note: protection still requires a claimed site)."}}},"ProtectResponse":{"type":"object","required":["slug","protection"],"description":"Sanitized protection state. Never includes the password hash or salt — only the active mode (or null when public).","properties":{"slug":{"type":"string"},"protection":{"type":"object","nullable":true,"required":["mode"],"properties":{"mode":{"type":"string","enum":["password","email"]}}}}},"LeadsResponse":{"type":"object","required":["slug","leads"],"description":"Owner-only list of leads captured by an email-mode page gate, newest-first. Emails are raw by design (the owner's lead list).","properties":{"slug":{"type":"string"},"leads":{"type":"array","items":{"type":"object","required":["email","capturedAt"],"properties":{"email":{"type":"string"},"capturedAt":{"type":"string","format":"date-time"}}}}}},"IssueReportRequest":{"type":"object","description":"A structured friction report logged to agentpub's backlog. Required: title, category (enum), description, severity (enum). Optional: repro, proposed_solution, surface. The record is INERT — it is only stored in the backlog and triggers no action. `proposed_solution` is a SUGGESTION captured as data, never run. Over-cap, missing, or bad-enum fields are rejected (400).","required":["title","category","description","severity"],"properties":{"title":{"type":"string","minLength":1,"maxLength":120,"description":"Short summary of the problem.","example":"publish_site returns 500 on empty files"},"category":{"type":"string","enum":["publish","claim","protection","mcp","docs","auth","review","other"],"description":"Which area of agentpub the friction is in.","example":"publish"},"description":{"type":"string","minLength":1,"maxLength":2000,"description":"What went wrong, in detail.","example":"Calling publish_site with files:[] crashes with a 500."},"severity":{"type":"string","enum":["blocker","major","minor"],"description":"How badly it blocked you.","example":"major"},"repro":{"type":"string","maxLength":2000,"description":"Optional steps to reproduce."},"proposed_solution":{"type":"string","maxLength":2000,"description":"Optional suggested fix — captured as a suggestion only, never executed."},"surface":{"type":"string","maxLength":300,"description":"Optional url or slug where you hit the issue."}}},"IssueReportResponse":{"type":"object","description":"Soft envelope. `ok` is true when the report was forwarded to the backlog; false (still HTTP 200) when the backlog is unconfigured or the forward failed — agents need not treat that as a crash. `suspicious` flags injection/destructive markers detected in the text (a triage signal only; the report is still forwarded). The backlog credentials and any raw error are never returned.","required":["ok","suspicious"],"properties":{"ok":{"type":"boolean"},"suspicious":{"type":"boolean"},"reason":{"type":"string","description":"Present only when ok is false."}}},"CommentRequest":{"type":"object","description":"A public review comment. `body` is required (<= 2000 chars). `email` is optional; when present it is stored masked and echoed back only in verifyHint.url. `anchor` is optional; when present it identifies the page element this comment targets.","required":["body"],"properties":{"body":{"type":"string","maxLength":2000},"email":{"type":"string"},"anchor":{"type":"object","nullable":true,"description":"Optional element anchor identifying the page element this comment targets. All sub-fields are strings; all are optional within the object.","properties":{"selector":{"type":"string","description":"CSS selector for the element, e.g. '#hero'."},"tag":{"type":"string","description":"HTML tag name of the element, e.g. 'h1'."},"text":{"type":"string","description":"Visible text content of the element, used to locate it when the selector alone is ambiguous."}}}}},"SafeComment":{"type":"object","description":"Public projection of a comment. Never includes the raw author email — only a masked form (or null). `anchor` is null for page-level comments and an object with selector/tag/text for element-anchored comments.","required":["id","body","authorMasked","verified","status","versionId","createdAt","anchor"],"properties":{"id":{"type":"string"},"body":{"type":"string"},"authorMasked":{"type":["string","null"]},"verified":{"type":"boolean"},"status":{"type":"string","enum":["open","addressed","dismissed"]},"versionId":{"type":["string","null"]},"createdAt":{"type":"string"},"anchor":{"type":["object","null"],"description":"Element anchor when the reviewer targeted a specific DOM element; null for page-level comments.","properties":{"selector":{"type":"string","description":"CSS selector for the element, e.g. '#hero'."},"tag":{"type":"string","description":"HTML tag name of the element, e.g. 'h1'."},"text":{"type":"string","description":"Visible text content of the element."}}}}},"ApproveRequest":{"type":"object","description":"Optional body for the approve endpoint. Provide `email` to record the approver; it will be masked before storage.","properties":{"email":{"type":"string"}}},"ApproveResponse":{"type":"object","required":["approved","approvedVersionId"],"properties":{"approved":{"type":"boolean","enum":[true]},"approvedVersionId":{"type":["string","null"]}}},"ClientEventRequest":{"type":"object","required":["event"],"description":"A review-funnel event from a browser surface. `event` must be one of the allow-listed client event names. `surface` is an optional label for which UI fired it.","properties":{"event":{"type":"string","enum":["review_prompt_copied"]},"surface":{"type":"string","enum":["review_widget","dashboard"]}}},"GlobalEventRequest":{"type":"object","required":["event"],"description":"A non-site-scoped client event from a browser surface (the /for-agents install hub). `event` must be one of the allow-listed global event names. `client` is clamped to a fixed allow-list of the documented install clients; any other value is recorded as \"other\".","properties":{"event":{"type":"string","enum":["config_copied","sample_viewed","sample_commented","cta_clicked"]},"client":{"type":"string","enum":["claude-desktop","claude-code","cursor","codex","windsurf","raw-mcp","curl","other"]}}},"OkResponse":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true]}}},"CommentActionResponse":{"type":"object","required":["comment"],"properties":{"comment":{"$ref":"#/components/schemas/SafeComment"}}},"CommentDeleteRequest":{"type":"object","description":"Optional body for deleting a comment. Owners authenticate with the Bearer key (no body needed). Anonymous sites provide claimToken. The comment author can retract their just-posted comment by passing the deleteToken returned at create (the on-page Undo path). Any one of the three authorizes the delete.","properties":{"claimToken":{"type":"string","description":"Claim token for moderating an anonymous site."},"deleteToken":{"type":"string","description":"The comment's single-use delete token from the create response. Authorizes deleting only this comment, valid until the comment is deleted (no time expiry)."}}},"CommentDeleteResponse":{"type":"object","required":["success","deleted"],"properties":{"success":{"type":"boolean","enum":[true]},"deleted":{"type":"string"}}},"CommentCreateResponse":{"type":"object","description":"Created comment plus a deleteToken for author self-delete and, when an email was supplied, a verifyHint url that prefills the reviewer's dashboard.","required":["comment","deleteToken"],"properties":{"comment":{"$ref":"#/components/schemas/SafeComment"},"deleteToken":{"type":"string","description":"Single-use token for deleting this comment (on-page Undo or later author self-delete). Valid until the comment is deleted (no time expiry). Shown once. DELETE the comment with {deleteToken} to consume it."},"verifyHint":{"type":"object","required":["url"],"properties":{"url":{"type":"string"}}}}},"CommentListResponse":{"type":"object","required":["slug","approved","comments"],"properties":{"slug":{"type":"string"},"approved":{"type":"boolean"},"comments":{"type":"array","items":{"$ref":"#/components/schemas/SafeComment"}}}},"ReviewPacket":{"type":"object","description":"Everything an agent needs to revise a site in one call: the site + current version, content references (the file manifest plus where to fetch the bytes — not the bytes themselves), the OPEN reviewer comments (each with its anchor + status), the computed reviewStatus, and the approved-version pointer. Owner-only data: authorize with a Bearer key, or the site's claimToken for anonymous sites.","required":["slug","site","version","contentRefs","comments","openComments","reviewStatus","approved","approvedVersionId"],"properties":{"slug":{"type":"string"},"site":{"type":"object","required":["slug","anonymous","currentVersionId","reviewEnabled","title","description"],"properties":{"slug":{"type":"string"},"anonymous":{"type":"boolean"},"currentVersionId":{"type":["string","null"]},"reviewEnabled":{"type":"boolean"},"title":{"type":["string","null"]},"description":{"type":["string","null"]}}},"version":{"type":"object","description":"The current live version and its vN label.","required":["versionId","n"],"properties":{"versionId":{"type":["string","null"]},"n":{"type":["integer","null"],"description":"Sequential version number (the vN label)."}}},"contentRefs":{"type":"object","description":"How to read the current content: the file manifest plus the URLs to fetch it. Bytes are NOT inlined — call getContentUrl (get_site_content) or open siteUrl.","required":["versionId","getContentUrl","siteUrl","files"],"properties":{"versionId":{"type":["string","null"]},"getContentUrl":{"type":"string","description":"GET this (owner Bearer / claimToken) to read the stored file bytes."},"siteUrl":{"type":"string","description":"The live served page URL."},"files":{"type":"array","description":"The current version's STORED manifest. Note: no byte size — size is enforced at upload, not stored on the version.","items":{"type":"object","required":["path","contentType"],"properties":{"path":{"type":"string","example":"index.html"},"contentType":{"type":"string","example":"text/html; charset=utf-8"},"hash":{"type":"string","description":"SHA-256 hex of the file bytes, when known."}}}}}},"comments":{"type":"array","description":"OPEN reviewer comments only (the actionable revise set); addressed/dismissed are excluded. Each carries its anchor + status.","items":{"$ref":"#/components/schemas/SafeComment"}},"openComments":{"type":"integer","minimum":0,"description":"Count of open comments (length of comments)."},"reviewStatus":{"type":"string","enum":["draft","in_review","changes_requested","approved"],"description":"Computed review-lifecycle status (same derivation as GET /api/v1/sites/{slug})."},"approved":{"type":"boolean"},"approvedVersionId":{"type":["string","null"]}}},"SiteStatus":{"type":"object","description":"Public projection of a site.","required":["slug","anonymous","expiresAt","currentVersionId","reviewEnabled","approved","openComments","reviewStatus"],"properties":{"slug":{"type":"string"},"anonymous":{"type":"boolean"},"expiresAt":{"type":["string","null"]},"currentVersionId":{"type":["string","null"]},"reviewEnabled":{"type":"boolean","description":"Whether review mode is enabled on this site."},"approved":{"type":"boolean","description":"Whether a reviewer has approved the site's current version."},"openComments":{"type":"integer","minimum":0,"description":"Number of open (unaddressed) comments. Always 0 when reviewEnabled is false."},"reviewStatus":{"type":"string","enum":["draft","in_review","changes_requested","approved"],"description":"Computed review-lifecycle status, derived from reviewEnabled, openComments, and approved (approval wins): 'draft' (review off), 'in_review' (review on, no open comments), 'changes_requested' (review on, open comments awaiting the owner/agent), 'approved'."},"title":{"type":["string","null"],"description":"Human-meaningful title, auto-derived from the page's <title>/<h1> or agent-declared; null until set."},"description":{"type":["string","null"],"description":"Human-meaningful description, auto-derived from <meta name=\"description\"> or agent-declared; null until set."},"protection":{"type":["object","null"],"description":"Page-access protection MODE only — never the password hash/salt. {mode:'password'|'email'} when the page is gated, or null when public. Lets an agent verify a page is gated.","required":["mode"],"properties":{"mode":{"type":"string","enum":["password","email"]}}}}},"OwnerSite":{"type":"object","description":"Owner projection — adds siteUrl and timestamps.","required":["slug","siteUrl","anonymous","expiresAt","currentVersionId","createdAt","updatedAt"],"properties":{"slug":{"type":"string"},"siteUrl":{"type":"string"},"anonymous":{"type":"boolean"},"expiresAt":{"type":["string","null"]},"currentVersionId":{"type":["string","null"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"title":{"type":["string","null"],"description":"Human-meaningful title, auto-derived from the page's <title>/<h1> or agent-declared; null until set."},"description":{"type":["string","null"],"description":"Human-meaningful description, auto-derived from <meta name=\"description\"> or agent-declared; null until set."}}},"OwnerSitesResponse":{"type":"object","required":["sites"],"properties":{"sites":{"type":"array","items":{"$ref":"#/components/schemas/OwnerSite"}}}},"ApiKeyInfo":{"type":"object","description":"One API key on the account. `id` is the first 12 hex chars of the key's SHA-256 (use it to revoke). Raw keys are never recoverable. `suffix` (last 4 chars of the raw key) and `createdAt` are null for legacy keys minted before key listing existed. `current` marks the key used for this request.","required":["id","name","createdAt","suffix","legacy","current"],"properties":{"id":{"type":"string","example":"1a2b3c4d5e6f"},"createdAt":{"type":["string","null"]},"suffix":{"type":["string","null"],"example":"x9Qz"},"name":{"type":["string","null"],"description":"Caller-supplied label; null for unnamed keys.","example":"cursor"},"legacy":{"type":"boolean"},"current":{"type":"boolean"}}},"CreateKeyRequest":{"type":"object","properties":{"name":{"type":"string","description":"Optional label for the key (e.g. the tool using it). Capped at 60 chars; omit for an unnamed key.","example":"cursor"}}},"CreateKeyResponse":{"type":"object","required":["apiKey","id","name"],"properties":{"apiKey":{"type":"string","description":"The raw key — shown once. Send as Bearer token."},"id":{"type":"string","example":"1a2b3c4d5e6f"},"name":{"type":["string","null"],"example":"cursor"}}},"AccountResponse":{"type":"object","required":["accountId","email","createdAt","siteCount","keyCount"],"properties":{"accountId":{"type":"string","example":"acc_1a2b3c4d5e6f"},"email":{"type":"string","format":"email","example":"you@example.com"},"createdAt":{"type":"string","format":"date-time","example":"2026-06-12T12:00:00.000Z"},"siteCount":{"type":"integer","example":3},"keyCount":{"type":"integer","example":1}}},"ListKeysResponse":{"type":"object","required":["keys"],"properties":{"keys":{"type":"array","description":"Newest-first; legacy keys last.","items":{"$ref":"#/components/schemas/ApiKeyInfo"}}}},"RevokeKeyResponse":{"type":"object","required":["success","revoked","selfRevoked"],"properties":{"success":{"type":"boolean","enum":[true]},"revoked":{"type":"string","example":"1a2b3c4d5e6f"},"selfRevoked":{"type":"boolean","description":"True when you revoked the key authorizing this request (it no longer works afterward)."}}},"RequestCodeRequest":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","example":"you@example.com"}}},"RequestCodeResponse":{"type":"object","required":["ok","message"],"properties":{"ok":{"type":"boolean","enum":[true]},"message":{"type":"string"},"devCode":{"type":"string","description":"Only present in dev (AUTH_DEV_RETURN_CODE=true); never in prod."}}},"VerifyCodeRequest":{"type":"object","required":["email","code"],"properties":{"email":{"type":"string","format":"email","example":"you@example.com"},"code":{"type":"string","example":"482913"},"name":{"type":"string","description":"Optional label for the key minted on success (capped at 60 chars).","example":"cursor"}}},"VerifyCodeResponse":{"type":"object","required":["apiKey","accountId","email","accountCreated"],"properties":{"apiKey":{"type":"string","description":"Send as Bearer token."},"accountId":{"type":"string","example":"acc_1a2b3c4d5e6f7a8b"},"email":{"type":"string","format":"email"},"accountCreated":{"type":"boolean","description":"True if this sign-in created a new account; false for a returning account."}}},"PairStartRequest":{"type":"object","properties":{"name":{"type":"string","description":"Optional label for the key minted on approval (capped at 60 chars).","example":"cursor"}}},"PairStartResponse":{"type":"object","required":["pairingId","deviceSecret","userCode","verificationUrl","verificationUrlComplete","pollIntervalSeconds","expiresInSeconds"],"properties":{"pairingId":{"type":"string","example":"pr_8f3c1d2e4b5a6978"},"deviceSecret":{"type":"string","description":"Secret proving ownership of this pairing on poll.","example":"ds_1a2b3c4d5e6f7a8b"},"userCode":{"type":"string","description":"Short code the user enters at the verification URL.","example":"WDJB-MJHT"},"verificationUrl":{"type":"string","example":"https://agentpub.io/pair"},"verificationUrlComplete":{"type":"string","example":"https://agentpub.io/pair?code=WDJB-MJHT"},"pollIntervalSeconds":{"type":"integer","example":3},"expiresInSeconds":{"type":"integer","example":600}}},"PairApproveRequest":{"type":"object","required":["userCode"],"properties":{"userCode":{"type":"string","example":"WDJB-MJHT"},"name":{"type":"string","description":"Optional label for the key minted on approval (capped at 60 chars).","example":"cursor"},"deny":{"type":"boolean","description":"Set true to deny the pairing instead of approving."}}},"PairApproveResponse":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true]}}},"PairPollRequest":{"type":"object","required":["pairingId","deviceSecret"],"properties":{"pairingId":{"type":"string","example":"pr_8f3c1d2e4b5a6978"},"deviceSecret":{"type":"string","example":"ds_1a2b3c4d5e6f7a8b"}}},"PairPollResponse":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["pending","approved","denied","expired","consumed"],"description":"Poll outcome. Only 'approved' carries the minted key fields."},"apiKey":{"type":"string","description":"Present only when approved — send as Bearer token."},"keyId":{"type":"string","example":"1a2b3c4d5e6f"},"keyName":{"type":["string","null"],"example":"cursor"}}}}},"paths":{"/api/auth/agent/request-code":{"post":{"summary":"Email a one-time sign-in code","description":"Sends a sign-in code to the email. Step 1 of API-key issuance.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestCodeRequest"},"example":{"email":"you@example.com"}}}},"responses":{"200":{"description":"Code sent (or generated if email is not configured).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestCodeResponse"},"example":{"ok":true,"message":"A sign-in code was emailed. Paste it into verify-code."}}}},"400":{"description":"a valid email is required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"a valid email is required"}}}},"429":{"description":"a code was just sent; wait before requesting another","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"a code was just sent; wait before requesting another"}}}}}}},"/api/auth/agent/verify-code":{"post":{"summary":"Verify a code and receive an API key","description":"Exchanges a valid sign-in code for a permanent API key. Step 2.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyCodeRequest"},"example":{"email":"you@example.com","code":"482913"}}}},"responses":{"200":{"description":"API key issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyCodeResponse"},"example":{"apiKey":"ap_live_8f3c1d2e4b5a6978","accountId":"acc_1a2b3c4d5e6f7a8b","email":"you@example.com","accountCreated":true}}}},"400":{"description":"no pending code; request a new one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"no pending code; request a new one"}}}},"401":{"description":"invalid code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"invalid code"}}}},"429":{"description":"too many attempts; request a new code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"too many attempts; request a new code"}}}}}}},"/api/v1/pair/start":{"post":{"summary":"Start a device pairing and get a user code","description":"Begins a device-pairing flow. Returns a pairingId + deviceSecret (kept by the device) and a short userCode the user enters at the verification URL. No auth required.","security":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PairStartRequest"},"example":{"name":"cursor"}}}},"responses":{"200":{"description":"Pairing started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PairStartResponse"},"example":{"pairingId":"pr_8f3c1d2e4b5a6978","deviceSecret":"ds_1a2b3c4d5e6f7a8b","userCode":"WDJB-MJHT","verificationUrl":"https://agentpub.io/pair","verificationUrlComplete":"https://agentpub.io/pair?code=WDJB-MJHT","pollIntervalSeconds":3,"expiresInSeconds":600}}}},"429":{"description":"pair/start rate limit exceeded; retry later","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"pair/start rate limit exceeded; retry later"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}}}},"/api/v1/pair/approve":{"post":{"summary":"Approve (or deny) a device pairing","description":"Approves the pairing for the given userCode, binding it to the authenticated account so the device can claim a key. Set deny:true to reject instead. Bearer key required.","security":[{"bearerApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PairApproveRequest"},"example":{"userCode":"WDJB-MJHT","name":"cursor"}}}},"responses":{"200":{"description":"Pairing approved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PairApproveResponse"},"example":{"ok":true}}}},"400":{"description":"userCode is required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"userCode is required"}}}},"401":{"description":"authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required"}}}},"404":{"description":"no such pairing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such pairing"}}}}}}},"/api/v1/pair/poll":{"post":{"summary":"Poll a device pairing for its outcome","description":"The device polls with its pairingId + deviceSecret until the pairing is approved (returns a minted apiKey), denied, expired, or consumed. No auth required; the deviceSecret authorizes the poll.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PairPollRequest"},"example":{"pairingId":"pr_8f3c1d2e4b5a6978","deviceSecret":"ds_1a2b3c4d5e6f7a8b"}}}},"responses":{"200":{"description":"Current pairing status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PairPollResponse"},"example":{"status":"approved","apiKey":"ap_live_8f3c1d2e4b5a6978","keyId":"1a2b3c4d5e6f","keyName":"cursor"}}}},"400":{"description":"pairingId and deviceSecret are required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"pairingId and deviceSecret are required"}}}},"403":{"description":"invalid device secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"invalid device secret"}}}},"429":{"description":"polling too fast; retry later","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"slow_down","code":"slow_down","message":"polling too fast; retry later"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}}}},"/api/v1/publish":{"post":{"summary":"Create a site (anonymous, or owned with Bearer auth)","description":"Creates a new site and a pending version. Omit Authorization for an anonymous 24h site (returns a one-time claimToken); send a Bearer key for a permanent owned site. Returns presigned upload URLs; PUT each, then POST finalizeUrl.","security":[{"bearerApiKey":[]},{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishManifest"},"example":{"files":[{"path":"index.html","size":1024,"contentType":"text/html; charset=utf-8"}]}}}},"responses":{"201":{"description":"Created; pending finalize.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishResponse"},"example":{"slug":"calm-saddle-7h2k","siteUrl":"https://calm-saddle-7h2k.agentpub.io/","status":"pending","requiresFinalize":true,"action":"create","name":null,"authenticated":false,"anonymous":true,"expiresAt":"2026-06-13T12:00:00.000Z","upload":{"versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","uploads":[{"path":"index.html","method":"PUT","url":"https://r2.example/sites/calm-saddle-7h2k/v/01HV4Z3K7Q9X2M8N6P0R5T1W3Y/index.html?sig=..."}],"carried":[],"finalizeUrl":"https://agentpub.io/api/v1/publish/calm-saddle-7h2k/finalize","expiresInSeconds":3600},"claimToken":"ct_9z8y7x6w5v4u3t2s","claimUrl":"https://agentpub.io/claim?slug=calm-saddle-7h2k&token=ct_9z8y7x6w5v4u3t2s","warning":"Save claimToken and claimUrl now — they are shown only once."}}}},"400":{"description":"files must be a non-empty array","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"files must be a non-empty array"}}}},"401":{"description":"naming a site requires an account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"naming a site requires an account"}}}},"403":{"description":"account site limit reached (100); delete sites to free quota","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"quota_exceeded","code":"quota_exceeded","message":"account site limit reached (100); delete sites to free quota"}}}},"409":{"description":"you already have a site named \"ads-daily\"; publish to it by name to update it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"conflict","code":"conflict","message":"you already have a site named \"ads-daily\"; publish to it by name to update it"}}}},"429":{"description":"anonymous publish rate limit reached; retry in 60s or use an API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"anonymous publish rate limit reached; retry in 60s or use an API key"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}},"503":{"description":"Service unavailable — either R2 upload credentials are not configured (storage_not_configured) or a unique slug could not be allocated after retries (service_unavailable). Both are transient; retry with exponential backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"storage_not_configured":{"summary":"R2 credentials missing","value":{"error":"storage_not_configured","code":"storage_not_configured","message":"R2 upload credentials are not configured"}},"service_unavailable":{"summary":"Slug allocation exhausted","value":{"error":"service_unavailable","code":"service_unavailable","message":"could not allocate a unique slug"}}}}}}}}},"/api/v1/publish/{slug}":{"put":{"summary":"Update an existing site (owner key or claim token)","description":"Creates a new pending version for an existing site. Owner sites require the Bearer key; anonymous sites require claimToken in the body. Files whose hash matches the live version are carried (no presigned URL) and listed under upload.carried.","security":[{"bearerApiKey":[]},{}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishManifest"},"example":{"files":[{"path":"index.html","size":2048,"contentType":"text/html; charset=utf-8","hash":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}],"claimToken":"ct_9z8y7x6w5v4u3t2s"}}}},"responses":{"200":{"description":"Updated; pending finalize.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishResponse"},"example":{"slug":"calm-saddle-7h2k","siteUrl":"https://calm-saddle-7h2k.agentpub.io/","status":"pending","requiresFinalize":true,"action":"update","name":null,"authenticated":true,"anonymous":false,"expiresAt":null,"upload":{"versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","uploads":[{"path":"index.html","method":"PUT","url":"https://r2.example/sites/calm-saddle-7h2k/v/01HV4Z3K7Q9X2M8N6P0R5T1W3Y/index.html?sig=..."}],"carried":[{"path":"logo.png"}],"finalizeUrl":"https://agentpub.io/api/v1/publish/calm-saddle-7h2k/finalize","expiresInSeconds":3600}}}}},"400":{"description":"invalid slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"invalid slug"}}}},"401":{"description":"authentication required to update this site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required to update this site"}}}},"403":{"description":"not your site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"not your site"}}}},"404":{"description":"no such site to update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such site to update"}}}},"503":{"description":"R2 upload credentials are not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"storage_not_configured","code":"storage_not_configured","message":"R2 upload credentials are not configured"}}}}}},"delete":{"summary":"Delete a site (owner key, claim token, or operator admin key)","description":"Deletes a site and all its versions. Owner: Bearer key. Anonymous: claimToken in body. Operator: x-admin-key header.","security":[{"bearerApiKey":[]},{}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteRequest"},"example":{"claimToken":"ct_9z8y7x6w5v4u3t2s"}}}},"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"},"example":{"success":true,"slug":"calm-saddle-7h2k"}}}},"400":{"description":"invalid slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"invalid slug"}}}},"401":{"description":"authentication required to update this site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required to update this site"}}}},"403":{"description":"not your site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"not your site"}}}},"404":{"description":"no such site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such site"}}}}}}},"/api/v1/publish/{slug}/patch":{"post":{"summary":"Overlay files onto a site (incremental update)","description":"Seeds the file list from the current version's manifest, overlays the provided files by path, then runs the normal version flow: unchanged files carry forward (no presigned URL, listed under upload.carried), provided files upload. Unmentioned files are kept (patch never deletes). Finalize with the returned versionId via the /finalize endpoint. Owner: Bearer key. Anonymous: claimToken in body.","security":[{"bearerApiKey":[]},{}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishManifest"},"example":{"files":[{"path":"data.json","size":512,"contentType":"application/json","hash":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}],"claimToken":"ct_9z8y7x6w5v4u3t2s"}}}},"responses":{"200":{"description":"Patched; pending finalize.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishResponse"},"example":{"slug":"calm-saddle-7h2k","siteUrl":"https://calm-saddle-7h2k.agentpub.io/","status":"pending","requiresFinalize":true,"action":"update","name":"ads-daily","authenticated":true,"anonymous":false,"expiresAt":null,"upload":{"versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","uploads":[{"path":"data.json","method":"PUT","url":"https://r2.example/sites/calm-saddle-7h2k/v/01HV4Z3K7Q9X2M8N6P0R5T1W3Y/data.json?sig=..."}],"carried":[{"path":"index.html"}],"finalizeUrl":"https://agentpub.io/api/v1/publish/calm-saddle-7h2k/finalize","expiresInSeconds":3600}}}}},"400":{"description":"invalid slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"invalid slug"}}}},"401":{"description":"authentication required to update this site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required to update this site"}}}},"403":{"description":"not your site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"not your site"}}}},"404":{"description":"no such site to patch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such site to patch"}}}},"503":{"description":"R2 upload credentials are not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"storage_not_configured","code":"storage_not_configured","message":"R2 upload credentials are not configured"}}}}}}},"/api/v1/publish/{slug}/finalize":{"post":{"summary":"Make an uploaded version live","description":"Verifies every uploaded file landed, copies carried files into the new version, then flips the live pointer. No auth — possession of the versionId from a successful create/update is the capability.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinalizeRequest"},"example":{"versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y"}}}},"responses":{"200":{"description":"Version is live.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinalizeResponse"},"example":{"success":true,"slug":"calm-saddle-7h2k","siteUrl":"https://calm-saddle-7h2k.agentpub.io/","currentVersionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","previousVersionId":null,"persistence":"expires_24h","expiresAt":"2026-06-13T12:00:00.000Z"}}}},"400":{"description":"versionId is required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"versionId is required"}}}},"404":{"description":"no pending version (it may have expired)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no pending version (it may have expired)"}}}},"409":{"description":"file not uploaded: index.html","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"conflict","code":"conflict","message":"file not uploaded: index.html"}}}}}}},"/api/v1/publish/by-name/{name}":{"get":{"summary":"Resolve an account-scoped name to its slug (owner only)","description":"Looks up the slug the authenticated account published under this name. The name index is account-scoped, so a Bearer key is required.","security":[{"bearerApiKey":[]}],"parameters":[{"name":"name","in":"path","required":true,"description":"Account-scoped site name, e.g. ads-daily.","schema":{"type":"string"},"example":"ads-daily"}],"responses":{"200":{"description":"Name resolved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ByNameResponse"},"example":{"slug":"calm-saddle-7h2k","name":"ads-daily"}}}},"400":{"description":"name is required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"name is required"}}}},"401":{"description":"authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required"}}}},"404":{"description":"no site with that name","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no site with that name"}}}}}}},"/api/v1/publish/{slug}/versions":{"get":{"summary":"List a site's finalized versions, newest-first, with the live version flagged","description":"Owner sites require the Bearer key; anonymous sites require the claimToken query param (GETs carry no body).","security":[{"bearerApiKey":[]},{}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"},{"name":"claimToken","in":"query","required":false,"description":"Claim token for anonymous sites.","schema":{"type":"string"}}],"responses":{"200":{"description":"Versions list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionsResponse"},"example":{"slug":"calm-saddle-7h2k","versions":[{"versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","n":2,"finalizedAt":"2026-06-12T12:00:00.000Z","current":true},{"versionId":"01HV4Z0A1B2C3D4E5F6G7H8J9K","n":1,"finalizedAt":"2026-06-12T11:00:00.000Z","current":false}]}}}},"400":{"description":"invalid slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"invalid slug"}}}},"401":{"description":"authentication required to update this site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required to update this site"}}}},"403":{"description":"claimToken required to update an anonymous site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"claimToken required to update an anonymous site"}}}},"404":{"description":"no such site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such site"}}}}}}},"/api/v1/publish/{slug}/content":{"get":{"summary":"Read back a site's stored file contents (owner only)","description":"Returns the file bytes of a version (default: the live version). Pass `path` for one file, or `version` (a vN handle or versionId) for a prior version. Text files come back as utf8 content, binary files as base64. With no `path`, a version over the read cap returns a file list (path/size/contentType) with truncated=true. Owner sites require the Bearer key; anonymous sites require the claimToken query param.","security":[{"bearerApiKey":[]},{}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"},{"name":"path","in":"query","required":false,"description":"Single file to read. Omit to read all files.","schema":{"type":"string"},"example":"index.html"},{"name":"version","in":"query","required":false,"description":"Version to read: a vN handle (e.g. v2) or a versionId. Defaults to the live version.","schema":{"type":"string"},"example":"v1"},{"name":"claimToken","in":"query","required":false,"description":"Claim token for anonymous sites.","schema":{"type":"string"}}],"responses":{"200":{"description":"File contents.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentResponse"},"example":{"slug":"calm-saddle-7h2k","versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","files":[{"path":"index.html","contentType":"text/html; charset=utf-8","encoding":"utf8","content":"<!doctype html><h1>dashboard</h1>"}]}}}},"400":{"description":"invalid slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"invalid slug"}}}},"401":{"description":"authentication required to update this site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required to update this site"}}}},"403":{"description":"not your site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"not your site"}}}},"404":{"description":"no such version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such version"}}}}}}},"/api/v1/publish/{slug}/rollback":{"post":{"summary":"Roll the live pointer back to a prior version","description":"Flips the live pointer to a prior finalized version in one call. Owner: Bearer key. Anonymous: claimToken in body.","security":[{"bearerApiKey":[]},{}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RollbackRequest"},"example":{"versionId":"01HV4Z0A1B2C3D4E5F6G7H8J9K"}}}},"responses":{"200":{"description":"Rolled back.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RollbackResponse"},"example":{"success":true,"slug":"calm-saddle-7h2k","siteUrl":"https://calm-saddle-7h2k.agentpub.io/","currentVersionId":"01HV4Z0A1B2C3D4E5F6G7H8J9K","previousVersionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y"}}}},"400":{"description":"versionId is required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"versionId is required"}}}},"401":{"description":"authentication required to update this site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required to update this site"}}}},"403":{"description":"not your site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"not your site"}}}},"404":{"description":"no such version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such version"}}}}}}},"/api/v1/publish/{slug}/claim":{"post":{"summary":"Adopt an anonymous site into the authenticated account","description":"An authenticated account adopts an anonymous site via its claim token, making it permanent. Bearer key required.","security":[{"bearerApiKey":[]}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimRequest"},"example":{"claimToken":"ct_9z8y7x6w5v4u3t2s"}}}},"responses":{"200":{"description":"Claimed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimResponse"},"example":{"success":true,"slug":"calm-saddle-7h2k","siteUrl":"https://calm-saddle-7h2k.agentpub.io/","ownerAccountId":"acc_1a2b3c4d5e6f7a8b"}}}},"400":{"description":"claimToken is required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"claimToken is required"}}}},"401":{"description":"an API key is required to claim a site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"an API key is required to claim a site"}}}},"403":{"description":"invalid claim token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"invalid claim token"}}}},"404":{"description":"no such site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such site"}}}},"409":{"description":"site is already owned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"conflict","code":"conflict","message":"site is already owned"}}}}}}},"/api/v1/publish/{slug}/review":{"post":{"summary":"Enable or disable review mode for a site","description":"Toggles the review gate on a site. Owner Bearer key OR anonymous site's claimToken in the body. Pass {\"enabled\":true} to turn on review mode; false to turn it off. Anonymous publishers can enable review before claiming by including their claimToken in the body.","security":[{"bearerApiKey":[]},{}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewToggleRequest"},"example":{"enabled":true}}}},"responses":{"200":{"description":"Review mode updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewToggleResponse"},"example":{"slug":"calm-saddle-7h2k","reviewEnabled":true}}}},"400":{"description":"enabled must be a boolean","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"enabled must be a boolean"}}}},"401":{"description":"authentication required to update this site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required to update this site"}}}},"403":{"description":"claimToken required to update an anonymous site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"claimToken required to update an anonymous site"}}}},"404":{"description":"no such site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such site"}}}}}}},"/api/v1/publish/{slug}/protect":{"post":{"summary":"Set or clear page access protection for a site","description":"Protects a CLAIMED page behind a shared password or an email gate, or clears protection. Owner Bearer key OR anonymous site's claimToken authorizes the call, but protection itself is available on claimed sites only. {\"mode\":\"password\",\"password\":\"...\"} sets a password (hashed server-side, never returned); {\"mode\":\"email\"} sets an email gate; {\"mode\":\"off\"} removes it. The response carries only the active mode — never the hash/salt.","security":[{"bearerApiKey":[]},{}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProtectRequest"},"example":{"mode":"password","password":"s3cret-pass"}}}},"responses":{"200":{"description":"Protection state updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProtectResponse"},"example":{"slug":"calm-saddle-7h2k","protection":{"mode":"password"}}}}},"400":{"description":"protection is available on claimed sites only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"protection is available on claimed sites only"}}}},"401":{"description":"authentication required to update this site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required to update this site"}}}},"403":{"description":"claimToken required to update an anonymous site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"claimToken required to update an anonymous site"}}}},"404":{"description":"no such site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such site"}}}}}}},"/api/v1/sites/{slug}/leads":{"get":{"summary":"List the captured leads for an email-gated site (owner)","description":"Returns the emails captured by an email-mode page gate, newest-first. OWNER-ONLY: the authenticated account must own the site (401 without auth, 403 if not the owner). Raw emails are returned by design — this is the owner's own lead list. Default is JSON; pass ?format=csv to download a text/csv file with an `email,capturedAt` header row and one row per lead (RFC 4180 escaping).","security":[{"bearerApiKey":[]}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"},{"name":"format","in":"query","required":false,"description":"Pass \"csv\" for a downloadable text/csv export; omit for JSON.","schema":{"type":"string","enum":["csv"]},"example":"csv"}],"responses":{"200":{"description":"Captured leads. JSON by default; text/csv when format=csv.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeadsResponse"},"example":{"slug":"calm-saddle-7h2k","leads":[{"email":"ada@example.com","capturedAt":"2026-06-12T12:05:00.000Z"},{"email":"grace@example.com","capturedAt":"2026-06-12T11:00:00.000Z"}]}},"text/csv":{"schema":{"type":"string"},"example":"email,capturedAt\r\nada@example.com,2026-06-12T12:05:00.000Z\r\ngrace@example.com,2026-06-12T11:00:00.000Z"}}},"401":{"description":"authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required"}}}},"403":{"description":"not your site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"not your site"}}}},"404":{"description":"no such site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such site"}}}}}}},"/api/v1/account":{"get":{"summary":"Get the authenticated account's profile","description":"Returns the account's id, email, createdAt, and current site and key counts. No secrets are returned. Bearer key required.","security":[{"bearerApiKey":[]}],"responses":{"200":{"description":"Account profile.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"},"example":{"accountId":"acc_1a2b3c4d5e6f","email":"you@example.com","createdAt":"2026-06-12T12:00:00.000Z","siteCount":3,"keyCount":1}}}},"401":{"description":"authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required"}}}}}},"delete":{"summary":"Delete the account and all its data (irreversible)","description":"Permanently deletes the authenticated account and every site it owns (files, versions, review data, and site records), all the account's API keys, and the account record itself. This cannot be undone and after it succeeds the Bearer key no longer authenticates. No other account is affected. Bearer key required. An OPTIONAL deletion-feedback survey body may be sent; it is recorded for analytics and never affects deletion.","security":[{"bearerApiKey":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAccountRequest"},"example":{"reason":"too_hard","feedback":"the API confused me"}}}},"responses":{"200":{"description":"Account and all data deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAccountResponse"},"example":{"success":true}}}},"401":{"description":"authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required"}}}}}}},"/api/v1/keys":{"get":{"summary":"List the authenticated account's API keys","description":"Lists the account's API keys (id, createdAt, masked suffix), newest-first, with the request's own key flagged current. Raw keys are unrecoverable — only hashes are stored. Bearer key required.","security":[{"bearerApiKey":[]}],"responses":{"200":{"description":"API keys.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListKeysResponse"},"example":{"keys":[{"id":"1a2b3c4d5e6f","name":"cursor","createdAt":"2026-06-12T12:00:00.000Z","suffix":"x9Qz","legacy":false,"current":true}]}}}},"401":{"description":"authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required"}}}}}},"post":{"summary":"Mint an additional API key","description":"Creates a new key on the authenticated account, optionally labelled with a name. The raw key is returned once and is unrecoverable afterward. Bearer key required.","security":[{"bearerApiKey":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyRequest"},"example":{"name":"cursor"}}}},"responses":{"201":{"description":"Key created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyResponse"},"example":{"apiKey":"ap_live_…","id":"1a2b3c4d5e6f","name":"cursor"}}}},"400":{"description":"name must be a string","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"name must be a string"}}}},"401":{"description":"authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required"}}}}}}},"/api/v1/keys/{id}":{"delete":{"summary":"Revoke an API key","description":"Revokes the key with this id (the first 12+ hex chars of its SHA-256, from GET /api/v1/keys). Revoking the key used for this request is allowed and logs you out (selfRevoked: true). 404s if the id matches no key on this account. Bearer key required.","security":[{"bearerApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Key id from GET /api/v1/keys (12+ hex chars).","schema":{"type":"string","pattern":"^[0-9a-f]{12,}$"},"example":"1a2b3c4d5e6f"}],"responses":{"200":{"description":"Key revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeKeyResponse"},"example":{"success":true,"revoked":"1a2b3c4d5e6f","selfRevoked":false}}}},"400":{"description":"invalid key id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"invalid key id"}}}},"401":{"description":"authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required"}}}},"404":{"description":"no such key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such key"}}}}}}},"/api/v1/sites":{"get":{"summary":"List the authenticated account's owned sites","description":"Bearer key required.","security":[{"bearerApiKey":[]}],"responses":{"200":{"description":"Owned sites.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OwnerSitesResponse"},"example":{"sites":[{"slug":"calm-saddle-7h2k","siteUrl":"https://calm-saddle-7h2k.agentpub.io/","anonymous":false,"expiresAt":null,"currentVersionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","createdAt":"2026-06-12T10:00:00.000Z","updatedAt":"2026-06-12T12:00:00.000Z"}]}}}},"401":{"description":"authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required"}}}}}}},"/api/v1/sites/{slug}":{"get":{"summary":"Public site status","description":"Existence, anonymous flag, expiry, and live version. Rate-limited by IP; no auth required.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"responses":{"200":{"description":"Site status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteStatus"},"example":{"slug":"calm-saddle-7h2k","anonymous":true,"expiresAt":"2026-06-13T12:00:00.000Z","currentVersionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","reviewEnabled":false,"approved":false,"openComments":0,"reviewStatus":"draft"}}}},"400":{"description":"invalid slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"invalid slug"}}}},"404":{"description":"no such site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such site"}}}},"429":{"description":"rate limit exceeded; retry in 60s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"rate limit exceeded; retry in 60s"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}}}},"/api/v1/sites/{slug}/refresh-meta":{"post":{"summary":"Re-extract a site's title/description (owner only)","description":"Re-reads the current live version's index.html and overwrites the site's stored title/description from its <title> (or first <h1>) and <meta name=\"description\">. Owner Bearer key, or a matching claimToken for an anonymous site. Rate-limited by IP.","parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"claimToken":{"type":"string"}}},"example":{"claimToken":"anon-claim-token"}}}},"responses":{"200":{"description":"Updated title/description.","content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":["string","null"]},"description":{"type":["string","null"]}},"required":["title","description"]},"example":{"title":"Q3 Investor Update","description":"A deck for Acme"}}}},"400":{"description":"invalid slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"invalid slug"}}}},"401":{"description":"authentication required to update this site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required to update this site"}}}},"403":{"description":"not your site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"not your site"}}}},"404":{"description":"no such site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such site"}}}},"429":{"description":"rate limit exceeded; retry in 60s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"rate limit exceeded; retry in 60s"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}}}},"/api/v1/sites/{slug}/review/approve":{"post":{"summary":"Approve a review-enabled site (public, rate-limited)","description":"Any reviewer can signal approval on a review-enabled site. Public, unauthenticated, rate-limited by IP. Optional `email` is stored masked (never returned raw). Idempotent — re-approving overwrites the state with a fresh timestamp and version. Returns CORS headers.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApproveRequest"},"example":{"email":"approver@example.com"}}}},"responses":{"200":{"description":"Approved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApproveResponse"},"example":{"approved":true,"approvedVersionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y"}}}},"400":{"description":"email is not a valid email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"email is not a valid email"}}}},"404":{"description":"no such review site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such review site"}}}},"429":{"description":"rate limit exceeded; retry in 60s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"rate limit exceeded; retry in 60s"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}}}},"/api/v1/events":{"post":{"summary":"Record a global client event (public, allow-listed)","description":"Inert, bounded funnel beacon for the /for-agents install hub (non-site-scoped). Records ONE of a fixed allow-list of global event names (currently only `config_copied`) server-side; any other name is rejected. `client` is clamped to the documented install clients (else \"other\"). No other side effect. Public, rate-limited by IP. Same-origin (served from the apex), so no CORS.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GlobalEventRequest"},"example":{"event":"config_copied","client":"cursor"}}}},"responses":{"200":{"description":"Event recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResponse"},"example":{"ok":true}}}},"400":{"description":"unknown event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"unknown event"}}}},"429":{"description":"rate limit exceeded; retry in 60s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"rate limit exceeded; retry in 60s"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}}}},"/api/v1/sites/{slug}/events":{"post":{"summary":"Record a review-funnel event (public, allow-listed)","description":"Inert, bounded funnel beacon fired by the browser review surfaces (on-page widget + dashboard). Records ONE of a fixed allow-list of client event names (currently only `review_prompt_copied`) server-side; any other name is rejected. No other side effect. Public, rate-limited by IP, requires review mode. CORS-enabled for the cross-origin on-page widget.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientEventRequest"},"example":{"event":"review_prompt_copied","surface":"review_widget"}}}},"responses":{"200":{"description":"Event recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResponse"},"example":{"ok":true}}}},"400":{"description":"unknown event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"unknown event"}}}},"404":{"description":"no such review site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such review site"}}}},"429":{"description":"rate limit exceeded; retry in 60s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"rate limit exceeded; retry in 60s"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}}}},"/api/v1/sites/{slug}/comments/{id}/addressed":{"post":{"summary":"Mark a comment as addressed (owner only)","description":"Owner Bearer key required. Flips the comment status to 'addressed'. Does not require review mode to be enabled — owners can moderate regardless. Idempotent: re-addressing updates the addressedAt timestamp.","security":[{"bearerApiKey":[]}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"},{"name":"id","in":"path","required":true,"description":"Comment id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Comment addressed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentActionResponse"},"example":{"comment":{"id":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","body":"The hero copy buries the call to action.","authorMasked":"r***@example.com","verified":false,"status":"addressed","versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","createdAt":"2026-06-13T12:00:00.000Z","anchor":{"selector":"#hero","tag":"h1","text":"Quarterly Strategy Brief"}}}}}},"401":{"description":"authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required"}}}},"403":{"description":"not your site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"not your site"}}}},"404":{"description":"comment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"comment not found"}}}}}}},"/api/v1/sites/{slug}/comments/{id}/dismiss":{"post":{"summary":"Dismiss a comment (owner or claim token)","description":"Owner Bearer key required (or claimToken for anonymous sites). Flips the comment status to 'dismissed' (won't-fix) and sets dismissedAt. Unlike addressed, sends NO email to the reviewer. Does not require review mode to be enabled. Rate-limited by IP.","security":[{"bearerApiKey":[]},{}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"},{"name":"id","in":"path","required":true,"description":"Comment id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Comment dismissed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentActionResponse"},"example":{"comment":{"id":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","body":"The hero copy buries the call to action.","authorMasked":"r***@example.com","verified":false,"status":"dismissed","versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","createdAt":"2026-06-13T12:00:00.000Z","anchor":{"selector":"#hero","tag":"h1","text":"Quarterly Strategy Brief"}}}}}},"401":{"description":"authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required"}}}},"403":{"description":"not your site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"not your site"}}}},"404":{"description":"comment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"comment not found"}}}}}}},"/api/v1/sites/{slug}/comments/{id}":{"delete":{"summary":"Delete a comment (owner, claim token, or delete token)","description":"Permanently deletes the comment. Authorize with ANY of: the owner Bearer key, the site's claimToken (anonymous sites), or the comment's single-use deleteToken from the create response (the author self-delete path; valid until the comment is deleted). Does not require review mode to be enabled. Rate-limited by IP.","security":[{"bearerApiKey":[]},{}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"},{"name":"id","in":"path","required":true,"description":"Comment id.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentDeleteRequest"},"example":{"deleteToken":"cdt_9z8y7x6w5v4u3t2s"}}}},"responses":{"200":{"description":"Comment deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentDeleteResponse"},"example":{"success":true,"deleted":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y"}}}},"401":{"description":"authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required"}}}},"403":{"description":"not your site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"not your site"}}}},"404":{"description":"comment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"comment not found"}}}},"429":{"description":"comment mutation rate limit exceeded; retry shortly","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"comment mutation rate limit exceeded; retry shortly"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}}}},"/api/v1/issues":{"post":{"summary":"Log a structured friction report to the backlog","description":"Open to any agent. Forwards ONE validated, capped, INERT friction report (problem + optional proposed fix) to agentpub's external backlog. This is the endpoint's entire capability — it records the report and does nothing else: no action is triggered and `proposed_solution` is captured as a suggestion, never executed. Rate-limited per IP. Send a Bearer key to attribute the report to your account; otherwise it is logged anonymously. The response is a soft envelope: ok:false (still HTTP 200) when the backlog is unconfigured or the forward failed, so it is never a crash.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssueReportRequest"},"example":{"title":"publish_site returns 500 on empty files","category":"publish","description":"Calling publish_site with files:[] crashes with a 500.","severity":"major","repro":"1. call publish_site with files:[] 2. observe a 500","proposed_solution":"Validate files is non-empty and return 400.","surface":"calm-saddle-7h2k"}}}},"responses":{"200":{"description":"Report processed (ok:true forwarded; ok:false soft failure).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssueReportResponse"},"example":{"ok":true,"suspicious":false}}}},"400":{"description":"title is required and must be a string","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"title is required and must be a string"}}}},"429":{"description":"issue report rate limit exceeded; retry in 60s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"issue report rate limit exceeded; retry in 60s"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}}}},"/api/v1/sites/{slug}/comments":{"post":{"summary":"Post a review comment to a review-enabled site (public)","description":"Public, unauthenticated. Submits a comment on a site that has review mode on. Rate-limited by IP. Body is required (<= 2000 chars). An optional reviewer email is stored masked and echoed back only inside verifyHint.url. Comments never expose a raw email. The 201 response includes a single-use deleteToken for author self-delete, valid until the comment is deleted (DELETE the comment with {deleteToken}). Sites that are missing, anonymous, or not review-enabled return a uniform 404.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentRequest"},"example":{"body":"The hero copy buries the call to action.","email":"reviewer@example.com","anchor":{"selector":"#hero","tag":"h1","text":"Quarterly Strategy Brief"}}}}},"responses":{"201":{"description":"Comment created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentCreateResponse"},"example":{"comment":{"id":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","body":"The hero copy buries the call to action.","authorMasked":"r***@example.com","verified":false,"status":"open","versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","createdAt":"2026-06-13T12:00:00.000Z","anchor":{"selector":"#hero","tag":"h1","text":"Quarterly Strategy Brief"}},"deleteToken":"cdt_9z8y7x6w5v4u3t2s","verifyHint":{"url":"https://agentpub.io/dashboard?email=reviewer%40example.com"}}}}},"400":{"description":"body is required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"body is required"}}}},"404":{"description":"no such review site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such review site"}}}},"429":{"description":"comment rate limit exceeded; retry in 60s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"comment rate limit exceeded; retry in 60s"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}}},"get":{"summary":"List a review site's comments, newest-first (public)","description":"Public, unauthenticated. Returns every comment (any status) for a review-enabled site, newest-first, plus the site's approval flag. Rate-limited by IP. Raw author emails are never returned. Sites that are missing, anonymous, or not review-enabled return 404.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"}],"responses":{"200":{"description":"Comments and approval state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentListResponse"},"example":{"slug":"calm-saddle-7h2k","approved":false,"comments":[{"id":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","body":"The hero copy buries the call to action.","authorMasked":"r***@example.com","verified":false,"status":"open","versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","createdAt":"2026-06-13T12:00:00.000Z","anchor":{"selector":"#hero","tag":"h1","text":"Quarterly Strategy Brief"}}]}}}},"400":{"description":"invalid slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"invalid slug"}}}},"404":{"description":"no such review site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such review site"}}}},"429":{"description":"rate limit exceeded; retry in 60s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"rate limit exceeded; retry in 60s"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}}}},"/api/v1/sites/{slug}/review-packet":{"get":{"summary":"One-call review packet for revising a site (owner only)","description":"Returns everything an agent needs to apply feedback in a single call: the site + current version, content references (the manifest plus where to fetch bytes), the OPEN reviewer comments with anchors and status, the computed reviewStatus, and approvedVersionId. Replaces stitching get_site_content + get_feedback + get_site_status. Owner-only data: Bearer key, or the claimToken query param for anonymous sites. Missing or non-review sites return a uniform 404.","security":[{"bearerApiKey":[]},{}],"parameters":[{"name":"slug","in":"path","required":true,"description":"Site slug, e.g. calm-saddle-7h2k.","schema":{"type":"string"},"example":"calm-saddle-7h2k"},{"name":"claimToken","in":"query","required":false,"description":"Claim token for anonymous sites.","schema":{"type":"string"}}],"responses":{"200":{"description":"The review packet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewPacket"},"example":{"slug":"calm-saddle-7h2k","site":{"slug":"calm-saddle-7h2k","anonymous":false,"currentVersionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","reviewEnabled":true,"title":"Quarterly Strategy Brief","description":"Q2 plan"},"version":{"versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","n":3},"contentRefs":{"versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","getContentUrl":"https://agentpub.io/api/v1/publish/calm-saddle-7h2k/content","siteUrl":"https://calm-saddle-7h2k.agentpub.io/","files":[{"path":"index.html","contentType":"text/html; charset=utf-8","hash":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}]},"comments":[{"id":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","body":"The hero copy buries the call to action.","authorMasked":"r***@example.com","verified":false,"status":"open","versionId":"01HV4Z3K7Q9X2M8N6P0R5T1W3Y","createdAt":"2026-06-13T12:00:00.000Z","anchor":{"selector":"#hero","tag":"h1","text":"Quarterly Strategy Brief"}}],"openComments":1,"reviewStatus":"changes_requested","approved":false,"approvedVersionId":null}}}},"400":{"description":"invalid slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_request","code":"invalid_request","message":"invalid slug"}}}},"401":{"description":"authentication required to update this site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"unauthorized","code":"unauthorized","message":"authentication required to update this site"}}}},"403":{"description":"claimToken required to update an anonymous site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","code":"forbidden","message":"claimToken required to update an anonymous site"}}}},"404":{"description":"no such review site","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","code":"not_found","message":"no such review site"}}}},"429":{"description":"rate limit exceeded; retry in 60s","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"rate_limit_exceeded","code":"rate_limit_exceeded","message":"rate limit exceeded; retry in 60s"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}}}}}}}