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", "language": "en", "extraLanguages": ["de", "fr"], "excludeFromNetwork": false,
    "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). Archived posts are excluded unless you pass includeArchived=1; they carry archivedAt. Localized siblings share a translationGroupId. indexState is Google's index coverage from our daily sampler (indexed, discovered, not_submitted, or null if not checked yet).

// 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",
    "translationGroupId": null, "indexState": "indexed", "indexCheckedAt": "2026-06-14 10:00:00", "archivedAt": null,
    "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/mentions

Plain-text brand mentions, the other half of placements: the network alternates 1:1 between dofollow links and unlinked brand mentions (for AI/LLM visibility). received are mentions of your brand on member sites; given are mentions your posts carry.

// 200
{ "siteId": 123,
  "stats": { "total": 6, "live": 5, "domains": 4 },
  "received": [
    { "id": 3, "sourceDomain": "peer.com", "brandText": "ExampleApp", "status": "live",
      "url": "https://peer.com/blog/x", "sourcePostTitle": "...", "createdAt": "2026-06-08 03:00:00" }
  ],
  "given": [
    { "id": 11, "targetDomain": "other.com", "brandText": "OtherBrand", "status": "live",
      "url": "https://example.com/blog/y", "sourcePostTitle": "...", "createdAt": "2026-06-12 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/equity

The link-equity points economy: hosting placements for other members earns points, receiving them spends points. allowance is the monthly grant (resets each cycle), earned persists. enforced says whether equity currently gates new placements; balances accrue either way.

// 200
{ "siteId": 123, "enforced": false,
  "balance": 12.5, "allowance": 4, "earned": 8.5, "lifetimeEarned": 30.2,
  "ledger": [
    { "id": 88, "delta": 2.5, "bucket": "earned", "reason": "placement_earn", "refType": "outbound_link",
      "createdAt": "2026-07-10 05:00:00",
      "placement": { "postTitle": "...", "hostDomain": "example.com", "receiverDomain": "peer.com", "brandText": null } }
  ] }

GET /sites/:id/flywheel

The content flywheel: from 30 days after publish we evaluate each post's index coverage. Stuck posts get an internal-link boost, then a proposed content refresh. suggested are proposals awaiting your review (nothing is applied automatically); log is the recent action history.

// 200
{ "siteId": 123,
  "suggested": [
    { "id": 4, "postId": 456, "postTitle": "Notion vs Obsidian", "kind": "content_refresh", "round": 2,
      "status": "suggested", "rationale": "Still not indexed after the link boost.", "detail": { "...": "..." },
      "createdAt": "2026-07-12 11:00:00" }
  ],
  "log": [
    { "id": 31, "postId": 456, "postTitle": "Notion vs Obsidian", "event": "boost_internal",
      "detail": { "until": "2026-07-26" }, "createdAt": "2026-07-05 11: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", "...": "..." }