API reference

Manage your content plan, posts, hubs, product features, backlinks and SEO data programmatically. Base URL:

https://rankgoat.app/api/v1

Authentication

Create a key in Settings → API. A key acts as your account and reaches all your sites. Send it as a bearer token:

curl https://rankgoat.app/api/v1/me \
  -H "Authorization: Bearer rg_live_..."

Keys are shown once at creation - store them safely and revoke any that leak. Missing or invalid keys return 401.

Conventions

Account

GET /me

The authenticated member and a summary of their sites with credit balances.

// 200
{
  "member": { "id": 42, "email": "you@example.com" },
  "sites": [
    { "id": 123, "domain": "example.com", "status": "active",
      "credits": { "balance": 12, "subscription": 10, "purchased": 2, "allotment": 15 } }
  ]
}

Sites

GET /sites

Your sites.

// 200
{ "sites": [
  { "id": 123, "domain": "example.com", "niche": "Productivity software reviews",
    "keywords": ["notion alternatives", "best note apps"], "tone": "professional",
    "status": "active", "authorityScore": 34, "authorityStart": 28,
    "createdAt": "2026-01-04 10:00:00", "approvedAt": "2026-01-05 09:00:00", "activatedAt": "2026-01-05 09:10:00",
    "publishing": { "mode": "wordpress", "autoPublishEnabled": true, "autoApproveEnabled": false, "lastPublishAt": "2026-06-14 03:01:00" } }
] }

GET /sites/:id

A single site (same shape as one entry above).

// 200
{ "id": 123, "domain": "example.com", "status": "active", "authorityScore": 34, "...": "..." }

GET /sites/:id/credits

Credit balance, split by bucket, plus the monthly allotment.

// 200
{ "siteId": 123, "balance": 12, "subscription": 10, "purchased": 2, "allotment": 15 }

Content plan

GET /sites/:id/plan?month=YYYY-MM

The month's briefs (defaults to the current month). Status is planned, drafted, or skipped (archived).

// 200
{ "siteId": 123, "month": "2026-06", "items": [
  { "id": 789, "siteId": 123, "month": "2026-06", "position": 1, "status": "planned",
    "type": "comparison", "title": "Notion vs Obsidian", "targetKeyword": "notion alternatives",
    "metaDescription": "Which note app wins...", "intent": "commercial",
    "outline": ["Overview", "Pricing", "Verdict"], "productAngle": "...",
    "hubId": 5, "postId": null, "scheduledFor": "2026-06-03",
    "createdAt": "2026-06-01 12:00:00", "updatedAt": "2026-06-01 12:00:00" }
] }

POST /sites/:id/plan

Add a brief. Either let AI plan it (mode: "auto", async) or supply the fields yourself. Adding is free.

// request (AI) - returns a job to poll
{ "mode": "auto", "month": "2026-06" }

// 202
{ "job": { "kind": "plan-add", "target": "123" } }
// request (manual). Only "title" is required; a past "scheduledFor" is clamped to today.
{ "title": "Notion vs Obsidian", "targetKeyword": "notion alternatives",
  "metaDescription": "Which note app wins for developers?", "type": "comparison",
  "scheduledFor": "2026-06-20", "month": "2026-06" }

// 201
{ "id": 812, "siteId": 123, "month": "2026-06", "position": 16, "status": "planned",
  "type": "comparison", "title": "Notion vs Obsidian", "scheduledFor": "2026-06-20", "...": "..." }

PATCH /plan/:itemId

Edit a planned brief. Any omitted field is left unchanged; a blank title keeps the current one.

// request
{ "title": "Notion vs Obsidian (2026)", "type": "guide", "scheduledFor": "2026-06-22" }

// 200
{ "id": 789, "status": "planned", "title": "Notion vs Obsidian (2026)", "type": "guide", "scheduledFor": "2026-06-22", "...": "..." }

POST /plan/:itemId/schedule

Set a brief's date. Past dates return 400.

// request
{ "date": "2026-06-25" }

// 200
{ "id": 789, "status": "planned", "scheduledFor": "2026-06-25", "...": "..." }

POST /plan/:itemId/archive

Set a brief aside (no body).

// 200
{ "id": 789, "status": "skipped", "...": "..." }

POST /plan/:itemId/restore

Return an archived brief to the plan; its date is cleared and reassigned (no body).

// 200
{ "id": 789, "status": "planned", "scheduledFor": "2026-06-28", "...": "..." }

POST /plan/:itemId/generate

Write the post now. Spends 1 credit and runs in the background (a couple of minutes); returns a job to poll. Out of credits returns 402.

// 202 (no body)
{ "job": { "kind": "plan-generate", "target": "789" } }

GET /jobs/:kind/:target

Poll a background job (e.g. the one returned by generate). Status is running, done, or error.

// 200
{ "kind": "plan-generate", "target": "789", "status": "done", "error": null,
  "result": { "ok": true, "postId": 456 } }

Posts

GET /sites/:id/posts?status=

List posts, optionally filtered by status (draft, ready_to_publish, published, verified).

// 200
{ "siteId": 123, "posts": [
  { "id": 456, "siteId": 123, "title": "Notion vs Obsidian", "slug": "notion-vs-obsidian",
    "status": "published", "metaDescription": "...", "tags": ["note apps", "productivity"], "wordCount": 1180, "language": "en",
    "publishedUrl": "https://example.com/blog/notion-vs-obsidian",
    "generatedAt": "2026-06-03 03:01:00", "publishedAt": "2026-06-03 03:05:00",
    "memberApprovedAt": "2026-06-03 03:04:00" }
] }

