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 (or application/rss+xml / application/feed+json / application/xml for feeds and sitemap).
  • Timestamps: ISO-8601, UTC (Z-suffixed).
  • Caching:
    • Post endpoints (/api/public/...) return Cache-Control: private, max-age=60, stale-while-revalidate=300, stale-if-error=86400 plus a weak ETag. Send If-None-Match on subsequent requests to get a 304 Not Modified and skip the body.
    • Feed and sitemap endpoints (/api/sites/.../feed.xml, feed.json, sitemap.xml) return Cache-Control: public, max-age=300, s-maxage=600.
  • CORS: Access-Control-Allow-Origin: *, with OPTIONS preflight support.
  • Envelope: every JSON response is { "ok": boolean, ... }. On error, ok: false with a message field.

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 .../options returns the architecture catalog rendered for this site, with the per-site webhookSecret, readApiKey, apiBaseUrl, and copy-paste receiver templates substituted in.
  • POST .../configure accepts { architecture, publishEndpoint?, deployHookUrl?, githubRepo?, githubBranch?, githubContentPath?, githubToken?, cmsAdapter?, autoPushPublishedPosts? }. Cross-field validation enforces which inputs are required for each architecture (e.g. static_deploy_hook requires both publishEndpoint and deployHookUrl). Writes to sites.delivery_config.
  • POST .../test fires a signed test ping for static_deploy_hook, nextjs_isr, and cms_adapter; probes the GitHub API for github_mdx; echoes the read key for dynamic_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.