← Back to Homepage

Versaunt SEO API

Generate long-form SEO articles stored in your Versaunt account. Every request must send your SEO API key as Authorization: Bearer vsk_…. Create or rotate a key in Dashboard → Settings (signed-in users only). The full secret is shown once when generated; store it in a password manager or env var.

What the API writes about

Articles are written for the business you describe in the generate request: your siteUrl, businessSummary, and allowedPaths (the only on-site paths the draft may link to). The model is instructed to match that publisher, not to produce Versaunt product marketing. Versaunt hosts storage and generation; the output is meant to publish on your site.

Base URL

Use your deployment origin (production example):

https://www.versaunt.com

Authentication

  • Sign in to Versaunt, open Dashboard → Settings, and use Generate API key (or Rotate). Copy the value immediately; it is not shown again.
  • Keys look like vsk_<uuid>_<64 hex chars>. Send them on every SEO API request:Authorization: Bearer <full key>.
  • Use the key from server-side code or scripts whenever possible. Do not embed it in public frontends.

Dashboard vs SEO API

Signing in at /sign-in is only for the Versaunt web app (including creating an API key in Settings). The SEO HTTP routes do not accept email, password, or Supabase JWTs. They only validate the API key string.

Endpoints

MethodPathAuth
GET/api/seo/v1/sessionAPI key
GET/api/seo/v1/quotaAPI key
GET/api/seo/v1/postsAPI key
GET/api/seo/v1/posts?slug=…API key
GET/api/seo/v1/posts/[id]API key
POST/api/seo/v1/generateAPI key

Generate: request body

JSON body for POST /api/seo/v1/generate. Required fields:

  • topic — working title or subject (at least 10 characters).
  • siteUrl — canonical site URL for the publisher.
  • businessSummary — description of the business (at least 20 characters, max 4000; used to steer tone and examples).
  • allowedPaths — non-empty array of site-relative paths (each starts with /); internal links in the draft are limited to these.
  • author — object with all of name, title, and linkedin (use null if there is no LinkedIn URL; not optional).

Common validation mistakes (400 responses)

These return 400 with success: false and no article saved (generation never runs):

  • Missing JSON body, or omitting Content-Type: application/json.
  • Using keywords as an array. Use optional targetKeyword as a single string (or omit).
  • author with only name. You must also send title and linkedin (string or null).
  • businessSummary shorter than 20 characters, or allowedPaths empty or paths not starting with /.

Common optional fields:

  • targetKeyword, audience, tone, ctas
  • audienceTargetUrl — optional landing page URL for extra context
  • reviewedBy — reviewer name, title, LinkedIn, or null
  • evidencePack — optional structured facts (proof points, stats, source URLs, etc.) to ground the draft

Generate: successful response

On 200 OK, the JSON envelope includes success: true, data, and meta with remaining quota headroom. Shape (abbreviated):

{
  "success": true,
  "data": {
    "id": "<uuid>",
    "slug": "your-post-slug",
    "frontmatter": {
      "title": "...",
      "metaTitle": "...",
      "metaDescription": "...",
      "slug": "your-post-slug",
      "tldr": "...",
      "author": { "name": "...", "title": "...", "linkedin": null },
      "targetKeyword": "...",
      "tags": ["..."],
      "internalLinkSuggestions": [{ "anchorText": "...", "targetSlug": "/pricing" }]
    },
    "body": "<MDX string — headings, paragraphs, components as emitted by the model>",
    "provider": "vertex",
    "model": "...",
    "grounding": { "...": "search grounding metadata" },
    "usage": { "promptTokenCount": 0, "candidatesTokenCount": 0 }
  },
  "meta": {
    "remainingArticles": 99,
    "remainingFailures": 200,
    "windowDays": 30
  },
  "error": null
}

List posts: query parameters

GET /api/seo/v1/posts supports limit and offset for pagination (newest first). Defaults: limit=20, offset=0. Maximum limit is 100. If slug=... is present, the handler returns a single full post (or 404) and ignores pagination.

Article format (MDX)

The article body is a string of MDX (Markdown with optional JSX-style components, depending on what the model emits). Your site or CMS needs an MDX pipeline, a Markdown renderer, or a conversion step to HTML. Metadata for SEO and display (title, description, tags, etc.) is duplicated in data.frontmatter for convenience. When fetching a saved post via GET .../posts/..., the same body is stored in the content field on the row.

Examples by business type

One POST /generate call creates one article. Reuse the same siteUrl, businessSummary, and allowedPaths for your business; change topic (and optional fields) for each new angle. Below are illustrative JSON bodies only: send them with your API key and Content-Type: application/json.

Startups (product, pricing, hiring, content pillars)

Use separate generates for each “pillar”: one post for your core product, one for pricing transparency, one for security or integrations, one for a hiring story. Narrow topic per post; use targetKeyword and audience to match who should find it in search.

{
  "topic": "Why teams outgrow spreadsheets when syncing ads across Meta and Google",
  "targetKeyword": "cross-platform ad workflow",
  "audience": "Marketing leads at Series A–B SaaS companies",
  "tone": "Direct and practical, no hype",
  "siteUrl": "https://example.com",
  "businessSummary": "We sell workflow software that connects ad platforms and reporting. Teams use us to ship campaigns faster and see spend in one place.",
  "allowedPaths": ["/pricing", "/product", "/security", "/blog", "/contact"],
  "author": { "name": "Alex Kim", "title": "CEO", "linkedin": null }
}

Local and service businesses (areas, services, comparisons)

For zip codes, neighborhoods, or services, put the location or offer inside topic and targetKeyword. Use evidencePack for real hours, pricing notes, or links you want the draft to respect. For competitor-aware pieces, name alternatives in topic or evidencePack and keep claims factual.

