API REFERENCE
The Mentionwell public API is small, RESTful, and read-only. Two endpoints cover almost every integration; three more give you syndication feeds.
Base URL
https://app.mentionwell.com
Self-hosted deployments use their own origin. Set this in MENTIONWELL_API_URL on your destination site.
Conventions
- Auth:
Authorization: Bearer <site-api-key>on the/api/public/*endpoints. Feeds and sitemaps are open. - Content type:
application/json(orapplication/rss+xml/application/feed+json/application/xmlfor feeds and sitemap). - Timestamps: ISO-8601, UTC (Z-suffixed).
- Caching:
- Post endpoints (
/api/public/...) returnCache-Control: private, max-age=60, stale-while-revalidate=300, stale-if-error=86400plus a weakETag. SendIf-None-Matchon subsequent requests to get a304 Not Modifiedand skip the body. - Feed and sitemap endpoints (
/api/sites/.../feed.xml,feed.json,sitemap.xml) returnCache-Control: public, max-age=300, s-maxage=600.
- Post endpoints (
- CORS:
Access-Control-Allow-Origin: *, withOPTIONSpreflight support. - Envelope: every JSON response is
{ "ok": boolean, ... }. On error,ok: falsewith amessagefield.
List posts
GET /api/public/{siteSlug}/posts
Returns up to limit published posts for one site, newest first. Use this for your /blog index page.
Path parameters
| Name | Type | Description |
|---|---|---|
siteSlug |
string (required) | The slug of your site, as shown in the dashboard URL. |
Query parameters
| Name | Type | Description |
|---|---|---|
limit |
integer (optional) | Maximum posts to return. Default 20, max 100. Clamped server-side. |
Example — cURL
curl https://app.mentionwell.com/api/public/your-site-slug/posts?limit=12 \
-H "Authorization: Bearer <MENTIONWELL_API_KEY>"
Example — JavaScript
const res = await fetch(
`${process.env.MENTIONWELL_API_URL}/api/public/${siteSlug}/posts?limit=12`,
{
headers: { Authorization: `Bearer ${process.env.MENTIONWELL_API_KEY}` },
next: { revalidate: 300, tags: ["mentionwell:posts"] }
}
);
const { posts } = await res.json();
Example — Python
import os, httpx
r = httpx.get(
f"{os.environ['MENTIONWELL_API_URL']}/api/public/{site_slug}/posts",
params={"limit": 12},
headers={"Authorization": f"Bearer {os.environ['MENTIONWELL_API_KEY']}"}
)
posts = r.json()["posts"]
Response
{
"ok": true,
"site": { "slug": "your-site-slug", "name": "Your Site", "domain": "yoursite.com" },
"posts": [
{
"slug": "five-questions-to-ask-before-you-buy",
"title": "Five Questions to Ask Before You Buy",
"excerpt": "A short, scannable summary that drops into your card UI.",
"metaDescription": "150-character SEO description.",
"featuredImage": "https://cdn.mentionwell.com/.../hero.jpg",
"readingTime": 7,
"tags": ["buying", "first-home"],
"category": { "title": "Buying", "slug": "buying" },
"publishedAt": "2026-04-21T15:00:00.000Z",
"updatedAt": "2026-04-21T15:00:00.000Z",
"author": { "name": "Editorial Team", "avatarUrl": null, "url": null },
"canonicalUrl": null
}
]
}
The list endpoint omits heavy fields (html, markdown, tldr, toc, faqs, jsonLd) to keep payloads small. Fetch the detail endpoint for those.
Get post
GET /api/public/{siteSlug}/posts/{slug}
Returns the full post: HTML body, table of contents, FAQ, JSON-LD, author block. Use for /blog/[slug] detail pages.
Path parameters
| Name | Type | Description |
|---|---|---|
siteSlug |
string (required) | The slug of your site. |
slug |
string (required) | The slug of a published post. |
Example
curl https://app.mentionwell.com/api/public/your-site-slug/posts/five-questions-to-ask-before-you-buy \
-H "Authorization: Bearer <MENTIONWELL_API_KEY>"
Response
{
"ok": true,
"post": {
"slug": "five-questions-to-ask-before-you-buy",
"title": "Five Questions to Ask Before You Buy",
"metaTitle": "Five Questions to Ask Before You Buy a Home | Your Site",
"metaDescription": "Short SEO description.",
"excerpt": "...",
"html": "<header class=\"wb-header\">...</header><section class=\"wb-section\">...</section>",
"markdown": "# Five Questions...\n\n...",
"featuredImage": "https://cdn.mentionwell.com/.../hero.jpg",
"readingTime": 7,
"tags": ["buying", "first-home"],
"category": { "title": "Buying", "slug": "buying" },
"publishedAt": "2026-04-21T15:00:00.000Z",
"updatedAt": "2026-04-21T15:00:00.000Z",
"author": { "name": "Editorial Team", "avatarUrl": null, "url": null },
"tldr": { "items": ["Takeaway 1", "Takeaway 2", "Takeaway 3"] },
"toc": [{ "id": "intro", "title": "Introduction", "level": 2 }],
"faqs": [{ "question": "Is now a good time to buy?", "answer": "..." }],
"canonicalUrl": null,
"jsonLd": "{\"@context\":\"https://schema.org\",\"@type\":\"Article\",...}"
}
}
RSS feed
GET /api/sites/{siteSlug}/feed.xml
Standard RSS 2.0. Public, no authentication. Add it to your <head>:
<link rel="alternate" type="application/rss+xml"
title="Your Site Blog"
href="https://app.mentionwell.com/api/sites/your-site-slug/feed.xml" />
JSON Feed
GET /api/sites/{siteSlug}/feed.json
JSON Feed 1.1 format. Useful for n8n / Zapier / Make automations and AI ingestion pipelines.
Sitemap
GET /api/sites/{siteSlug}/sitemap.xml
Standard sitemap.xml. Submit to Google Search Console and Bing Webmaster Tools.
Errors
All errors return a JSON envelope:
{ "ok": false, "message": "Human-readable description." }
| Status | Meaning | What to do |
|---|---|---|
400 |
Malformed request | Check the path and query parameters against this reference. |
401 |
Unauthorized | Missing or wrong Authorization header. |
404 |
Site or post not found | Verify the slug; treat as a normal no-result state in your UI. |
5xx |
Origin error | Retry with exponential backoff. Cached responses still serve via SWR. |
Dashboard / integration endpoints (authenticated)
These power the in-product Connect-destination wizard and the Custom Article tab. They require a logged-in dashboard session or a personal access token (mw_pat_...) with sites:write. Most customers never call these directly — the dashboard does — but they're documented here because they're the contract every architecture flows through.
Connect-destination wizard
GET /api/sites/{siteSlug}/delivery/options
POST /api/sites/{siteSlug}/delivery/configure
POST /api/sites/{siteSlug}/delivery/test
GET .../optionsreturns the architecture catalog rendered for this site, with the per-sitewebhookSecret,readApiKey,apiBaseUrl, and copy-paste receiver templates substituted in.POST .../configureaccepts{ architecture, publishEndpoint?, deployHookUrl?, githubRepo?, githubBranch?, githubContentPath?, githubToken?, cmsAdapter?, autoPushPublishedPosts? }. Cross-field validation enforces which inputs are required for each architecture (e.g.static_deploy_hookrequires bothpublishEndpointanddeployHookUrl). Writes tosites.delivery_config.POST .../testfires a signed test ping forstatic_deploy_hook,nextjs_isr, andcms_adapter; probes the GitHub API forgithub_mdx; echoes the read key fordynamic_reader. Returns{ ok, mode, message, detail }.
The architecture IDs are: static_deploy_hook, nextjs_isr, dynamic_reader, github_mdx, cms_adapter. See Connect wizard for what each picks.
Custom Article
POST /api/sites/{siteSlug}/headlines
Body for the custom-article flow:
{
"mode": "create_custom",
"title": "Breaking: feature ships",
"customBrief": "Optional. Pasted text the writer should treat as authoritative.",
"customSourceUrls": ["https://primary-source.example.com"],
"researchMode": "custom_plus_research",
"intent": "info",
"targetKeyword": "breaking feature",
"wordCountTarget": 1800,
"autoApprove": true
}
researchMode is one of auto, custom_only, or custom_plus_research. At least one of customBrief or customSourceUrls is required. See Custom articles for the full pipeline.
Trigger a draft
POST /api/sites/{siteSlug}/draft
Body: { "headlineId": "...", "includeGeoContext"?: boolean }. Counts against the monthly quota. Returns a jobId you can poll via /api/jobs/{id}.
Versioning
The public API is currently un-versioned because additive changes only. If a breaking change ever ships, it will land at a new path prefix and the existing one will continue to serve.