openapi: 3.2.0 info: title: anew Pages API version: 1.0.0 summary: Encode self-contained HTML pages into shareable URLs. description: anew turns a self-contained HTML document into a shareable URL. license: name: ISC identifier: ISC servers: - url: https://anew.page security: - {} tags: - name: Pages description: Write self-contained HTML pages, and read them back. paths: /write: post: operationId: writePage tags: - Pages summary: Write HTML to an anew.page URL description: 'POST raw HTML; receive the full anew.page URL as text. A 200 also carries the measurement in headers — Anew-URL-Bytes, Anew-URL-Limit, Anew-Decoded-Bytes, Anew-Decoded-Limit, Anew-Share-Warning — so one write reports exactly where the page stands against both budgets. The 4000-byte URL figure is an share-safe budget, not a limit: a longer URL still mints, renders and serves — the 200 carries Anew-Share-Warning (and a JSON `warning` field) saying it links may break over this length in some apps. Returns 413 only past the physical ceilings: the encoded URL exceeds 65000 bytes — the edge severs the request line there (code url_over_budget) — the HTML exceeds the 163839-byte decode cap (content_too_large), or the request body exceeds 262144 bytes (body_too_large). Errors are JSON: { "error": { "code", "message", "hint?", "details?" } }; url_over_budget and content_too_large detail both ceilings and the overage, body_too_large details the body budget. Measure by posting, not by calculating: a write is repeatable, so send the page and read what comes back instead of modelling the encoder. No source HTML and no page record are kept — the URL carries the page — though a successful write also kicks off a best-effort screenshot render that, when it succeeds, is cached publicly at the page''s .png twin. A 200 reports the page against both budgets in its Anew-URL-Bytes and Anew-Decoded-Bytes response headers, and carries Anew-Share-Warning when the URL minted past the share-safe budget (valid, serves, may break in some apps); a 413 fires only past the physical ceilings and names the exact overage and the fix for the wall that tripped. Never estimate a compressed size and never pre-cut a page to fit a budget — Brotli is non-linear, so source bytes removed are not URL bytes saved, and pages that look far too big routinely fit. If your environment cannot issue HTTP POST at all (egress proxies that refuse POST or CONNECT are the common cause), https://anew.page/llms.txt carries a local-mint fallback that builds a working URL for the same page offline — byte-identical to /write only for the reference brotli recipe there, and a different but equally valid slug otherwise. Exact recipe: https://anew.page/llms.txt. Works cross-origin from browser JS: every /write response — success and error — carries Access-Control-Allow-Origin: *, OPTIONS preflight is answered, and the request Content-Type is never inspected, so a POST with a safelisted type like text/plain needs no preflight at all. The measurement headers on a 200 are named in Access-Control-Expose-Headers, so response.headers.get(''Anew-URL-Bytes'') works from a foreign origin too.' parameters: - name: Accept in: header required: false schema: type: string enum: - text/plain - application/json description: Representation of the 200. Omit it, or send a wildcard, for the bare URL as text/plain; name application/json at a weight at least equal to text/plain for the WriteResult object instead. requestBody: required: true content: text/html: schema: type: string minLength: 1 description: HTML document, fragment, or plain text example: anew

a new internet starts with anew.page

