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 param | Meaning |
|---|---|
format=fragment | JSON fragment for the SDK instead of a full HTML page |
page=N | Index 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": "…" }
| Where | Status | detail | Meaning |
|---|---|---|---|
| Any endpoint | 404 | unknown or unverified site key | The 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} | 404 | no blog is mounted at this path | No blog's base path matches {path}. |
/v1/render/{publicKey}/{path} | 404 | no such post | The path resolved to a blog, but the slug doesn't. |
/v1/delivery/{publicKey}/posts | 404 | no blog at that base path | The base_path query param doesn't match any blog. |
/v1/sdk/md2.css | 503 | stylesheet temporarily unavailable | The 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-Matchwith304 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.