{
  "topic": "Self-service laundry vs wash-and-fold near Inman Park: what locals choose and why",
  "targetKeyword": "laundromat Inman Park Atlanta",
  "audience": "Residents and students within 15 minutes of the store",
  "siteUrl": "https://example-laundry.com",
  "businessSummary": "Family-run laundromat with self-service washers, wash-and-fold, and pickup windows. We emphasize speed, fair pricing, and clean facilities.",
  "allowedPaths": ["/services", "/pricing", "/hours", "/contact", "/blog"],
  "author": { "name": "Jordan Lee", "title": "Owner", "linkedin": null },
  "evidencePack": {
    "proofPoints": ["Same-day wash-and-fold cut-off at 2pm", "Free dry on self-service Tuesdays"],
    "sourceUrls": ["https://example-laundry.com/hours", "https://example-laundry.com/pricing"]
  }
}

Another call could target a ZIP instead: set topic to something like “Coin laundry and dry cleaning options in 30309: what to expect” and targetKeyword to laundromat 30309.

E-commerce (categories, buying guides, trust)

Aim one article per intent: a category guide, a comparison, shipping and returns, or a how-to. Point audienceTargetUrl at a relevant collection or product page when it helps the model align CTAs. Use ctas to spell out the action you want (shop, bundle, subscribe).

{
  "topic": "How to choose the right size when buying running shoes online",
  "targetKeyword": "running shoe size guide",
  "audience": "First-time buyers comparing brands",
  "ctas": "Link to size chart and to best-selling trainers; encourage free returns",
  "siteUrl": "https://shop.example.com",
  "audienceTargetUrl": "https://shop.example.com/collections/running-shoes",
  "businessSummary": "Independent footwear retailer with free returns, expert fit notes, and curated brands for road and trail running.",
  "allowedPaths": ["/collections/running-shoes", "/shipping-returns", "/about", "/contact"],
  "author": { "name": "Sam Rivera", "title": "Merchandising Lead", "linkedin": null }
}

SEO habits that work well with this API

  • One main intent per article. Split “zip posts,” “neighborhood posts,” and “service explainer” into separate generates so each URL targets one search intent clearly.
  • Stable business context, specific topics. Keep businessSummary accurate and refresh it when your offers change; rotate topic for each new piece.
  • Keywords in the right fields. Put the phrase you care about in targetKeyword and reinforce it naturally in topic.
  • Internal links that match your site. List real paths in allowedPaths only; the draft will not invent URLs outside that list.
  • Facts in evidencePack. Hours, prices, service areas, or source URLs help grounded, trustworthy copy—especially for local and regulated topics.
  • Pace and quotas. Many short posts means many calls: watch GET /quota, respect per-minute limits, and space out batches if you hit 429.

Quotas

Rolling 30 days: up to 100 successful article generations and up to 200 recorded failures per account. Check GET /api/seo/v1/quota for remaining headroom.

Rate limits

Per account, rolling one minute: up to 90 requests to GET endpoints (session, quota, posts) and up to 8 POST /api/seo/v1/generate calls. These limits are separate from the 30-day article quotas above. If you exceed them, the API returns 429 with a Retry-After header (seconds).

HTTP errors

  • 400 — Invalid JSON or validation (e.g. missing topic, bad URL).
  • 401 — Missing or invalid API key in Authorization: Bearer.
  • 404 — Post not found (id or slug).
  • 409 — Slug already exists for your account.
  • 429 — Rolling article or failure quota exceeded, or per-minute request rate limit exceeded (see Retry-After when present).
  • 500 — Server or upstream error.

Examples

Verify session

curl -sS "https://www.versaunt.com/api/seo/v1/session" \
  -H "Authorization: Bearer YOUR_SEO_API_KEY"

Generate an article (replace body fields; businessSummary must be at least 20 characters)

curl -sS -X POST "https://www.versaunt.com/api/seo/v1/generate" \
  -H "Authorization: Bearer YOUR_SEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"topic":"Your headline or working title here (10+ chars)","targetKeyword":"optional single phrase","siteUrl":"https://example.com","businessSummary":"At least twenty characters describing the business for tone and context.","allowedPaths":["/pricing","/contact"],"author":{"name":"Jane Doe","title":"Founder","linkedin":null}}'

Same request using a JSON file (easier for long bodies): curl ... -d @body.json

# body.json
{
  "topic": "5 laundry hacks for busy professionals",
  "targetKeyword": "laundry pickup and delivery",
  "siteUrl": "https://your-site.com",
  "businessSummary": "Your business in at least twenty characters so the draft matches your offer and geography.",
  "allowedPaths": ["/services", "/contact", "/blog"],
  "author": { "name": "Your Name", "title": "Editorial", "linkedin": null }
}

curl -sS -X POST "https://www.versaunt.com/api/seo/v1/generate" \
  -H "Authorization: Bearer YOUR_SEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json

Get one saved article by id (from list or generate response)

curl -sS "https://www.versaunt.com/api/seo/v1/posts/POST_ID_UUID" \
  -H "Authorization: Bearer YOUR_SEO_API_KEY"

Same article by URL slug

curl -sS "https://www.versaunt.com/api/seo/v1/posts?slug=your-article-slug" \
  -H "Authorization: Bearer YOUR_SEO_API_KEY"

Fetch from JavaScript (server-side recommended)

const r = await fetch("https://www.versaunt.com/api/seo/v1/session", {
  headers: { Authorization: `Bearer ${process.env.VERSAUNT_SEO_API_KEY}` },
});
const json = await r.json();

Questions? hello@versaunt.com