text/plain: schema: type: string minLength: 1 description: HTML document, fragment, or plain text application/octet-stream: schema: type: string minLength: 1 description: HTML document, fragment, or plain text responses: '200': description: 'anew.page URL — proof by construction that it decodes to the posted HTML. text/plain (the bare URL) is the default; Accept: application/json returns the same URL and both measurements as one object. Vary: Accept.' headers: RateLimit-Policy: description: 'The enforced quota, draft-ietf-httpapi-ratelimit-headers-11 form: "api";q=300;w=60 — 300 requests per 60 seconds per client IP, shared across /write, /mcp and /a2a/v1.' schema: type: string Anew-URL-Bytes: description: Encoded URL length in bytes. schema: type: integer Anew-URL-Limit: description: 'Unfurl-safe URL threshold (4000); headroom = limit - bytes. A threshold, not a wall: past it the write still succeeds and Anew-Share-Warning rides along.' schema: type: integer Anew-Share-Warning: description: 'Present only when the page minted past a share-safe budget — URL over the 4000-byte share-safe budget (hard ceiling 65000), or content over the recommended 160000-byte budget: the page is valid and serves, with the stated caveat. Relay it to the user with the URL.' schema: type: string Anew-Decoded-Bytes: description: The HTML as encoded, in UTF-8 bytes — measured after decoding the body, so a stripped BOM makes it differ from the bytes posted. schema: type: integer Anew-Decoded-Limit: description: Recommended decoded-byte budget (160000); the hard decode ceiling is 163839. schema: type: integer Access-Control-Expose-Headers: description: Names the measurement headers, Anew-Share-Warning, and RateLimit-Policy, so cross-origin browser JS can read them. schema: type: string content: text/plain: schema: type: string format: uri application/json: schema: $ref: '#/components/schemas/WriteResult' '400': description: 'Not a postable page — code empty_body, invalid_utf8, or json_envelope (a JSON wrapper such as { "html": ... } was posted in place of the HTML itself).' content: application/json: schema: $ref: '#/components/schemas/Error' '405': description: 'Wrong method. Carries Allow: POST, OPTIONS and a hint stating the full POST contract.' content: application/json: schema: $ref: '#/components/schemas/Error' '413': description: Over a physical ceiling — code url_over_budget (encoded URL > 65000 bytes, where the edge severs the request line), content_too_large (decoded HTML > 163839 bytes, the server's decode cap), or body_too_large (request body > 262144 bytes). The 4000-byte share-safe budget never 413s — it warns on a 200. content: application/json: schema: $ref: '#/components/schemas/Error' '429': description: 'Over the published quota (code rate_limited). Carries Retry-After: 60, RateLimit-Policy, and RateLimit with r=0. Reading a page URL is not rate limited. Policy: https://anew.page/llms.txt' headers: RateLimit-Policy: description: 'The enforced quota, draft-ietf-httpapi-ratelimit-headers-11 form: "api";q=300;w=60 — 300 requests per 60 seconds per client IP, shared across /write, /mcp and /a2a/v1.' schema: type: string Retry-After: description: Seconds to wait — always the full 60-second window, so one wait always clears the refusal. schema: type: integer RateLimit: description: 'draft-ietf-httpapi-ratelimit-headers-11 quota state at the moment of refusal: r=0 (exact) and t = the full window (an upper bound on the wait).' schema: type: string content: application/json: schema: $ref: '#/components/schemas/Error' /{slug}: get: operationId: readPage tags: - Pages summary: Read a page description: 'GET an anew.page URL. Default `Accept` renders the page. `Accept: text/plain` returns the author''s exact HTML — no anew-injected tags, no proxied image srcs — so it re-encodes byte-for-byte through `POST /write`. `Accept: text/markdown` (or a `.md` suffix) returns a lossy Markdown rendering of the built page, which cannot be re-encoded. Responses `Vary: Accept`. A `.png` suffix returns a 1200×630 screenshot of the rendered page — the page''s own scripts do run, but the capture blocks network egress, so fetch, XHR and external scripts never resolve. `HEAD` answers like `GET` without the body — the cheap way to check that a slug decodes (200) or does not (404).' parameters: - name: slug in: path required: true schema: type: string description: The encoded document — everything after the origin. Cosmetic path prefixes and multi-segment slugs also resolve, though the {slug} template cannot express them. - name: Accept in: header required: false schema: type: string enum: - text/html - text/plain - text/markdown description: Representation to return. responses: '200': description: The page, its source, or its Markdown rendering. content: text/html: schema: type: string text/plain: schema: type: string text/markdown: schema: type: string '404': description: The slug does not decode to a page. JSON for a client that asks for it, Markdown with where to look next for one that asks for text, an HTML page otherwise. content: application/json: schema: $ref: '#/components/schemas/Error' text/markdown: schema: type: string text/html: schema: type: string '451': description: The page decodes, but it links to a host currently flagged as phishing or malware by a threat feed (code content_blocked). Neither representation names the host, the feed or an address. JSON for a client that asks for it, the bare status code as text otherwise. content: application/json: schema: $ref: '#/components/schemas/Error' text/plain: schema: type: string '502': description: 'The page decodes, but the Markdown conversion failed (code markdown_failed). Only the Markdown representation is affected — the same URL still serves HTML and, under Accept: text/plain, the author''s source.' content: application/json: schema: $ref: '#/components/schemas/Error' components: schemas: WriteResult: type: object required: - url - url_bytes - url_limit - decoded_bytes - decoded_limit description: A successful write, for callers that asked for application/json instead of the bare URL. The same numbers the Anew-* response headers carry. properties: url: type: string format: uri description: The complete shareable anew.page URL. Return it verbatim. url_bytes: type: integer description: Encoded URL length in bytes. url_limit: type: integer description: Unfurl-safe URL threshold (4000); headroom = limit - bytes. A threshold, not a wall — writes past it succeed with a warning, up to the 65000-byte serve ceiling. decoded_bytes: type: integer description: The HTML as encoded, in UTF-8 bytes. decoded_limit: type: integer description: Recommended decoded-byte budget (160000). warning: type: string description: Present when the page minted past a share-safe budget — URL over the share-safe budget (links may break over it in some apps) or content over the recommended byte budget. The URL works and serves either way. Relay it to the user with the URL. Error: type: object required: - error description: 'The envelope every REST failure answers with: a machine-readable code, a human-readable message, a hint where one helps, details where there is a measurement.' properties: error: type: object required: - code - message properties: code: type: string enum: - not_found - method_not_allowed - origin_forbidden - rate_limited - empty_body - invalid_utf8 - json_envelope - url_over_budget - content_too_large - body_too_large - markdown_failed - content_blocked description: Which failure this is. Stable across releases; a caller may switch on it exhaustively. message: type: string hint: type: string description: What to do instead. details: type: object description: 'On url_over_budget and content_too_large: both physical ceilings (url_limit = the 65,000-byte request line, decoded_limit = the 163,839-byte decode cap) and the overage against the one that tripped — these fire only past the ceilings, never at the 4,000-byte share-safe budget, which warns on a 200 instead. On body_too_large: the body budget, since the encode never ran and there is no url/decoded measurement to report.' properties: url_bytes: type: integer url_limit: type: integer decoded_bytes: type: integer decoded_limit: type: integer body_bytes: type: integer body_limit: type: integer over: type: integer externalDocs: url: https://anew.page/llms.txt description: 'Developer documentation: authentication, rate limits, versioning and deprecation policy, and every machine-readable resource.'