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
- JSON in, JSON out. Errors look like
{ "error": { "code": "...", "message": "..." } }. - Status codes:
400bad input,401no/invalid key,402out of credits,404not found or not yours,409wrong state,429rate limited. - Rate limits: ~120 requests/min per key; post generation is capped tighter at ~10/min.
- Any
:siteIdyou pass must belong to you, or you get404.
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", "...": "..." }