Delivery API

The public, unauthenticated surface that serves your published content. Everything is keyed by your site's public key (pk_…) and works only for verified sites. GET and HEAD are supported everywhere.

Base origin: https://api.dailysmith.com.

Rendered pages

GET /v1/render/{publicKey}/{path}

{path} is the path as it appears on your site — e.g. /blog for the index, /blog/my-post-slug for a post. baas resolves which blog owns the path by its base path, so proxy, SDK, and prerender all share one URL scheme.

Query paramMeaning
format=fragmentJSON fragment for the SDK instead of a full HTML page
page=NIndex pagination (10 posts per page)

A rendered page is always served in its blog's own language — there is no locale parameter here. A blog holds one language, so the only thing another value could produce is an empty page declaring a language it has nothing in, which is not worth handing to a crawler. Run a second blog if you publish in a second language. The structured listing below does take locale, because there an explicit empty answer to "do you have a German edition?" is useful.

Full pages are complete HTML documents with title, meta description, Open Graph and Twitter card tags (including og:image when the post has a featured image), canonical URL, JSON-LD, a readable default style, and your theme palette inlined.

Fragments (format=fragment) return:

{
  "html": "<article class=\"baas-post\">…</article>",
  "kind": "post",
  "title": "Post title",
  "description": "Meta description",
  "canonical": "https://example.com/blog/my-post-slug",
  "json_ld": "{…}",
  "locale": "en",
  "og_locale": "en_US",
  "css": "/* the blog's theme palette, only when one is set */",
  "css_href": "/v1/sdk/md2.css",
  "hero_image_url": "https://api.dailysmith.com/v1/media/01a01789-959f-78f7-a5d0-78fc208903f6",
  "hero_image_alt": "Abstract illustration for Post title",
  "hero_image_width": 1376,
  "hero_image_height": 768
}

The four hero_image_* fields are omitted entirely for a post with no featured image, which is every post written before you turned them on. The picture is also already inside html as a <img class="baas-hero"> — one renderer serves every delivery mode, so you get it for free by inserting the fragment. The separate fields exist so you can build a social card or a listing thumbnail without parsing the markup; our own SDK reads them to set og:image and clears those tags again when it navigates to a post that has none. The image is served from https://api.dailysmith.com/v1/media/{id} — permanent, immutable, and public by id, so it needs no key and can be cached forever.

locale is the blog's language as a plain tag — put it on the element you insert the fragment into, or the article is announced by a screen reader in whatever language your own page declares. og_locale is the same language in the language_TERRITORY form Open Graph wants; set it as <meta property="og:locale"> or a share card is assumed to be en_US. Our SDK always puts lang on the fragment it injects, and sets og:locale too unless you opted out of head management.

css and css_href are both omitted when they don't apply, rather than sent empty: css carries only the blog's theme palette — never the component stylesheet — so it's absent with no active theme; css_href points at the shared, cacheable stylesheet below and is present only when the content actually uses MD2 components. Full pages inline both instead of linking them. For the index (kind: "index"), description and json_ld are always empty strings rather than omitted.

Structured listings

GET /v1/delivery/{publicKey}/blogs
GET /v1/delivery/{publicKey}/posts?base_path=/blog&locale=en&page=1

blogs lists the site's blogs (name, base path, default locale). posts lists published posts for one blog — slug, title, excerpt, site path, publish date, reading minutes — paginated (the response also carries page and total_pages) — for building your own index UI or feeding other systems.

Sync feed

GET /v1/delivery/{publicKey}/posts/updated?base_path=/blog&since=2026-08-19T09:30:00Z
GET /v1/delivery/{publicKey}/posts/updated?base_path=/blog&since=…&cursor=MjAyNi0…

The polling feed for systems that keep their own copy of your posts — it is what the WordPress plugin syncs from. Entries are ordered by updated_at ascending and carry everything a mirror needs in one request: id (the stable post identifier — the same value the WordPress push writes as the _baas_post_id marker), slug, title, excerpt, path, published_at, updated_at, reading_minutes, the rendered body_html, a seo object (title_tag, meta_description), the brief's focus_keyword, and hero_image_url / hero_image_alt when the post has a featured image. The envelope carries the blog-level styling once — theme_css (the palette, already scoped to .baas-wrap) and css_href (resolve it against this API's origin, like the fragment field) — plus next_cursor, page, total_pages, base_path, and locale.

since is the entry point and is compared with >=: remember the newest updated_at you saw, and send it back next time. The boundary post is re-listed on the next poll by design — skip entries whose updated_at you already hold. An absent or unparseable since returns everything, so a lost checkpoint means a slow poll, never a stuck one.

next_cursor is how you get the rest of one walk. It is an opaque position — send it back as cursor, unchanged, and the next response picks up immediately after the last entry you were handed. Keep following it until a response arrives without one; that absence means you have reached the end, and the newest updated_at you collected is your next checkpoint. An unreadable cursor is ignored rather than refused: the walk restarts from since, which re-lists more than you need and stays correct.

Archived posts drop out of the feed like every other endpoint here; a mirror that also prunes should periodically walk the feed without since and withdraw local copies whose id no longer appears.

Feeds

GET /v1/delivery/{publicKey}/sitemap.xml
GET /v1/delivery/{publicKey}/rss.xml

The sitemap covers blog indexes and every published post with last-modified dates; RSS carries the 50 most recent posts. Reference the sitemap from your robots.txt:

Sitemap: https://api.dailysmith.com/v1/delivery/pk_YOUR_KEY/sitemap.xml

Each blog also serves an llms.txt index for AI crawlers through the render path — {base_path}/llms.txt, e.g. GET /v1/render/{publicKey}/blog/llms.txt — listing every published post with its canonical URL and a one-line description.

SDK script

GET /v1/sdk/blog.js
GET /v1/sdk/md2.css

The JS SDK and the MD2 rich-content stylesheet a fragment's css_href points at — both served with long-lived cache headers.

Errors

Every error is problem+json (RFC 9457):

{ "type": "about:blank", "title": "Not found", "status": 404, "detail": "…" }
WhereStatusdetailMeaning
Any endpoint404unknown or unverified site keyThe key doesn't exist, or the site hasn't passed verification — indistinguishable on purpose, so a key can't be used to probe which sites exist.
/v1/render/{publicKey}/{path}404no blog is mounted at this pathNo blog's base path matches {path}.
/v1/render/{publicKey}/{path}404no such postThe path resolved to a blog, but the slug doesn't.
/v1/delivery/{publicKey}/posts404no blog at that base pathThe base_path query param doesn't match any blog.
/v1/sdk/md2.css503stylesheet temporarily unavailableThe rendering sidecar is unreachable — retry.

Nothing on this surface ever returns 402 or 422 — quota and validation errors only apply to the authenticated content API that creates posts.

Caching semantics

  • Every response carries a strong ETag (a content hash) and honors If-None-Match with 304 Not Modified.
  • Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600 — friendly to CDNs and your edge cache; content changes (a new post, an edited theme) change the ETag.
  • CORS is wide open (Access-Control-Allow-Origin: *): the content is public by definition, which is what lets the SDK fetch it from your domain.
enro