GET /posts/:postId

A single post, including the rendered bodyHtml.

// 200
{ "id": 456, "siteId": 123, "title": "Notion vs Obsidian", "slug": "notion-vs-obsidian",
  "status": "published", "wordCount": 1180, "publishedUrl": "https://example.com/blog/notion-vs-obsidian",
  "bodyHtml": "<p>...</p>", "...": "..." }

POST /posts/:postId/approve

Approve a drafted post into the publish queue (no credit, no body). Requires publishing to be configured.

// 200
{ "id": 456, "status": "ready_to_publish", "memberApprovedAt": "2026-06-16 10:00:00", "...": "..." }

SEO & links

GET /sites/:id/backlinks

The site's inbound and outbound link graph.

// 200
{ "siteId": 123,
  "inbound": [
    { "id": 1, "sourceDomain": "peer.com", "sourceDr": 41, "anchor": "best note apps",
      "status": "live", "url": "https://peer.com/blog/x", "sourcePostTitle": "...", "verifiedAt": "2026-06-10 03:00:00" }
  ],
  "outbound": [
    { "id": 9, "targetDomain": "other.com", "anchor": "modern note-taking", "status": "live",
      "url": "https://example.com/blog/y", "sourcePostTitle": "...", "lastCheckedAt": "2026-06-15 03:00:00" }
  ] }

GET /sites/:id/on-page

Cached on-page audit rollup.

// 200
{ "siteId": 123, "summary": { "total": 40, "scanned": 38, "avgRatio": 0.82, "issues": 12, "excluded": 2, "lastScanned": "2026-06-15 04:00:00" } }

GET /sites/:id/sitemap

Cached sitemap audit (or null if not run yet).

// 200
{ "siteId": 123, "audit": { "report": { "found": true, "totalUrls": 120, "sampled": 25, "accessible": 25, "broken": [], "redirects": [] }, "error": null, "checkedAt": "2026-06-15 02:00:00" } }

GET /sites/:id/authority

Domain Rating now, the baseline at join, and the history.

// 200
{ "siteId": 123, "current": 34, "baseline": 28,
  "history": [ { "score": 28, "checked_at": "2026-01-05 09:00:00" }, { "score": 34, "checked_at": "2026-06-14 09:00:00" } ] }

GET /sites/:id/gsc

Cached Search Console performance + coverage (or null if not connected).

// 200
{ "siteId": 123, "gsc": { "property": "sc-domain:example.com",
  "performance": { "clicks": 320, "impressions": 9100, "topQueries": [ { "query": "notion alternatives", "position": 7.2, "impressions": 540 } ] },
  "coverage": { "indexedPages": 38 }, "error": null, "fetchedAt": "2026-06-15 06:00:00" } }

Content hubs

GET /sites/:id/hubs

Content hubs (topic clusters) for the site, active first. The monthly planner draws briefs from active hubs, weighted by priority.

// 200
{ "siteId": 123, "hubs": [
  { "id": 5, "name": "Productivity apps", "description": "...", "keywords": ["note apps", "task managers"], "priority": "high", "status": "active" }
] }

POST /sites/:id/hubs

Create a hub. Only name is required; keywords takes an array or a comma-separated string (max 8); priority is high, medium, or low (default medium).

// request
{ "name": "AI note-taking", "description": "How AI changes capture and recall",
  "keywords": ["ai notes", "ai note app"], "priority": "high" }

// 201
{ "id": 12, "name": "AI note-taking", "keywords": ["ai notes", "ai note app"], "priority": "high", "status": "active", "...": "..." }

PATCH /hubs/:hubId

Edit a hub. Any omitted field is left unchanged; a blank name keeps the current one.

// request
{ "priority": "low", "keywords": "ai notes, smart notes" }

// 200
{ "id": 12, "priority": "low", "keywords": ["ai notes", "smart notes"], "...": "..." }

POST /hubs/:hubId/archive

Retire a hub (no body). The planner stops drawing briefs from it; existing briefs and posts keep their hub.

// 200
{ "id": 12, "status": "archived", "...": "..." }

POST /hubs/:hubId/restore

Reactivate an archived hub (no body).

// 200
{ "id": 12, "status": "active", "...": "..." }

Product features

GET /sites/:id/features

The site's product features, active first. These ground content generation: posts can lean on real capabilities and never invent ones the product does not have.

// 200
{ "siteId": 123, "features": [
  { "id": 7, "name": "Offline sync", "description": "Notes sync across devices without a connection.", "status": "active", "source": "ai" }
] }

POST /sites/:id/features

Add a feature. Only name is required.

// request
{ "name": "End-to-end encryption", "description": "Notes are encrypted before they leave the device." }

// 201
{ "id": 15, "name": "End-to-end encryption", "status": "active", "source": "manual", "...": "..." }

PATCH /features/:featureId

Edit a feature. Any omitted field is left unchanged; a blank name keeps the current one.

// request
{ "description": "Zero-knowledge: only your devices hold the keys." }

// 200
{ "id": 15, "description": "Zero-knowledge: only your devices hold the keys.", "...": "..." }

POST /features/:featureId/archive

Retire a feature so new posts stop mentioning it (no body).

// 200
{ "id": 15, "status": "archived", "...": "..." }

POST /features/:featureId/restore

Reactivate an archived feature (no body).

// 200
{ "id": 15, "status": "active", "...": "..." }