{"openapi":"3.1.0","info":{"title":"blogz.site Publishing API","version":"2.0.0","summary":"Publish Markdown articles to the blogs you own.","description":"The publishing API manages your own publications, articles, and themes. Public reading pages live on each publication's subdomain when its public site is enabled.\n\n### Get an API token\n\nSign in at [your dashboard](https://blogz.site/dashboard), open **API tokens**, and create a named credential. Choose **read** or **write**, select all your blogs or one publication, and choose a 30-day, 90-day, 1-year, 10-year, or 20-year expiry (90 days by default). Lifetimes are fixed at 30, 90, 365, 3650, or 7300 days. The token is shown only once. Send it in the `Authorization: Bearer YOUR_API_TOKEN` header. You can revoke it immediately from the dashboard. New account signup requires Cloudflare Turnstile verification; existing account login and bearer API requests do not.\n\nRead tokens allow GET and HEAD. Write tokens also allow publishing and theme changes. A publication-scoped token sees only its selected publication and cannot create blogs or use the general preview and keyword-generation endpoints. Every resource request also checks ownership. The operator publishing token is a separate credential and cannot authenticate this API.\n\n### Weekly publishing plan\n\nThe free plan allows **1 new published article per UTC week**, shared across all owned blogs, dashboard sessions, user API tokens, and operator writes to an owned blog, including headless blogs. The week resets on **Monday at 00:00 UTC**. Drafts do not use a weekly slot; publishing a draft does. Editing or republishing the same article after it has gone live does not use another slot. Scheduled articles reserve a slot in their publication week, and rescheduling an upcoming publication moves that reservation when the target week has room. Backdated first publications count in the current week. Deleting or unpublishing does not refund a slot. An exhausted POST or PUT returns **429 PUBLICATION_WEEKLY_LIMIT** with `{ error, quota, contact_url }` and `Retry-After` in seconds until that week ends. The operator can change an account’s allowance; users cannot. See [pricing](https://blogz.site/pricing) or [contact us](mailto:admin@blogz.site) for a higher allowance.\n\n### Daily allowance\n\nBy default, each account can create **3 new posts per UTC calendar day**, shared across all owned blogs, dashboard sessions, and user API tokens. The blogz.site operator can raise or lower the limit for an individual account; account owners and their tokens cannot change it. Drafts count when created. Existing posts can be edited without consuming a slot, including publishing a saved draft. Deletion does not refund a slot. The allowance resets at **00:00 UTC**. An exhausted article-create POST returns **429** with `{ error, quota }`; `quota` contains your account’s current `limit`, `used`, `remaining`, and the ISO `resets_at` time. The `Retry-After` header contains the seconds until the next midnight UTC. Article PUT only edits existing posts and is not subject to this creation allowance.\n\n### Markdown first\n\nArticles accept JSON with a `markdown` field or `text/markdown` with YAML frontmatter. A title and nonempty manuscript are required. POST creates an article; PUT replaces its editable fields while keeping the URL slug stable. Save drafts privately or publish now; future publication dates stay private until their time arrives. Request a single article with `Accept: text/markdown` to export its editable source and frontmatter.\n\n### Public or headless blogs\n\nNew blogs and existing publications have `public_enabled: true` by default. Set `public_enabled: false` when creating a blog to use it as a headless CMS. Change it later with `PUT /api/v2/blogs/{slug}/visibility` and `{ \"public_enabled\": false }` or `{ \"public_enabled\": true }`. This requires write permission and blog ownership; a write token scoped to that blog is sufficient. The dashboard exposes the same setting.\n\nWhen the public site is disabled, its subdomain and legacy public paths return a generic 404, and the blog and its articles disappear from public directories, search, saved-story retrieval, feeds, sitemaps, and social images. Authenticated account and publishing APIs continue to return its manuscripts, including drafts and scheduled posts. Disabling the site preserves content, themes, and article publication status. Enabling it exposes only already-published posts whose publication date has arrived. The `url` field identifies the reserved publication address; it is not a readable public website while `public_enabled` is false.\n\n### Authors and article metadata\n\nEach blog has its own authors, created or replaced with `PUT /api/v2/blogs/{slug}/authors/{author}`. An article’s optional `author` field holds an author slug from the same blog; publication pages then show that author’s name, job title, and portrait and publish a schema.org Person with `jobTitle`, `image`, `sameAs` (the author’s links), and `url`. Authors can add `bio_markdown` (up to 30,000 words), `profile_enabled`, and `is_default`. Public author cards link to the biography and social profiles; hiding a profile removes those links from pages and JSON-LD. An unassigned article uses the blog’s default author, or falls back to the blog-level author string. Profiles can be managed under **Authors** in the blog dashboard.  An author cannot be deleted while articles reference it.\n\nArticles also accept `cover_image_url` and `cover_image_alt`, an `seo_title` that overrides the HTML title and og:title, a `meta_description` used verbatim instead of the derived description (`excerpt` stays the card summary), up to 30 plain-text `faq` entries that publication pages render with FAQPage JSON-LD, and `structured_data`, a JSON object (at most 16 KiB) of extra schema.org properties such as `about`, `mentions`, or `speakable` that is merged into the page’s BlogPosting without replacing its own properties. All of these work in JSON and YAML front matter and appear in the Markdown export. Imports may also send `updated_at` to keep the original modification time; normal edits should omit it so the edit time is recorded.\n\n### Media\n\nUpload images with `POST /api/v2/blogs/{slug}/media` (raw bytes) or copy them from another server with `POST /api/v2/blogs/{slug}/media/import` and `{ \"source_url\": \"https://…\" }`. Both return `{ url, key, content_type, bytes, sha256, width, height, variants }`. PNG and JPEG files also get AVIF and WebP renditions, and WebP files get AVIF, at `{url}.avif` and `{url}.webp`; `variants` lists those stored (smaller than the original, not animated). Rendition URLs always answer, with the original while a rendition is pending or when none exists, so a `<picture>` can list both sources unconditionally. Renditions are free. Files are content-addressed per blog, so identical bytes always get the same URL, and re-sending them is free. New files count toward the blog owner’s daily media allowance: 50 files or 100 MiB per UTC day unless the operator changed it; a full allowance returns **429** with `{ error, quota }` and `Retry-After`. Public media URLs work for headless blogs too, so an external site can embed them. Media writes do not count toward the daily post allowance.\n\n### Building a site from a headless blog\n\nA read token is enough to build a static site. Fetch every page of `GET /api/v2/blogs/{slug}/articles?live=true&fields=full&limit=25` (raise `offset` by 25 until a page has fewer than 25 entries) plus `GET /api/v2/blogs/{slug}/authors`, and reference images by their `/media` URLs, which stay public for headless blogs. Use `updated_since` for incremental refreshes and the blog webhook to know when to rebuild.\n\n### Webhooks\n\nGive a blog a webhook with `PUT /api/v2/blogs/{slug}/webhook` so a site that reads it through this API can rebuild. Article, author, and blog-setting writes then POST `{ event, blog, article?, author?, occurred_at }` with `X-Blogz-Event` and, when a secret is set, `X-Blogz-Signature: sha256=<hex HMAC of the raw body>`. A cron every 5 minutes sends `article.published` once per scheduled article when its time arrives. Add `?notify=false` to writes during bulk imports. Deliveries never fail the write.\n\n### Article keywords\n\nYou may provide up to 12 keywords or short phrases. Nonempty author-provided keywords are preserved. If they are missing or empty, the Worker asks OpenRouter’s `openrouter/free` model with zero-price provider limits. If the provider is unavailable or no Worker key is configured, deterministic local extraction supplies keywords so saving still succeeds. No paid-model fallback is used.\n\n### Browser requests\n\nAPI-token requests do not require an Origin header. If a browser sends Origin, it must match the account apex exactly. Cookie-authenticated changes always require that exact Origin. Tenant subdomains never serve account, authentication, or publishing APIs. All authenticated responses are private and not cached. API tokens cannot generate, list, or revoke other tokens; those operations require a Better Auth dashboard session."},"servers":[{"url":"https://blogz.site","description":"Account apex"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Account","description":"The account associated with the API token."},{"name":"Blogs","description":"Owned publications and their subdomains."},{"name":"Articles","description":"Markdown manuscripts, private drafts, and published articles."},{"name":"Authors","description":"Bylines for a publication, referenced by articles and published as schema.org Person data."},{"name":"Media","description":"Images stored on R2 and served publicly from the apex, including for headless blogs."},{"name":"Webhooks","description":"Rebuild notifications for sites that read a blog through the API."},{"name":"Themes","description":"Minimal validated publication appearance settings."},{"name":"Writing tools","description":"Sanitized previews and automatic keywords."},{"name":"API tokens","description":"Dashboard-session operations. Bearer tokens cannot manage credentials."},{"name":"Operator","description":"Deployment-operator endpoints under /api/v1. They require the operator PUBLISH_TOKEN; dashboard sessions and user API tokens are rejected."}],"paths":{"/api/v2/me":{"get":{"tags":["Account"],"operationId":"getAccount","summary":"Get your account","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","required":["user"],"properties":{"user":{"$ref":"#/components/schemas/User"}}}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v2/blogs":{"get":{"tags":["Blogs"],"operationId":"listBlogs","summary":"List your blogs","description":"Returns all owned blogs. A publication-scoped token returns only its selected blog.","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","required":["blogs"],"properties":{"blogs":{"type":"array","items":{"$ref":"#/components/schemas/Blog"}}}}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Blogs"],"operationId":"createBlog","summary":"Create a publication","description":"Requires write access to all owned blogs. The slug reserves its subdomain and must not be reserved. public_enabled defaults to true; set it to false to create a headless blog with authenticated CMS access and no public website.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlogInput"}}}},"responses":{"201":{"description":"Publication created.","content":{"application/json":{"schema":{"type":"object","required":["blog"],"properties":{"blog":{"$ref":"#/components/schemas/Blog"}}}}},"headers":{"Location":{"schema":{"type":"string"},"description":"The created account API resource."}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The slug is already taken or the active-token limit was reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body exceeds the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v2/blogs/{slug}":{"parameters":[{"name":"slug","in":"path","required":true,"description":"The publication subdomain, for example sample.","schema":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"example":"sample"}],"get":{"tags":["Blogs"],"operationId":"getBlog","summary":"Get publication settings","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","required":["blog"],"properties":{"blog":{"$ref":"#/components/schemas/Blog"}}}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v2/blogs/{slug}/visibility":{"parameters":[{"name":"slug","in":"path","required":true,"description":"The publication subdomain, for example sample.","schema":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"example":"sample"}],"put":{"tags":["Blogs"],"operationId":"setBlogVisibility","summary":"Enable or disable a blog’s public site","parameters":[{"name":"notify","in":"query","required":false,"schema":{"type":"string","enum":["false"]},"description":"Send `false` to skip the blog webhook for this write, for example during a bulk import."}],"description":"Requires write access and ownership, including a write token scoped to this blog. Send public_enabled false for a headless blog or true to enable its public website. Only this visibility setting changes; articles, their publication statuses, themes, and authenticated CMS access are preserved. Public content stays subject to published status and publication date. Cookie-authenticated clients use the same route under /api/account and must send the exact apex Origin.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlogVisibilityInput"}}}},"responses":{"200":{"description":"Publication visibility updated.","content":{"application/json":{"schema":{"type":"object","required":["blog"],"properties":{"blog":{"$ref":"#/components/schemas/Blog"}}}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The slug is already taken or the active-token limit was reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body exceeds the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v2/blogs/{slug}/articles":{"parameters":[{"name":"slug","in":"path","required":true,"description":"The publication subdomain, for example sample.","schema":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"example":"sample"}],"get":{"tags":["Articles"],"operationId":"listArticles","summary":"List your articles","description":"By default includes your drafts and scheduled articles, and entries contain metadata with an empty markdown field and an empty faq list; fetch a single article for the manuscript. For a site build, `live=true` keeps only published articles whose publication date has arrived (headless blogs included), `fields=full` returns complete articles (markdown, faq, cover image, author, SEO fields, timestamps), and `updated_since` keeps articles changed at or after a time. Results are always ordered by published_at descending (unpublished drafts last), then created_at and id descending: a total order, so offset pages neither overlap nor skip while content is unchanged. Page until a response has fewer than `limit` entries. Pages of full articles hold at most 25 entries.","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":100},"description":"Default 100, or 25 with `fields=full`, which allows at most 25 (a larger value returns 400)."},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"maximum":1000000,"default":0}},{"name":"live","in":"query","schema":{"type":"string","enum":["true","false"],"default":"false"},"description":"`true`: only status published with published_at at or before now. Drafts and scheduled posts are left out."},{"name":"fields","in":"query","schema":{"type":"string","enum":["metadata","full"],"default":"metadata"},"description":"`full` includes markdown and faq bodies in each entry; its pages hold at most 25 articles."},{"name":"updated_since","in":"query","schema":{"type":"string","format":"date-time"},"description":"ISO-8601 with `Z` or a `±HH:MM` offset. Only articles whose updated_at is at or after this time. Encode `+` as `%2B`; an unencoded `+` that arrives as a space before the offset is also accepted. Deletions do not appear; use the article.deleted webhook or compare a full listing.","example":"2026-10-01T00:00:00Z"}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","required":["articles"],"properties":{"articles":{"type":"array","items":{"$ref":"#/components/schemas/Article"}}}}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Articles"],"operationId":"createArticle","summary":"Create a Markdown article","requestBody":{"required":true,"description":"Maximum 1 MiB request body. Send JSON or a UTF-8 Markdown manuscript with YAML frontmatter containing a title.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleInput"}},"text/markdown":{"schema":{"type":"string","example":"---\ntitle: A small place for good ideas\nslug: a-small-place-for-good-ideas\nstatus: draft\ntags:\n  - Writing\nkeywords:\n  - personal blogging\n  - writing practice\nauthor: jane-doe\nseo_title: A small place for good ideas | Sample\nfaq:\n  - question: Why keep a blog?\n    answer: To return to the ideas worth keeping.\n---\n## A place to begin\n\nA blog is a small place to keep the ideas you want to return to.\n"}}}},"parameters":[{"name":"notify","in":"query","required":false,"schema":{"type":"string","enum":["false"]},"description":"Send `false` to skip the blog webhook for this write, for example during a bulk import."}],"description":"Requires write access. A missing slug is derived from the title. The default status is draft. Keywords are generated if missing or empty. Creating a draft or published article consumes one of the account’s daily new-post slots (3 per UTC calendar day unless the operator changed this account’s limit). Status published also consumes a weekly publication slot (1 per UTC week on the free plan), including headless blogs; scheduled articles reserve their publication week. The limits are shared across owned blogs and credentials. Deletion does not refund either allowance.","responses":{"201":{"description":"Article created.","content":{"application/json":{"schema":{"type":"object","required":["article"],"properties":{"article":{"$ref":"#/components/schemas/Article"}}}}},"headers":{"Location":{"schema":{"type":"string"},"description":"The created account API resource."}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The slug is already taken or the active-token limit was reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body exceeds the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The account has reached its daily creation allowance or weekly publication allowance. The error code identifies the allowance: POST_DAILY_LIMIT or PUBLICATION_WEEKLY_LIMIT. Weekly errors include a contact link for a higher allowance.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1},"description":"Seconds until the relevant allowance resets: midnight UTC for creation, or Monday 00:00 UTC after the requested publication week for publishing.","example":3600}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ArticleCreateQuotaError"},{"$ref":"#/components/schemas/WeeklyPublicationQuotaError"}]}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v2/blogs/{slug}/articles/{article}":{"parameters":[{"name":"slug","in":"path","required":true,"description":"The publication subdomain, for example sample.","schema":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"example":"sample"},{"name":"article","in":"path","required":true,"description":"The stable article route slug.","schema":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-z0-9]+(?:-+[a-z0-9]+)*$","description":"Lowercase letters and numbers separated by one or more hyphens, up to 160 characters, so imported URLs such as `april-2026----everything-that-changed` keep their address. Slugs derived from a title use single hyphens. Blog slugs stay stricter because they are subdomain labels."},"example":"a-small-place-for-good-ideas"}],"get":{"tags":["Articles"],"operationId":"getArticle","summary":"Get a manuscript or Markdown export","description":"Returns full Markdown and metadata, including private drafts. Use Accept: text/markdown to export YAML frontmatter and the manuscript.","responses":{"200":{"description":"The complete article.","headers":{"Vary":{"schema":{"type":"string"},"example":"Accept"}},"content":{"application/json":{"schema":{"type":"object","required":["article"],"properties":{"article":{"$ref":"#/components/schemas/Article"}}}},"text/markdown":{"schema":{"type":"string","example":"---\ntitle: A small place for good ideas\nslug: a-small-place-for-good-ideas\nstatus: draft\ntags:\n  - Writing\nkeywords:\n  - personal blogging\n  - writing practice\nauthor: jane-doe\nseo_title: A small place for good ideas | Sample\nfaq:\n  - question: Why keep a blog?\n    answer: To return to the ideas worth keeping.\n---\n## A place to begin\n\nA blog is a small place to keep the ideas you want to return to.\n"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Articles"],"operationId":"replaceArticle","summary":"Replace an article","requestBody":{"required":true,"description":"Maximum 1 MiB request body. Send JSON or a UTF-8 Markdown manuscript with YAML frontmatter containing a title.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleInput"}},"text/markdown":{"schema":{"type":"string","example":"---\ntitle: A small place for good ideas\nslug: a-small-place-for-good-ideas\nstatus: draft\ntags:\n  - Writing\nkeywords:\n  - personal blogging\n  - writing practice\nauthor: jane-doe\nseo_title: A small place for good ideas | Sample\nfaq:\n  - question: Why keep a blog?\n    answer: To return to the ideas worth keeping.\n---\n## A place to begin\n\nA blog is a small place to keep the ideas you want to return to.\n"}}}},"parameters":[{"name":"notify","in":"query","required":false,"schema":{"type":"string","enum":["false"]},"description":"Send `false` to skip the blog webhook for this write, for example during a bulk import."}],"description":"Full replacement of an existing article, not a patch. Title and Markdown remain required; omitted optional fields reset to defaults. The route slug is stable and overrides any slug in the body. If publication status stays published and published_at is omitted, the previous publication date is retained. Missing or empty keywords are generated. Edits do not consume the daily creation allowance. First publication of a draft or moving a future publication into another week requires a weekly slot; articles already published remain editable without a new slot. A missing article returns 404.","responses":{"200":{"description":"Article replaced.","content":{"application/json":{"schema":{"type":"object","required":["article"],"properties":{"article":{"$ref":"#/components/schemas/Article"}}}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The slug is already taken or the active-token limit was reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body exceeds the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The account has reached its weekly publication allowance (1 unless the operator raised it). Drafts and edits to articles already published remain available. Contact admin@blogz.site for a higher allowance or retry after the requested publication week resets.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1},"description":"Seconds until Monday at 00:00 UTC following the requested publication week."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WeeklyPublicationQuotaError"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Articles"],"operationId":"deleteArticle","summary":"Delete an article","parameters":[{"name":"notify","in":"query","required":false,"schema":{"type":"string","enum":["false"]},"description":"Send `false` to skip the blog webhook for this write, for example during a bulk import."}],"description":"Requires write access. Removes the article permanently and sends `article.deleted`. Deleting does not refund the daily creation or weekly publication allowance.","responses":{"204":{"description":"Article deleted."},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v2/blogs/{slug}/authors":{"parameters":[{"name":"slug","in":"path","required":true,"description":"The publication subdomain, for example sample.","schema":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"example":"sample"}],"get":{"tags":["Authors"],"operationId":"listAuthors","summary":"List a blog’s authors","description":"Read access is enough. Ordered by name, then slug.","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":100}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"maximum":1000000,"default":0}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","required":["authors"],"properties":{"authors":{"type":"array","items":{"$ref":"#/components/schemas/Author"}}}}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v2/blogs/{slug}/authors/{author}":{"parameters":[{"name":"slug","in":"path","required":true,"description":"The publication subdomain, for example sample.","schema":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"example":"sample"},{"name":"author","in":"path","required":true,"description":"The stable author slug, unique within the blog.","schema":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-z0-9]+(?:-+[a-z0-9]+)*$","description":"Lowercase letters and numbers separated by one or more hyphens, up to 160 characters, so imported URLs such as `april-2026----everything-that-changed` keep their address. Slugs derived from a title use single hyphens. Blog slugs stay stricter because they are subdomain labels."},"example":"jane-doe"}],"get":{"tags":["Authors"],"operationId":"getAuthor","summary":"Get an author","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","required":["author"],"properties":{"author":{"$ref":"#/components/schemas/Author"}}}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Authors"],"operationId":"putAuthor","summary":"Create or replace an author","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorInput"}}}},"parameters":[{"name":"notify","in":"query","required":false,"schema":{"type":"string","enum":["false"]},"description":"Send `false` to skip the blog webhook for this write, for example during a bulk import."},{"name":"If-None-Match","in":"header","required":false,"schema":{"type":"string","const":"*"},"description":"Create only. Return 409 rather than overwrite an existing author at this address."}],"description":"Requires write access. Creates the author at this stable slug (201) or fully replaces it (200), keeping its ID and created_at; omitted optional fields are cleared. The read-only fields a GET returns (id, blog_id, slug, created_at, updated_at) are ignored, so a fetched author can be written back unchanged; any other unknown field is rejected. A blog can have up to 200 authors. Send If-None-Match: * to create only. Use bio_markdown for a biography of up to 30,000 words, profile_enabled for public display, and is_default to select this blog’s fallback byline.","responses":{"200":{"description":"Author replaced.","content":{"application/json":{"schema":{"type":"object","required":["author"],"properties":{"author":{"$ref":"#/components/schemas/Author"}}}}}},"201":{"description":"Author created.","content":{"application/json":{"schema":{"type":"object","required":["author"],"properties":{"author":{"$ref":"#/components/schemas/Author"}}}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The slug is already taken or the active-token limit was reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body exceeds the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Authors"],"operationId":"deleteAuthor","summary":"Delete an unused author","parameters":[{"name":"notify","in":"query","required":false,"schema":{"type":"string","enum":["false"]},"description":"Send `false` to skip the blog webhook for this write, for example during a bulk import."}],"description":"Requires write access. Returns 409 AUTHOR_IN_USE while any article in the blog references the author; reassign or clear those articles first.","responses":{"204":{"description":"Author deleted."},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"An article still references this author.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v2/blogs/{slug}/media":{"parameters":[{"name":"slug","in":"path","required":true,"description":"The publication subdomain, for example sample.","schema":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"example":"sample"}],"post":{"tags":["Media"],"operationId":"uploadMedia","summary":"Upload an image","description":"Requires write access. Send the raw image bytes with a matching Content-Type: PNG, JPEG, WebP, AVIF, or GIF, at most 10 MiB. SVG is rejected. The bytes are checked against the declared type and structurally: the file must be complete, contain nothing after the image’s end marker, and be at most 16,384 pixels per side and 100 megapixels in total (pixels are not decoded). Valid files are hashed with SHA-256 and stored once per blog, so uploading the same file again returns the same key and URL with 200. Media writes do not consume the daily new-post allowance.","requestBody":{"required":true,"content":{"image/png":{"schema":{"type":"string","format":"binary"}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"image/avif":{"schema":{"type":"string","format":"binary"}},"image/gif":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"These exact bytes were already stored for this blog; the existing object is returned and nothing is charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaObject"}}}},"201":{"description":"Image stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaObject"}}}},"400":{"description":"Invalid input: an empty body, `INVALID_IMAGE` (incomplete or damaged file, or data after the image), or `IMAGE_DIMENSIONS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The slug is already taken or the active-token limit was reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The image is larger than 10 MiB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Not a supported image, an SVG, or bytes that do not match the Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The blog owner’s daily media allowance (50 new files or 100 MiB per UTC day unless the operator changed it) cannot fit this new file. Files already stored keep working; re-sending identical bytes is still free.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1},"description":"Seconds until the allowance resets at midnight UTC."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaQuotaError"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v2/blogs/{slug}/media/import":{"parameters":[{"name":"slug","in":"path","required":true,"description":"The publication subdomain, for example sample.","schema":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"example":"sample"}],"post":{"tags":["Media"],"operationId":"importMedia","summary":"Copy an image from a URL","description":"Requires write access. The Worker downloads `source_url` itself: only public https:// hosts on the default port are allowed (no IP addresses, local or internal names, credentials, blogz.site hosts, or this Worker’s workers.dev address), up to 3 redirects are followed and each target is checked again, the download times out after 15 seconds, and the response must be at most 10 MiB with a supported image Content-Type or `application/octet-stream`. The stored type comes from the image bytes, which pass the same structural checks as uploads. Returns the same shape as an upload.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaImportInput"}}}},"responses":{"200":{"description":"These exact bytes were already stored for this blog; the existing object is returned and nothing is charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaObject"}}}},"201":{"description":"Image stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaObject"}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The slug is already taken or the active-token limit was reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The remote image is larger than 10 MiB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"The URL did not return a supported image.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The blog owner’s daily media allowance (50 new files or 100 MiB per UTC day unless the operator changed it) cannot fit this new file. Files already stored keep working; re-sending identical bytes is still free.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1},"description":"Seconds until the allowance resets at midnight UTC."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaQuotaError"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The image could not be downloaded: an HTTP error, too many redirects, a network failure, or a timeout.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/media/{blog}/{key}":{"parameters":[{"name":"blog","in":"path","required":true,"description":"The publication subdomain, for example sample.","schema":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"example":"sample"},{"name":"key","in":"path","required":true,"description":"A content-addressed file name (the SHA-256 of the bytes plus the extension), optionally followed by `.avif` or `.webp` for that file’s rendition.","schema":{"type":"string","pattern":"^[a-f0-9]{64}\\.(?:png|jpg|webp|avif|gif)(?:\\.(?:avif|webp))?$"},"example":"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.png"}],"get":{"tags":["Media"],"operationId":"getMedia","summary":"Read a public media file","security":[],"description":"Public and immutable. `{key}.avif` and `{key}.webp` return the AVIF or WebP rendition of a PNG, JPEG, or WebP original; without one they return the original’s bytes and type, cached for only 10 minutes while renditions are still pending. Other combinations (GIF or AVIF originals, `.webp.webp`) are 404. Served for headless blogs too, because external sites embed these files; this is the only public route that does not hide headless blogs. Responses carry `Cache-Control: public, max-age=31536000, immutable`, `X-Content-Type-Options: nosniff`, `Cross-Origin-Resource-Policy: cross-origin`, and an ETag. Only If-None-Match is evaluated (a match returns 304); other preconditions are ignored because the bytes behind a key never change. Repeat reads are answered from the Cloudflare edge cache, whose copies expire within a day, so an operator deletion reaches every data center by then.","responses":{"200":{"description":"The image.","content":{"image/png":{"schema":{"type":"string","format":"binary"}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}},"image/avif":{"schema":{"type":"string","format":"binary"}},"image/gif":{"schema":{"type":"string","format":"binary"}}}},"304":{"description":"Not modified."},"404":{"description":"No such blog or file.","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/v2/blogs/{slug}/webhook":{"parameters":[{"name":"slug","in":"path","required":true,"description":"The publication subdomain, for example sample.","schema":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"example":"sample"}],"get":{"tags":["Webhooks"],"operationId":"getWebhook","summary":"Get the blog webhook","description":"Requires write access, even for reading: the URL itself can be a credential. The secret is never returned, only whether one is configured.","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","required":["webhook"],"properties":{"webhook":{"$ref":"#/components/schemas/Webhook"}}}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Webhooks"],"operationId":"putWebhook","summary":"Set or remove the blog webhook","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookInput"}}}},"description":"Requires write access. `webhook_url` must be a public https:// URL on the default port; null removes the webhook and its secret. The secret is write-only: omit `webhook_secret` to keep the stored one, send a new value to replace it, or null to clear it. Changing only `webhook_url` keeps the current secret, so send a new secret in the same request when the webhook moves to a different receiver. When a secret is set, every delivery carries `X-Blogz-Signature: sha256=<hex HMAC-SHA256 of the raw body>`.","responses":{"200":{"description":"Webhook settings saved.","content":{"application/json":{"schema":{"type":"object","required":["webhook"],"properties":{"webhook":{"$ref":"#/components/schemas/Webhook"}}}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The slug is already taken or the active-token limit was reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body exceeds the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v2/blogs/{slug}/theme":{"parameters":[{"name":"slug","in":"path","required":true,"description":"The publication subdomain, for example sample.","schema":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"example":"sample"}],"get":{"tags":["Themes"],"operationId":"getTheme","summary":"Get the publication theme","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","required":["theme"],"properties":{"theme":{"$ref":"#/components/schemas/Theme"}}}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Themes"],"operationId":"replaceTheme","summary":"Save a publication theme","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Theme"}}}},"parameters":[{"name":"notify","in":"query","required":false,"schema":{"type":"string","enum":["false"]},"description":"Send `false` to skip the blog webhook for this write, for example during a bulk import."}],"description":"Send the complete theme. Text/background contrast must be at least 4.5:1. Values are limited to presets, hex colors, and fixed typography/layout choices; arbitrary CSS is not accepted.","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","required":["theme"],"properties":{"theme":{"$ref":"#/components/schemas/Theme"}}}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The slug is already taken or the active-token limit was reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body exceeds the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v2/keywords":{"post":{"tags":["Writing tools"],"operationId":"generateKeywords","summary":"Generate article keywords","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordSource"}}}},"description":"Requires an unscoped write token. Uses openrouter/free when configured and available; otherwise returns local extraction. Does not save an article.","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordResult"}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The slug is already taken or the active-token limit was reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body exceeds the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v2/preview":{"post":{"tags":["Writing tools"],"operationId":"previewMarkdown","summary":"Preview sanitized Markdown","description":"Requires an unscoped write token. Returns rendered HTML and a table of contents without saving.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["markdown"],"properties":{"markdown":{"type":"string","maxLength":950000}}}}}},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkdownPreview"}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The slug is already taken or the active-token limit was reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body exceeds the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/accounts/{account}/daily-post-limit":{"parameters":[{"name":"account","in":"path","required":true,"description":"The account ID, or its email address (case-insensitive; URL-encode it).","schema":{"type":"string","minLength":1,"maxLength":254},"example":"writer@example.com"}],"get":{"tags":["Operator"],"operationId":"getAccountDailyPostLimit","summary":"Get an account’s daily post limit","security":[{"operatorAuth":[]}],"description":"Operator only. Returns the account’s configured new-post limit and today’s UTC usage.","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountPostLimit"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Operator"],"operationId":"setAccountDailyPostLimit","summary":"Raise or lower an account’s daily post limit","security":[{"operatorAuth":[]}],"description":"Operator only. Sets how many new posts the account may create per UTC calendar day across all of its blogs and credentials; every account starts at 3. The change applies immediately to creation checks, 429 quota bodies, and Retry-After responses. Lowering a limit below today’s usage blocks further creations until the next UTC day; edits are never limited. Operator-created blogs without an owner have no daily limit.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountPostLimitInput"}}}},"responses":{"200":{"description":"Limit updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountPostLimit"}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/accounts/{account}/weekly-publication-limit":{"parameters":[{"name":"account","in":"path","required":true,"description":"The account ID, or its email address (case-insensitive; URL-encode it).","schema":{"type":"string","minLength":1,"maxLength":254},"example":"writer@example.com"}],"get":{"tags":["Operator"],"operationId":"getAccountWeeklyPublicationLimit","summary":"Get an account’s weekly publishing allowance","security":[{"operatorAuth":[]}],"description":"Operator only. Returns the configured weekly allowance and current Monday–Sunday UTC usage.","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountPublicationLimit"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Operator"],"operationId":"setAccountWeeklyPublicationLimit","summary":"Change an account’s weekly publishing allowance","security":[{"operatorAuth":[]}],"description":"Operator only. Sets the weekly publication allowance across owned blogs and credentials, including headless blogs. New accounts default to 1. The change applies immediately; lowering below existing usage leaves content intact and blocks new publications. Ownerless operator blogs have no allowance. This setting is separate from the daily creation limit.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountPublicationLimitInput"}}}},"responses":{"200":{"description":"Allowance updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountPublicationLimit"}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/accounts/{account}/daily-media-limit":{"parameters":[{"name":"account","in":"path","required":true,"description":"The account ID, or its email address (case-insensitive; URL-encode it).","schema":{"type":"string","minLength":1,"maxLength":254},"example":"writer@example.com"}],"get":{"tags":["Operator"],"operationId":"getAccountDailyMediaLimit","summary":"Get an account’s daily media allowance","security":[{"operatorAuth":[]}],"description":"Operator only. Returns the account’s new-file and byte allowances per UTC day and today’s usage.","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountMediaLimit"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Operator"],"operationId":"setAccountDailyMediaLimit","summary":"Change an account’s daily media allowance","security":[{"operatorAuth":[]}],"description":"Operator only. Sets both limits; every account starts at 50 new files and 100 MiB per UTC day across all of its blogs and credentials. New uploads and imports are charged atomically before they are stored; identical bytes already stored are free, and deleting media never refunds. Operator-created blogs without an owner have no media allowance.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountMediaLimitInput"}}}},"responses":{"200":{"description":"Allowance updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountMediaLimit"}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/blogs/{slug}/media/{key}":{"parameters":[{"name":"slug","in":"path","required":true,"description":"The publication subdomain, for example sample.","schema":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"example":"sample"},{"name":"key","in":"path","required":true,"description":"The content-addressed file name: the SHA-256 of the bytes plus the extension.","schema":{"type":"string","pattern":"^[a-f0-9]{64}\\.(?:png|jpg|webp|avif|gif)$"},"example":"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.png"}],"delete":{"tags":["Operator"],"operationId":"deleteMedia","summary":"Delete a media file","security":[{"operatorAuth":[]}],"description":"Operator only, for moderation. Deletes the R2 object and its record, so /media/{blog}/{key} returns 404. The daily allowance is not refunded. Browsers and other caches that already downloaded the file may keep their copy until it expires.","responses":{"204":{"description":"Media deleted."},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/account/tokens":{"get":{"tags":["API tokens"],"operationId":"listApiTokens","summary":"List API token metadata","security":[{"cookieAuth":[]}],"description":"Requires a Better Auth dashboard session. Returns active, expired, and revoked metadata. Never returns a token value or hash.","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","required":["tokens"],"properties":{"tokens":{"type":"array","items":{"$ref":"#/components/schemas/TokenMetadata"}}}}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["API tokens"],"operationId":"createApiToken","summary":"Create an API token","security":[{"cookieAuth":[]}],"parameters":[{"name":"Origin","in":"header","required":true,"schema":{"type":"string","const":"https://blogz.site"},"example":"https://blogz.site"}],"description":"Dashboard session only. Maximum 20 active tokens per account. The raw token is returned exactly once; only a SHA-256 digest is stored. Omit blog_slug to access all blogs you own.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenInput"}}}},"responses":{"201":{"description":"Copy the token now; it will not be shown again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedToken"}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The slug is already taken or the active-token limit was reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body exceeds the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/account/tokens/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"delete":{"tags":["API tokens"],"operationId":"revokeApiToken","summary":"Revoke a token immediately","security":[{"cookieAuth":[]}],"parameters":[{"name":"Origin","in":"header","required":true,"schema":{"type":"string","const":"https://blogz.site"},"example":"https://blogz.site"}],"description":"Requires a dashboard session that owns the token. Revoked credentials cannot authenticate subsequent requests.","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","required":["revoked"],"properties":{"revoked":{"type":"boolean","const":true}}}}}},"401":{"description":"Missing, invalid, expired, or revoked credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Read-only token, publication scope restriction, or disallowed browser Origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The resource does not exist or is not owned by this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{"blogEvent":{"post":{"tags":["Webhooks"],"operationId":"receiveBlogEvent","summary":"A blog changed","description":"blogz.site POSTs this JSON to the blog webhook after article create/update/delete, author create/update/delete, and blog setting changes (visibility, theme, operator metadata), unless the write used `?notify=false`. A cron every 5 minutes sends `article.published` once for each scheduled article whose future publication time has passed. Each delivery is one attempt with a 5-second timeout and no redirects; failures are logged and never fail the write. Verify `X-Blogz-Signature` against the raw body before trusting a delivery.","parameters":[{"name":"X-Blogz-Event","in":"header","required":true,"schema":{"type":"string"},"example":"article.updated"},{"name":"X-Blogz-Signature","in":"header","required":false,"schema":{"type":"string","pattern":"^sha256=[a-f0-9]{64}$"},"description":"HMAC-SHA256 of the raw request body with the webhook secret, hex encoded. Sent only when a secret is configured."},{"name":"X-Blogz-Delivery","in":"header","required":true,"schema":{"type":"string","format":"uuid"},"description":"Unique per delivery attempt."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Any 2xx response counts as delivered."}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"blogz.site API token","description":"A user-generated API token from Dashboard → API tokens. Shown only once; do not share it."},"cookieAuth":{"type":"apiKey","in":"cookie","name":"__Host-blogz.session_token","description":"Better Auth dashboard session. Browser cookies are managed by the login flow, never by API tokens."},"operatorAuth":{"type":"http","scheme":"bearer","bearerFormat":"operator PUBLISH_TOKEN","description":"The deployment operator’s publishing token for /api/v1. User API tokens and dashboard sessions are rejected."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}},"example":{"error":{"code":"UNAUTHORIZED","message":"Use a valid, unexpired API token."}}},"DailyPostQuota":{"type":"object","required":["limit","used","remaining","resets_at"],"properties":{"limit":{"type":"integer","minimum":1,"maximum":100000,"default":3,"description":"New posts this account may create per UTC calendar day: 3 unless the operator changed this account’s limit."},"used":{"type":"integer","minimum":0,"description":"New posts created today across all owned blogs; includes drafts and deleted posts. It can exceed limit after the operator lowers a limit."},"remaining":{"type":"integer","minimum":0},"resets_at":{"type":"string","format":"date-time","description":"The next midnight UTC, when the creation allowance resets."}},"example":{"limit":3,"used":3,"remaining":0,"resets_at":"2026-10-06T00:00:00.000Z"}},"ArticleCreateQuotaError":{"type":"object","required":["error","quota"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","const":"POST_DAILY_LIMIT"},"message":{"type":"string"}}},"quota":{"$ref":"#/components/schemas/DailyPostQuota"}},"example":{"error":{"code":"POST_DAILY_LIMIT","message":"You have reached your limit of 3 new posts per UTC day. You can still edit existing posts. New posts are available again at 00:00 UTC."},"quota":{"limit":3,"used":3,"remaining":0,"resets_at":"2026-10-06T00:00:00.000Z"}}},"WeeklyPublicationQuota":{"type":"object","required":["period","limit","used","remaining","starts_at","resets_at"],"properties":{"period":{"type":"string","const":"week"},"limit":{"type":"integer","minimum":1,"maximum":700000,"default":1},"used":{"type":"integer","minimum":0,"description":"Publications charged or reserved in the requested week, including deleted or unpublished articles."},"remaining":{"type":"integer","minimum":0},"starts_at":{"type":"string","format":"date-time","description":"Monday at 00:00 UTC starting the requested publication week."},"resets_at":{"type":"string","format":"date-time","description":"The next Monday at 00:00 UTC."}},"example":{"period":"week","limit":1,"used":1,"remaining":0,"starts_at":"2026-10-05T00:00:00.000Z","resets_at":"2026-10-12T00:00:00.000Z"}},"WeeklyPublicationQuotaError":{"type":"object","required":["error","quota","contact_url"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","const":"PUBLICATION_WEEKLY_LIMIT"},"message":{"type":"string"}}},"quota":{"$ref":"#/components/schemas/WeeklyPublicationQuota"},"contact_url":{"type":"string","const":"mailto:admin@blogz.site"}}},"AccountPublicationLimitInput":{"type":"object","additionalProperties":false,"required":["weekly_publication_limit"],"properties":{"weekly_publication_limit":{"type":"integer","minimum":1,"maximum":700000}},"example":{"weekly_publication_limit":10}},"AccountPublicationLimit":{"type":"object","required":["account","weekly_publication_limit","quota"],"properties":{"account":{"type":"object","required":["id","email","name"],"properties":{"id":{"type":"string"},"email":{"type":["string","null"],"format":"email"},"name":{"type":"string"}}},"weekly_publication_limit":{"type":"integer","minimum":1,"maximum":700000},"quota":{"$ref":"#/components/schemas/WeeklyPublicationQuota"}}},"AccountPostLimitInput":{"type":"object","additionalProperties":false,"required":["daily_post_limit"],"properties":{"daily_post_limit":{"type":"integer","minimum":1,"maximum":100000,"description":"New posts per UTC calendar day for this account."}},"example":{"daily_post_limit":2000}},"AccountPostLimit":{"type":"object","required":["account","daily_post_limit","quota"],"properties":{"account":{"type":"object","required":["id","email","name"],"properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"}}},"daily_post_limit":{"type":"integer","minimum":1,"maximum":100000},"quota":{"$ref":"#/components/schemas/DailyPostQuota"}},"example":{"account":{"id":"account-id","email":"writer@example.com","name":"Writer"},"daily_post_limit":2000,"quota":{"limit":2000,"used":12,"remaining":1988,"resets_at":"2026-10-07T00:00:00.000Z"}}},"User":{"type":"object","required":["id","name","email"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","format":"email"}},"example":{"id":"account-id","name":"Writer","email":"writer@example.com"}},"BlogInput":{"type":"object","required":["slug","title"],"properties":{"slug":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"title":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","maxLength":1000,"default":""},"public_enabled":{"type":"boolean","default":true,"description":"Whether blogz.site serves the public publication website. False makes the blog headless: authenticated CMS access continues, while public pages, discovery, feeds, sitemaps, and social images are unavailable. This does not change article status or delete content."}},"example":{"slug":"my-notebook","title":"My Notebook","description":"Ideas worth keeping.","public_enabled":true}},"BlogVisibilityInput":{"type":"object","additionalProperties":false,"required":["public_enabled"],"properties":{"public_enabled":{"type":"boolean","default":true,"description":"Whether blogz.site serves the public publication website. False makes the blog headless: authenticated CMS access continues, while public pages, discovery, feeds, sitemaps, and social images are unavailable. This does not change article status or delete content."}},"example":{"public_enabled":false}},"Blog":{"type":"object","required":["id","slug","title","description","author","accent","public_enabled","created_at","updated_at","url"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"author":{"type":"string"},"accent":{"type":"string","pattern":"^#[a-fA-F0-9]{6}$"},"public_enabled":{"type":"boolean","default":true,"description":"Whether blogz.site serves the public publication website. False makes the blog headless: authenticated CMS access continues, while public pages, discovery, feeds, sitemaps, and social images are unavailable. This does not change article status or delete content."},"owner_id":{"type":["string","null"]},"theme":{"anyOf":[{"$ref":"#/components/schemas/Theme"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"url":{"type":"string","format":"uri","description":"The reserved publication address. Public reading is unavailable when public_enabled is false.","example":"https://sample.blogz.site"}}},"ArticleInput":{"type":"object","required":["title","markdown"],"properties":{"title":{"type":"string","minLength":1,"maxLength":240,"example":"A small place for good ideas"},"slug":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-z0-9]+(?:-+[a-z0-9]+)*$","description":"Lowercase letters and numbers separated by one or more hyphens, up to 160 characters, so imported URLs such as `april-2026----everything-that-changed` keep their address. Slugs derived from a title use single hyphens. Blog slugs stay stricter because they are subdomain labels.","example":"a-small-place-for-good-ideas"},"markdown":{"type":"string","minLength":1,"maxLength":950000,"example":"## A place to begin\n\nA blog is a small place to keep the ideas you want to return to."},"excerpt":{"type":"string","maxLength":500,"description":"If omitted, a plain-text excerpt is derived from the Markdown.","example":"A small place to keep the ideas you want to return to."},"tags":{"type":"array","maxItems":20,"items":{"type":"string","minLength":1,"maxLength":48},"default":[],"example":["Writing"]},"keywords":{"type":"array","maxItems":12,"items":{"type":"string","minLength":1,"maxLength":80},"description":"Plain-text keywords or short phrases. Case-insensitive duplicates are removed. Missing or empty keywords are generated when saving.","example":["personal blogging","writing practice","good ideas"]},"status":{"type":"string","enum":["draft","published"],"default":"draft"},"featured":{"type":"boolean","default":false},"cover_style":{"type":"string","pattern":"^[a-z][a-z0-9-]{0,39}$","default":"sage"},"published_at":{"type":["string","null"],"format":"date-time","description":"Articles are publicly visible only when the blog has public_enabled true, their status is published, and their date is no later than now. A missing date on publication uses the current time. PUT preserves an existing publication date if omitted."},"author":{"type":["string","null"],"minLength":1,"maxLength":160,"pattern":"^[a-z0-9]+(?:-+[a-z0-9]+)*$","description":"Slug of an author in the same blog (see the Authors endpoints). Unknown or foreign slugs return 400. Null or omitted uses the blog’s default author on public pages, falling back to its author string when no default is set. Responses keep the explicit slug or null.","default":null,"example":"jane-doe"},"cover_image_url":{"type":["string","null"],"format":"uri","maxLength":2048,"default":null,"description":"Absolute https:// image URL, for example from the media upload endpoints. Shown above the article and used as og:image on publication pages.","example":"https://blogz.site/media/sample/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.webp"},"cover_image_alt":{"type":["string","null"],"maxLength":300,"default":null,"description":"Plain-text alternative text for the cover image."},"seo_title":{"type":["string","null"],"maxLength":240,"default":null,"description":"Overrides the HTML title and og:title when set. The visible headline stays `title`."},"meta_description":{"type":["string","null"],"maxLength":500,"default":null,"description":"Used verbatim as the meta, Open Graph, and JSON-LD description when set. `excerpt` stays the card summary."},"faq":{"type":"array","maxItems":30,"default":[],"items":{"$ref":"#/components/schemas/FaqEntry"},"description":"Plain-text questions and answers rendered after the article, with FAQPage JSON-LD, on publication pages."},"structured_data":{"type":["object","null"],"additionalProperties":true,"default":null,"description":"Extra schema.org properties for the article’s BlogPosting node, as a JSON object of at most 16 KiB (16,384 bytes) serialized, with finite numbers and at most 32 levels of nesting; arrays and scalars are rejected. In front matter, write it as a YAML mapping. Every article read and the Markdown export return it unchanged. Publication pages merge it into their own JSON-LD: it can add properties and refine `@type` (for example `NewsArticle`), but it never replaces the page’s own properties such as `@id`, `url`, `headline`, dates, `author`, or `image`; `@context` and `@graph` are ignored.","example":{"@type":"NewsArticle","about":[{"@type":"Thing","name":"Google Ads"}],"mentions":[{"@type":"Organization","name":"Google"}],"speakable":{"@type":"SpeakableSpecification","cssSelector":["h1"]}}},"updated_at":{"type":"string","format":"date-time","writeOnly":true,"description":"For imports only: preserve the original modification time (ISO-8601 with an offset, not in the future). Omit it for normal edits so the write time is recorded; sending back the article’s current updated_at unchanged (a read-and-write-back) also counts as a normal edit. It is not part of the Markdown export."}}},"Article":{"type":"object","required":["id","blog_id","slug","title","markdown","excerpt","tags","keywords","status","featured","cover_style","reading_minutes","created_at","updated_at","published_at","author","cover_image_url","cover_image_alt","seo_title","meta_description","faq","structured_data","url"],"properties":{"title":{"type":"string","minLength":1,"maxLength":240,"example":"A small place for good ideas"},"slug":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-z0-9]+(?:-+[a-z0-9]+)*$","description":"Lowercase letters and numbers separated by one or more hyphens, up to 160 characters, so imported URLs such as `april-2026----everything-that-changed` keep their address. Slugs derived from a title use single hyphens. Blog slugs stay stricter because they are subdomain labels.","example":"a-small-place-for-good-ideas"},"markdown":{"type":"string","minLength":1,"maxLength":950000,"example":"## A place to begin\n\nA blog is a small place to keep the ideas you want to return to."},"excerpt":{"type":"string","maxLength":500,"description":"If omitted, a plain-text excerpt is derived from the Markdown.","example":"A small place to keep the ideas you want to return to."},"tags":{"type":"array","maxItems":20,"items":{"type":"string","minLength":1,"maxLength":48},"default":[],"example":["Writing"]},"keywords":{"type":"array","maxItems":12,"items":{"type":"string","minLength":1,"maxLength":80},"description":"Plain-text keywords or short phrases. Case-insensitive duplicates are removed. Missing or empty keywords are generated when saving.","example":["personal blogging","writing practice","good ideas"]},"status":{"type":"string","enum":["draft","published"],"default":"draft"},"featured":{"type":"boolean","default":false},"cover_style":{"type":"string","pattern":"^[a-z][a-z0-9-]{0,39}$","default":"sage"},"published_at":{"type":["string","null"],"format":"date-time","description":"Articles are publicly visible only when the blog has public_enabled true, their status is published, and their date is no later than now. A missing date on publication uses the current time. PUT preserves an existing publication date if omitted."},"author":{"type":["string","null"],"minLength":1,"maxLength":160,"pattern":"^[a-z0-9]+(?:-+[a-z0-9]+)*$","description":"Slug of an author in the same blog (see the Authors endpoints). Unknown or foreign slugs return 400. Null or omitted uses the blog’s default author on public pages, falling back to its author string when no default is set. Responses keep the explicit slug or null.","default":null,"example":"jane-doe"},"cover_image_url":{"type":["string","null"],"format":"uri","maxLength":2048,"default":null,"description":"Absolute https:// image URL, for example from the media upload endpoints. Shown above the article and used as og:image on publication pages.","example":"https://blogz.site/media/sample/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.webp"},"cover_image_alt":{"type":["string","null"],"maxLength":300,"default":null,"description":"Plain-text alternative text for the cover image."},"seo_title":{"type":["string","null"],"maxLength":240,"default":null,"description":"Overrides the HTML title and og:title when set. The visible headline stays `title`."},"meta_description":{"type":["string","null"],"maxLength":500,"default":null,"description":"Used verbatim as the meta, Open Graph, and JSON-LD description when set. `excerpt` stays the card summary."},"faq":{"type":"array","maxItems":30,"default":[],"items":{"$ref":"#/components/schemas/FaqEntry"},"description":"Plain-text questions and answers rendered after the article, with FAQPage JSON-LD, on publication pages."},"structured_data":{"type":["object","null"],"additionalProperties":true,"default":null,"description":"Extra schema.org properties for the article’s BlogPosting node, as a JSON object of at most 16 KiB (16,384 bytes) serialized, with finite numbers and at most 32 levels of nesting; arrays and scalars are rejected. In front matter, write it as a YAML mapping. Every article read and the Markdown export return it unchanged. Publication pages merge it into their own JSON-LD: it can add properties and refine `@type` (for example `NewsArticle`), but it never replaces the page’s own properties such as `@id`, `url`, `headline`, dates, `author`, or `image`; `@context` and `@graph` are ignored.","example":{"@type":"NewsArticle","about":[{"@type":"Thing","name":"Google Ads"}],"mentions":[{"@type":"Organization","name":"Google"}],"speakable":{"@type":"SpeakableSpecification","cssSelector":["h1"]}}},"id":{"type":"string"},"blog_id":{"type":"string"},"reading_minutes":{"type":"integer","minimum":1,"readOnly":true},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true},"blog_slug":{"type":"string"},"blog_title":{"type":"string"},"blog_author":{"type":"string"},"blog_accent":{"type":"string"},"blog_public_enabled":{"type":"boolean","readOnly":true,"description":"The parent blog’s public-site visibility. False keeps this manuscript available only through authenticated CMS access, regardless of article status."},"url":{"type":"string","format":"uri","description":"The article address. Public reading requires an enabled public site, published status, and a publication date no later than now.","example":"https://sample.blogz.site/a-small-place-for-good-ideas"}}},"FaqEntry":{"type":"object","required":["question","answer"],"properties":{"question":{"type":"string","minLength":1,"maxLength":500,"description":"Plain text on one line."},"answer":{"type":"string","minLength":1,"maxLength":5000,"description":"Plain text. Blank lines separate paragraphs; single line breaks are kept."}},"example":{"question":"How often should I post?","answer":"Whenever you have something worth saying."}},"AuthorInput":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"slug":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-z0-9]+(?:-+[a-z0-9]+)*$","description":"Ignored: the route slug wins."},"id":{"type":"string","description":"Ignored, so a fetched author can be sent back as is."},"blog_id":{"type":"string","description":"Ignored."},"created_at":{"type":"string","description":"Ignored."},"updated_at":{"type":"string","description":"Ignored."},"name":{"type":"string","minLength":1,"maxLength":120},"job_title":{"type":["string","null"],"maxLength":120},"bio":{"type":["string","null"],"maxLength":2000,"description":"Optional plain-text introduction for author cards. Leave blank to derive an excerpt from bio_markdown."},"bio_markdown":{"type":["string","null"],"maxLength":950000,"description":"Markdown biography, limited to 30,000 whitespace-delimited words and 950,000 characters within the 1 MiB request body limit. Rendered with sanitized HTML on the author profile page. Null or omitted clears it."},"profile_enabled":{"type":"boolean","default":true,"description":"Publish the biography page, author card, and social links. False hides them, including sameAs in public JSON-LD. The byline name, role, and portrait remain visible. Headless blogs keep all profiles private."},"is_default":{"type":"boolean","default":false,"description":"Use this author for articles without an explicit author. Setting true atomically replaces the previous default in this blog; explicit article authors are preserved."},"image_url":{"type":["string","null"],"format":"uri","maxLength":2048,"description":"Absolute https:// portrait URL."},"image_alt":{"type":["string","null"],"maxLength":300},"email":{"type":["string","null"],"format":"email","maxLength":254,"description":"Returned only by authenticated APIs; never rendered on public pages."},"links":{"type":"array","maxItems":10,"default":[],"items":{"type":"string","format":"uri","maxLength":2048},"description":"Absolute https:// social or website URLs. Duplicates are removed. Shown on the profile and article author card, and as schema.org sameAs, while profile_enabled is true."}},"example":{"name":"Jane Doe","job_title":"Head of Paid Search","bio":"Writes about search advertising.","image_url":"https://blogz.site/media/sample/2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae.jpg","image_alt":"Jane Doe","links":["https://www.linkedin.com/in/jane-doe"]}},"Author":{"type":"object","required":["id","blog_id","slug","name","job_title","bio","bio_markdown","profile_enabled","is_default","image_url","image_alt","email","links","created_at","updated_at"],"properties":{"id":{"type":"string"},"blog_id":{"type":"string"},"slug":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-z0-9]+(?:-+[a-z0-9]+)*$","description":"Lowercase letters and numbers separated by one or more hyphens, up to 160 characters, so imported URLs such as `april-2026----everything-that-changed` keep their address. Slugs derived from a title use single hyphens. Blog slugs stay stricter because they are subdomain labels."},"name":{"type":"string"},"job_title":{"type":["string","null"]},"bio":{"type":["string","null"]},"bio_markdown":{"type":["string","null"],"maxLength":950000,"description":"The stored Markdown biography."},"profile_enabled":{"type":"boolean"},"is_default":{"type":"boolean"},"image_url":{"type":["string","null"]},"image_alt":{"type":["string","null"]},"email":{"type":["string","null"]},"links":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"DailyMediaQuota":{"type":"object","required":["files_limit","files_used","files_remaining","bytes_limit","bytes_used","bytes_remaining","resets_at"],"properties":{"files_limit":{"type":"integer","minimum":1,"maximum":100000,"default":50},"files_used":{"type":"integer","minimum":0},"files_remaining":{"type":"integer","minimum":0},"bytes_limit":{"type":"integer","minimum":1048576,"maximum":1099511627776,"default":104857600},"bytes_used":{"type":"integer","minimum":0},"bytes_remaining":{"type":"integer","minimum":0},"resets_at":{"type":"string","format":"date-time","description":"The next midnight UTC."}},"example":{"files_limit":50,"files_used":50,"files_remaining":0,"bytes_limit":104857600,"bytes_used":31457280,"bytes_remaining":73400320,"resets_at":"2026-10-07T00:00:00.000Z"}},"MediaQuotaError":{"type":"object","required":["error","quota"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","const":"MEDIA_DAILY_LIMIT"},"message":{"type":"string"}}},"quota":{"$ref":"#/components/schemas/DailyMediaQuota"}}},"AccountMediaLimitInput":{"type":"object","additionalProperties":false,"required":["daily_media_limit","daily_media_bytes_limit"],"properties":{"daily_media_limit":{"type":"integer","minimum":1,"maximum":100000,"description":"New media files per UTC calendar day."},"daily_media_bytes_limit":{"type":"integer","minimum":1048576,"maximum":1099511627776,"description":"Bytes of new media per UTC calendar day (1 MiB to 1 TiB)."}},"example":{"daily_media_limit":5000,"daily_media_bytes_limit":21474836480}},"AccountMediaLimit":{"type":"object","required":["account","daily_media_limit","daily_media_bytes_limit","quota"],"properties":{"account":{"type":"object","required":["id","email","name"],"properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"}}},"daily_media_limit":{"type":"integer"},"daily_media_bytes_limit":{"type":"integer"},"quota":{"$ref":"#/components/schemas/DailyMediaQuota"}}},"MediaImportInput":{"type":"object","additionalProperties":false,"required":["source_url"],"properties":{"source_url":{"type":"string","format":"uri","maxLength":2048,"description":"A public https:// image URL."}},"example":{"source_url":"https://cdn.prod.website-files.com/site-id/cover.webp"}},"MediaVariant":{"type":"object","required":["content_type","url","bytes"],"properties":{"content_type":{"type":"string","enum":["image/avif","image/webp"]},"url":{"type":"string","format":"uri","description":"The original URL plus `.avif` or `.webp`."},"bytes":{"type":"integer","minimum":1}}},"MediaObject":{"type":"object","required":["url","key","content_type","bytes","sha256","width","height","variants"],"properties":{"url":{"type":"string","format":"uri","description":"The public, immutable URL on the apex: /media/{blog}/{key}. Use it as cover_image_url, an author image_url, or in Markdown."},"key":{"type":"string","pattern":"^[a-f0-9]{64}\\.(?:png|jpg|webp|avif|gif)$"},"content_type":{"type":"string","enum":["image/png","image/jpeg","image/webp","image/avif","image/gif"]},"bytes":{"type":"integer","minimum":1,"maximum":10485760},"sha256":{"type":"string","pattern":"^[a-f0-9]{64}$"},"width":{"type":"integer","minimum":1,"maximum":16384,"description":"Pixel width read from the file header, for layout without downloading the image."},"height":{"type":"integer","minimum":1,"maximum":16384},"variants":{"type":"array","items":{"$ref":"#/components/schemas/MediaVariant"},"description":"Stored AVIF and WebP renditions, smallest format first; empty when none is smaller, the image is animated, or encoding is still pending (the cron retries)."}},"example":{"url":"https://blogz.site/media/sample/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.png","key":"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.png","content_type":"image/png","bytes":48213,"sha256":"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08","width":1600,"height":900,"variants":[{"content_type":"image/avif","url":"https://blogz.site/media/sample/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.png.avif","bytes":9120},{"content_type":"image/webp","url":"https://blogz.site/media/sample/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.png.webp","bytes":14760}]}},"Webhook":{"type":"object","required":["webhook_url","webhook_secret_configured","updated_at"],"properties":{"webhook_url":{"type":["string","null"],"format":"uri"},"webhook_secret_configured":{"type":"boolean","description":"Whether deliveries are signed. The secret itself is never returned."},"updated_at":{"type":["string","null"],"format":"date-time"}},"example":{"webhook_url":"https://api.cloudflare.com/client/v4/pages/webhooks/deploy_hooks/your-hook-id","webhook_secret_configured":true,"updated_at":"2026-10-06T12:00:00.000Z"}},"WebhookInput":{"type":"object","additionalProperties":false,"required":["webhook_url"],"properties":{"webhook_url":{"type":["string","null"],"format":"uri","maxLength":2048,"description":"A public https:// URL, or null to remove the webhook."},"webhook_secret":{"type":["string","null"],"minLength":16,"maxLength":256,"pattern":"^[\\x21-\\x7e]+$","writeOnly":true,"description":"Omit to keep the current secret (even when webhook_url changes); null clears it."}},"example":{"webhook_url":"https://example.com/hooks/blogz","webhook_secret":"a-long-random-shared-secret"}},"WebhookEvent":{"type":"object","required":["event","blog","occurred_at"],"properties":{"event":{"type":"string","enum":["article.created","article.updated","article.deleted","article.published","author.created","author.updated","author.deleted","blog.updated"]},"blog":{"type":"string","description":"The blog slug."},"article":{"type":"object","required":["slug","status","published_at","updated_at"],"properties":{"slug":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-z0-9]+(?:-+[a-z0-9]+)*$","description":"Lowercase letters and numbers separated by one or more hyphens, up to 160 characters, so imported URLs such as `april-2026----everything-that-changed` keep their address. Slugs derived from a title use single hyphens. Blog slugs stay stricter because they are subdomain labels."},"status":{"type":"string","enum":["draft","published"]},"published_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"description":"Present for article events."},"author":{"type":"object","required":["slug"],"properties":{"slug":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-z0-9]+(?:-+[a-z0-9]+)*$","description":"Lowercase letters and numbers separated by one or more hyphens, up to 160 characters, so imported URLs such as `april-2026----everything-that-changed` keep their address. Slugs derived from a title use single hyphens. Blog slugs stay stricter because they are subdomain labels."}},"description":"Present for author events."},"occurred_at":{"type":"string","format":"date-time"}},"example":{"event":"article.published","blog":"sample","article":{"slug":"a-small-place-for-good-ideas","status":"published","published_at":"2026-10-07T09:00:00.000Z","updated_at":"2026-10-06T16:20:00.000Z"},"occurred_at":"2026-10-07T09:05:00.000Z"}},"Theme":{"type":"object","additionalProperties":false,"required":["preset","accent","background","text","font","layout"],"properties":{"preset":{"type":"string","enum":["journal","editorial","studio","terminal"]},"accent":{"type":"string","pattern":"^#[a-fA-F0-9]{6}$"},"background":{"type":"string","pattern":"^#[a-fA-F0-9]{6}$"},"text":{"type":"string","pattern":"^#[a-fA-F0-9]{6}$"},"font":{"type":"string","enum":["serif","sans","mono"]},"layout":{"type":"string","enum":["list","grid"]}},"example":{"preset":"editorial","accent":"#a43124","background":"#fffdf7","text":"#191919","font":"serif","layout":"grid"}},"KeywordSource":{"type":"object","required":["title","markdown"],"properties":{"title":{"type":"string","minLength":1,"maxLength":240,"example":"A small place for good ideas"},"markdown":{"type":"string","minLength":1,"maxLength":950000,"example":"## A place to begin\n\nA blog is a small place to keep the ideas you want to return to."},"excerpt":{"type":"string","maxLength":500,"description":"If omitted, a plain-text excerpt is derived from the Markdown.","example":"A small place to keep the ideas you want to return to."}}},"KeywordResult":{"type":"object","required":["keywords","source"],"properties":{"keywords":{"type":"array","maxItems":12,"items":{"type":"string","minLength":1,"maxLength":80},"description":"Plain-text keywords or short phrases. Case-insensitive duplicates are removed. Missing or empty keywords are generated when saving.","example":["personal blogging","writing practice","good ideas"]},"source":{"type":"string","enum":["openrouter","local"]},"reason":{"type":"string","enum":["not_configured","unavailable"],"description":"Present only for the local fallback."}},"example":{"keywords":["personal blogging","writing practice"],"source":"openrouter"}},"MarkdownPreview":{"type":"object","required":["html","toc"],"properties":{"html":{"type":"string"},"toc":{"type":"array","items":{"type":"object","required":["id","text","level"],"properties":{"id":{"type":"string"},"text":{"type":"string"},"level":{"type":"integer","minimum":1,"maximum":6}}}}}},"TokenInput":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80},"scope":{"type":"string","enum":["read","write"],"default":"write"},"blog_slug":{"type":"string","minLength":3,"maxLength":63,"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","description":"Optional owned publication slug. Omit to access all owned blogs."},"expires_in_days":{"type":"integer","enum":[30,90,365,3650,7300],"minimum":30,"maximum":7300,"default":90,"description":"Fixed days from creation: 30 days, 90 days, 1 year (365), 10 years (3650), or 20 years (7300)."}},"example":{"name":"Publishing script","scope":"write","blog_slug":"sample","expires_in_days":90}},"TokenMetadata":{"type":"object","required":["id","name","prefix","scope","blog_slug","created_at","expires_at","last_used_at","revoked_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","maxLength":80},"prefix":{"type":"string","example":"bz_live_a1b2c3d4"},"scope":{"type":"string","enum":["read","write"]},"blog_slug":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"last_used_at":{"type":["string","null"],"format":"date-time"},"revoked_at":{"type":["string","null"],"format":"date-time"}}},"CreatedToken":{"type":"object","required":["token","apiToken"],"properties":{"token":{"type":"string","readOnly":true,"description":"The bearer credential. Returned only in this creation response; copy it now.","example":"bz_live_REPLACE_WITH_YOUR_NEW_TOKEN"},"apiToken":{"$ref":"#/components/schemas/TokenMetadata"}}}}}}