Public blog API
Publish your AI blog to your own website.
FounderReply generates and stores your SEO articles. Your site pulls the published ones over a single read API and renders them on your domain — with the schema.org structured data already attached. No webhooks to maintain, no scheduler to babysit.
- Read-only · no API key · CORS-open · edge-cached for five minutes
- Four endpoints: a list, an article, and an rss.xml / sitemap.xml pair for the crawlers
- Only posts you marked published are ever exposed
The model
A pull, not a push
FounderReply never reaches out to your site. Your site asks for posts when it builds. That keeps your blog fast, statically hosted, and independent — it keeps working even if this app is down between builds. Only posts you’ve marked published are ever exposed; drafts and archived posts stay private.
# list your published posts
curl "https://founderreply.com/api/public/blog?workspace_id=<your-workspace-id>"| Endpoint | Returns | Requires |
|---|---|---|
| GET /api/public/blog | Every published post, newest first, bodies omitted — twelve fields each, plus total, limit and offset. | workspace_id |
| GET /api/public/blog/[slug] | One article: the markdown body, embeddable schema.org JSON-LD, and the siblings worth linking to. | workspace_id · base_url optional |
| GET /api/public/blog/sitemap.xml | A urlset of every published post, rooted at your blog base, with honest lastmod. | workspace_id · base_url |
| GET /api/public/blog/rss.xml | RSS 2.0 with the full body in content:encoded — up to 50 items. | workspace_id · base_url |
All four are unauthenticated, CORS-open and edge-cached for five minutes (s-maxage=300, SWR 600), so they are cheap to call from a build step or straight from the browser. There is no write surface here at all: every route is a GET (plus its CORS preflight).
Step 1 · Pull
Pull your published posts
Two JSON endpoints do the work. The list gives you every published post with bodies omitted, so you can build an index and generate routes. The detail gives you one full article by slug, with the markdown body and ready-to-embed JSON-LD.
Your workspace id is what tells the API whose published posts to return. Copy it from your dashboard URL or settings and store it as an environment variable on your site. It is an unguessable identifier, it is not a secret in the credential sense, and it only ever surfaces public, published content — which is why none of these endpoints asks for a key.
FOUNDERREPLY_WORKSPACE_ID=<the uuid from your dashboard url># every published post, newest first
{
"ok": true,
"total": 24, "limit": 50, "offset": 0,
"posts": [
{ "id", "slug", "title", "type",
"keywords", "meta_description",
"cover_image_url", "created_at",
"published_at", "updated_at",
"intent", "visibility" }, …
]
}# one full article by slug
{
"ok": true,
"post": {
…all list fields,
"body_markdown": "…",
"json_ld": [ Article, FAQPage? ]
},
"related": [
{ "title", "slug", "url"? }, …
]
}Twelve fields per list item, and updated_atis the one to keep — it is what the sitemap’s lastmod is built from. related is scored on keyword overlap and gains an absolute url once you pass base_url — render those links server-side, because crawlers do not run your JavaScript.
- workspace_id
- Required on every endpoint. The UUID from your dashboard URL.There is no default workspace and no implicit one — missing or malformed is a 400.
- visibility
- listed (default) · unlisted · all — list endpoint only.Conquest pages are stored unlisted, so the default list omits them. Pass all if you want everything.
- limit / offset
- List endpoint. Default limit 50, hard cap 200; total is in the response.Page with offset once total exceeds your limit — a short list is not an error.
- base_url
- Your blog base, e.g. https://acme.com/blog. Detail, RSS and sitemap.On the detail endpoint it makes json_ld carry url/mainEntityOfPage and gives each related item an absolute url. RSS and sitemap require it, falling back to the blog base configured on your primary SEO site.
| Response body | Status | When, and what to do |
|---|---|---|
| workspace_id is required | 400 | All four endpoints. Nothing is implicit: there is no default workspace. |
| workspace_id must be a uuid | 400 | All four endpoints. The id is shape-checked before it reaches the database, so a typo comes back as a bad request with a message rather than a server error. |
| base_url is required (or configure the primary site blog_base_url) | 400 | sitemap.xml and rss.xml, when you pass no base_url and your primary SEO site has no blog base set. Better than guessing at a domain and emitting URLs that do not resolve. |
| blog post not found | 404 | The detail endpoint only. A draft, an archived post and a slug that never existed are all the same 404 — the API does not confirm that unpublished content exists. |
| too many requests | 429 | More than 120 requests a minute from one IP. A build that fans out one fetch per slug is the caller that finds this: pull the list once and stagger the details. |
| persistence not configured | 503 | The backing store is not available to this deployment. A build should fail loudly here rather than ship an empty index. |
Errors are never edge-cached, so a retry after a fix is answered fresh. Successful reads are, which is why a build that pulls the list once and then walks it usually stays well inside the rate limit.
Step 2 · Render
Render them on your domain, as fresh as you like
There is no schedule to configure inside FounderReply — you decide how often your site refreshes by choosing how it pulls. Pick whichever fits your stack, then generate a route per slug, render the markdown, and drop the JSON-LD into a script tag.
- Time-based (ISR)
- Set a revalidate window on the consuming page — the site re-pulls in the background on the next request after the window lapses. e.g. revalidate: 300 = at most 5 min stale.Simplest. Good default.
- Scheduled rebuild
- Run a cron (GitHub Action / Cloudflare cron / Vercel cron) that triggers a rebuild on a fixed cadence — nightly, hourly, whatever you choose. The site is 100% static between builds.Predictable, fully static.
- On-publish trigger
- Hit your host’s deploy hook the moment you mark a post published, so the new article goes live within a build cycle instead of waiting for a timer.Freshest. Event-driven.
// re-pull at most every 5 minutes
export const revalidate = 300const WS = process.env.FOUNDERREPLY_WORKSPACE_ID
const BASE = "https://founderreply.com/api/public/blog"
// one route per published slug
export async function generateStaticParams() {
const { posts } = await fetch(`${BASE}?workspace_id=${WS}`).then(r => r.json())
return posts.map((p) => ({ slug: p.slug }))
}
// render one article
export default async function Page({ params }) {
const { slug } = await params
const { post } = await fetch(`${BASE}/${slug}?workspace_id=${WS}`).then(r => r.json())
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(post.json_ld) }}
/>
<Markdown>{post.body_markdown}</Markdown>
</article>
)
}Use any markdown renderer (react-markdown, marked, MDX). The same pattern works in SvelteKit, Nuxt, Astro, or a plain build script — it is two GET requests. Pass base_url on the detail call and the JSON-LD comes back carrying your canonical URL, so you do not have to patch it in.
Going live
Mark it published, then point the crawlers at it
Two things are left, and neither is code you write: the gate that puts an article into the API at all, and the two XML surfaces that tell search engines the article exists.
What the public blog API will and will not serve
A post is generated, then published, then possibly archived. Only the middle state is visible here, and the transition into it is yours to make — the same rule the approval queue applies to everything else FounderReply writes.
- sitemap.xml
- A urlset of every published post, rooted at your blog base. No item cap.lastmod is the content-change time, falling back to first publish — never row noise. Reference it from your robots.txt or sitemap index, or mirror it at build time.
- rss.xml
- RSS 2.0 with the full body in content:encoded, newest 50 posts.The body ships as markdown: this API stores markdown, your site stays the canonical HTML render, and crawlers that fetch feeds parse it fine.
# the two XML surfaces — rooted at your blog base
curl "https://founderreply.com/api/public/blog/sitemap.xml?workspace_id=$WS&base_url=$BLOG_BASE"
curl "https://founderreply.com/api/public/blog/rss.xml?workspace_id=$WS&base_url=$BLOG_BASE"Leave base_url off and both fall back to the blog base configured on your primary SEO site. With neither, they return 400 — better than guessing at a domain and emitting a sitemap full of URLs that do not resolve.
Your domain, your blog
Two GET requests, and the articles live on your own site.
Generate the articles here, review them here, and serve them from your domain — where the SEO value belongs.
Free to start · Approval on by default · No card required