Agent API reference

Every endpoint of the Rankbox agent API: conventions, objects, parameters and example requests and responses for accounts, sites, articles, billing and more.

On this page
  1. Conventions
  2. Endpoint index
  3. Accounts
  4. Agent keys
  5. Activity
  6. Sites
  7. Research and keywords
  8. Articles
  9. Jobs
  10. Autopilot
  11. Integrations
  12. Site keys
  13. Credits
  14. Rank
  15. Backlinks
  16. Reddit
  17. Billing
  18. Events and webhooks
  19. Related

The agent API is the REST API behind agent access. It covers everything the dashboard does, for every site on the account. This page lists every endpoint with its parameters, an example request and an example response. Read Conventions first; the rest is reference.

Conventions

Base URL and versioning

All endpoints live under one base URL:

https://rankbox.xyz/api/agent/v1

The version is in the path. Within v1, Rankbox only adds things: new endpoints, new optional request fields, new response fields and new event types. Removing or renaming a field, or changing its meaning, would come as v2 at a new path. Write clients that ignore fields they don't know.

Authentication

Send an agent key or an OAuth access token in the Authorization header:

HTTP
Authorization: Bearer rv_agent_xxxxxxxxxxxx

Only POST /accounts works without it. Every credential has full access to the account; there are no scopes. See Agent authentication.

Requests

  • Bodies are JSON with Content-Type: application/json, at most 1 MB.
  • Field names are snake_case.
  • IDs are UUIDs, for example 6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64, except integration IDs, which are names such as webflow.
  • A site is addressed in the path: /sites/{site_id}/…. Get site IDs from GET /sites. The primary site's ID is also account.primary_site_id.
  • PATCH changes only the fields you send. Send null to clear a nullable field.
  • A resource on another account returns 404 not_found, never 403, so a probe learns nothing.

Responses

  • One object comes wrapped in its name: { "article": { … } }.
  • A list comes as a plural name plus a cursor: { "articles": [ … ], "next_cursor": "…" }.
  • Every response carries an X-Request-Id header. Quote it when you contact support.
  • Timestamps are ISO 8601 in UTC, for example 2026-10-02T14:03:11Z. Calendar days such as scheduled_date are YYYY-MM-DD.
  • Money is a number in US dollars, for example 49.5 for $49.50, with "currency": "usd".

Pagination

List endpoints take limit (1 to 100, default 50) and cursor. When next_cursor is null there is nothing more. Otherwise pass it back as ?cursor= to get the next page. Cursors are opaque strings; don't build or parse them.

cURL
curl "https://rankbox.xyz/api/agent/v1/sites/$SITE_ID/articles?limit=100&cursor=eyJ1IjoiMjAyNi0xMC0wMlQx…" \
  -H "Authorization: Bearer $RANKBOX_AGENT_KEY"

Idempotency

Every POST accepts an Idempotency-Key header: any string up to 255 characters, unique per operation. A UUID is a good choice.

RetryResult
Same key, same body, within 24 hoursThe original response, replayed with the header Idempotent-Replayed: true. Nothing runs twice and no credit is spent twice
Same key, different body409 conflict
Same key while the first request is still running409 conflict. Retry after a second

Send one on every POST that spends credits or money: generate, POST /sites, POST /sites/{site_id}/restore, POST /billing/activate, Reddit drafts. PATCH and DELETE are naturally idempotent.

Asynchronous jobs

Work that can take longer than 30 seconds runs as a job: the site scan, research, article generation, integration syncs and Reddit sweeps. These endpoints return 202 Accepted with { "job": { … } }. Poll Get a job with ?wait=30, or listen for the matching event. Everything else answers synchronously.

Errors

Errors use the same envelope as the public API: a sentence for people and a stable code for programs.

JSON
{
  "error": "This site has used all 7 article credits for this period. They reset on 9 October 2026.",
  "code": "insufficient_credits"
}

Some errors add fields: details (an array of { "field", "problem" } on validation_failed), action_url (a page for the owner on human_required and payment_failed) and docs_url.

StatuscodeMeaning
400bad_requestThe body isn't valid JSON, or a header is malformed
401unauthorizedMissing, unknown, revoked or expired credential
402subscription_requiredNeeds the trial or the plan; or the trial's card check failed; or the site isn't active on the plan
402paid_plan_requiredNeeds a paid invoice. The trial doesn't count. Applies to the backlink exchange, Reddit Presence and Studio
402insufficient_creditsThe site used this period's credits of the kind the call spends
402payment_failedA charge (a Studio site, ending the trial early) was declined. Nothing changed
403human_requiredOnly the signed-in owner can do this. action_url says where
404not_foundNo such resource on this account
409account_existsPOST /accounts with an email that already has an account
409conflictThe resource is in the wrong state for this call, or an idempotency key was reused
409integration_unavailableThe platform's Rankbox add-on isn't open to every account. Use the REST API path
422validation_failedA field is missing or invalid. See details
429rate_limitedToo many requests. Wait Retry-After seconds
500internal_errorRankbox failed. Retry with backoff; quote X-Request-Id if it persists
503unavailableTemporarily unavailable. Wait Retry-After seconds

Rate limit headers

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds) for the account's request budget. A 429 adds Retry-After in seconds. The numbers are in Billing, credits and limits.

Endpoint index

Paths are relative to https://rankbox.xyz/api/agent/v1. "Needs" is what the account must have; everything else works on an account with no plan.

MethodPathPurposeNeeds
POST/accountsCreate an account (no auth)
GET/accountGet the account
POST/account/resend-claimResend the claim email
DELETE/accountDelete the account (owner only)
GET/agent-keysList agent keys and OAuth connections
POST/agent-keysCreate an agent key
DELETE/agent-keys/{id}Revoke a key or connection
GET/activityRead the activity log
GET/sitesList sites
POST/sitesAdd a Studio site (charges the card)Paid plan
GET/sites/{site_id}Get a site
PATCH/sites/{site_id}Update brand and writing settings
DELETE/sites/{site_id}Schedule a Studio site's removal
POST/sites/{site_id}/restoreKeep or restore a Studio sitePaid plan
GET/sites/{site_id}/scanGet the site scan
POST/sites/{site_id}/scanRun the scan again
POST/sites/{site_id}/researchRun research
GET/sites/{site_id}/keywordsList keywords
POST/sites/{site_id}/keywordsAdd a keyword
DELETE/sites/{site_id}/keywords/{keyword_id}Delete a keyword
GET/sites/{site_id}/articlesList articles
POST/sites/{site_id}/articlesCreate an idea or scheduled article
GET/sites/{site_id}/articles/{article_id}Get an article
PATCH/sites/{site_id}/articles/{article_id}Update an article
DELETE/sites/{site_id}/articles/{article_id}Delete an article
POST/sites/{site_id}/articles/{article_id}/generateWrite the article (1 article credit)Trial or plan
POST/sites/{site_id}/articles/{article_id}/rewrite-sectionRewrite one passage
GET/sites/{site_id}/articles/{article_id}/scoreSEO and GEO score
POST/sites/{site_id}/articles/{article_id}/finishMark a hand-written article finished
POST/sites/{site_id}/articles/{article_id}/publishPush to destinations, record the live URL
GET/jobs/{job_id}Get a job
GET/jobsList jobs
GET/sites/{site_id}/autopilotGet autopilot
PATCH/sites/{site_id}/autopilotTurn autopilot on or off, set the pace
GET/sites/{site_id}/integrationsList integrations
GET/sites/{site_id}/integrations/{integration_id}Get one integration and its setup options
POST/sites/{site_id}/integrations/{integration_id}/connectStart a connectionTrial or plan
PATCH/sites/{site_id}/integrations/{integration_id}Configure a push integrationTrial or plan
POST/sites/{site_id}/integrations/{integration_id}/syncPush every missing or changed articleTrial or plan
DELETE/sites/{site_id}/integrations/{integration_id}Disconnect a push integration
GET/sites/{site_id}/keysList site keys
POST/sites/{site_id}/keysCreate a site keyTrial or plan
DELETE/sites/{site_id}/keys/{key_id}Revoke a site key
GET/sites/{site_id}/creditsCredit balances
GET/sites/{site_id}/rankRank: keyword coverage
GET/sites/{site_id}/backlinksBacklink exchange overview
PATCH/sites/{site_id}/backlinksExchange settingsPaid plan
POST/sites/{site_id}/backlinks/domainSet the domain to verifyPaid plan
POST/sites/{site_id}/backlinks/domain/verifyCheck domain verificationPaid plan
GET/sites/{site_id}/backlinks/targetsList targets
POST/sites/{site_id}/backlinks/targetsCreate a targetPaid plan
PATCH/sites/{site_id}/backlinks/targets/{target_id}Update a targetPaid plan
DELETE/sites/{site_id}/backlinks/targets/{target_id}Delete a targetPaid plan
GET/sites/{site_id}/backlinks/placementsList placements
DELETE/sites/{site_id}/backlinks/placements/{placement_id}Remove a hosted link
GET/sites/{site_id}/redditReddit Presence overview
PATCH/sites/{site_id}/redditTurn on and configure Reddit PresencePaid plan
POST/sites/{site_id}/reddit/sweepsStart a sweepPaid plan
GET/sites/{site_id}/reddit/threadsList threads
GET/sites/{site_id}/reddit/threads/{thread_id}Get a thread
PATCH/sites/{site_id}/reddit/threads/{thread_id}Save, dismiss or restore a threadPaid plan
POST/sites/{site_id}/reddit/threads/{thread_id}/draftsDraft or rewrite a reply (1 Reddit credit)Paid plan
PATCH/sites/{site_id}/reddit/drafts/{draft_id}Edit a draftPaid plan
POST/sites/{site_id}/reddit/repliesRecord a reply a person postedPaid plan
GET/sites/{site_id}/reddit/repliesList recorded replies
GET/billingBilling status
POST/billing/checkoutCheckout link for the owner
POST/billing/portalBilling portal link for the owner
POST/billing/activateEnd the trial now and payTrial
GET/billing/studio-quotePrice of one more Studio site
GET/eventsRead the event stream
GET/webhooksList webhook endpoints
POST/webhooksCreate a webhook endpoint
PATCH/webhooks/{webhook_id}Update a webhook endpoint
DELETE/webhooks/{webhook_id}Delete a webhook endpoint
POST/webhooks/{webhook_id}/testSend a test event

Accounts

Create an account

HTTP
POST /accounts

Creates an account, its primary site and an agent key, starts the site scan and emails the owner. No authentication. Full details, limits and the claim flow are in Create an account as an agent.

FieldTypeRequiredNotes
emailstringYesThe owner's email, at most 254 characters, not a disposable address
site_urlstringYesPublic http or https URL, at most 2,048 characters
site_namestringNo1 to 120 characters. Read from the site when left out
site_descriptionstringNoAt most 2,000 characters. Written from the site when left out
agent.namestringYes2 to 60 characters
agent.operatorstringNoAt most 100 characters
agent.contact_urlstringNoAn https URL

Returns 201 with account, site, scan_job_id, agent_key (with the secret key, shown once) and claim_url. The full example is in The response. Errors: 409 account_exists, 422 validation_failed, 429 rate_limited.

The account object

FieldTypeDescription
idstringAccount ID
emailstringThe owner's email
claimedbooleanWhether the owner has claimed the account and can sign in
claimed_atstring or nullWhen they claimed it
created_atstringWhen the account was created
created_by_agentobject or nullname, operator, contact_url of the agent that created it; null if a person signed up
primary_site_idstringThe site the plan covers
billing_statusstringnone, trialing, active, past_due or canceled. Details in GET /billing

Get the account

HTTP
GET /account
JSON
{
  "account": {
    "id": "8b0d3c4e-2f6a-4f1b-9d3e-5a7c1e2b9f40",
    "email": "maya@northwind.example",
    "claimed": true,
    "claimed_at": "2026-10-02T16:10:27Z",
    "created_at": "2026-10-02T14:03:11Z",
    "created_by_agent": {
      "name": "Atlas",
      "operator": "Northwind Analytics",
      "contact_url": "https://northwind.example/atlas"
    },
    "primary_site_id": "6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64",
    "billing_status": "trialing"
  }
}

A cheap call: use it to check a credential.

Resend the claim email

HTTP
POST /account/resend-claim

Sends the owner a new claim email and returns the new link. At most 3 a day per account.

JSON
{ "sent": true, "claim_url": "https://rankbox.xyz/claim/ct_xxxxxxxxxxxx" }

Errors: 409 conflict when the account is already claimed, 429 rate_limited after 3 in a day.

Delete the account

HTTP
DELETE /account

Deleting the account removes every site, article and setting and can't be undone, so only the signed-in owner can do it, from Dashboard → Settings → Account. The agent API always answers:

JSON
{
  "error": "Only the account owner can delete the account, from Dashboard → Settings → Account.",
  "code": "human_required",
  "action_url": "https://rankbox.xyz/dashboard/settings"
}

Agent keys

The agent key object

FieldTypeDescription
idstringID of the key or connection
typestringkey for an agent key, oauth for an app connected through OAuth
namestringThe agent's name, or the OAuth app's client_name
keystringThe secret. Only in the response that creates it
prefixstring or nullFirst 15 characters and an ellipsis, for keys; null for OAuth
client_idstring or nullThe OAuth client, for connections; null for keys
operatorstring or nullThe operator given when the key was created
contact_urlstring or nullThe contact URL given when the key was created
created_atstringWhen it was created or approved
last_used_atstring or nullThe last authenticated request

List agent keys and connections

HTTP
GET /agent-keys
JSON
{
  "agent_keys": [
    {
      "id": "2d4f6a8c-1e3b-4c5d-8f7a-9b0c1d2e3f45",
      "type": "key",
      "name": "Atlas",
      "prefix": "rv_agent_a1b2c3…",
      "client_id": null,
      "operator": "Northwind Analytics",
      "contact_url": "https://northwind.example/atlas",
      "created_at": "2026-10-02T14:03:11Z",
      "last_used_at": "2026-10-02T15:22:05Z"
    },
    {
      "id": "b8d0f2a4-6c8e-4a1b-9c3d-5e7f9a1b3c86",
      "type": "oauth",
      "name": "Claude",
      "prefix": null,
      "client_id": "c3f1e5d7-…",
      "operator": null,
      "contact_url": null,
      "created_at": "2026-10-04T08:31:50Z",
      "last_used_at": "2026-10-04T08:35:12Z"
    }
  ],
  "next_cursor": null
}

Revoked keys and disconnected apps aren't listed.

Create an agent key

HTTP
POST /agent-keys
FieldTypeRequiredNotes
namestringYes2 to 60 characters
operatorstringNoAt most 100 characters
contact_urlstringNoAn https URL

Returns 201 with { "agent_key": { … "key": "rv_agent_xxxxxxxxxxxx" … } }. The owner is emailed. At most 25 active keys and connections per account; beyond that, 409 conflict. Example in Create a key through the API.

Revoke a key or connection

HTTP
DELETE /agent-keys/{id}

Revokes an agent key or disconnects an OAuth app, immediately. Webhook endpoints it registered are disabled. Returns { "revoked": true, "id": "…" }. You can revoke the key you are calling with; the next request then returns 401.

Activity

List activity

HTTP
GET /activity

The activity log the owner sees at Dashboard → Settings → Agent access: every change made by an agent or by the owner, newest first, kept for 365 days.

Query parameterTypeNotes
actor_idstringOnly entries by this agent key or connection
site_idstringOnly entries about this site
sincestringOnly entries at or after this timestamp
limit, cursorPagination
JSON
{
  "activity": [
    {
      "id": "d1e3f5a7-9b2c-4d6e-8f0a-2b4c6d8e0f13",
      "created_at": "2026-10-02T15:22:05Z",
      "actor": { "type": "agent", "id": "2d4f6a8c-1e3b-4c5d-8f7a-9b0c1d2e3f45", "name": "Atlas" },
      "action": "article.generate",
      "site_id": "6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64",
      "resource": { "type": "article", "id": "f2b8c1d4-7e3a-4b9c-a6d5-3e8f1a2b4c76" },
      "summary": "Started writing “How to choose a product analytics tool for B2B SaaS” (1 article credit)",
      "ip": "203.0.113.7"
    }
  ],
  "next_cursor": null
}

actor.type is agent (a key), app (an OAuth connection), owner or rankbox (autopilot and other scheduled work).

Sites

The site object

FieldTypeDescription
idstringSite ID
kindstringprimary (the site the plan covers) or studio (an extra site billed at $49.50 a month)
statusstringpending (Studio payment in flight), active or archived
brand_namestring or nullBrand name
website_urlstring or nullThe website articles are published to
product_descriptionstring or nullWhat the business sells ("What you sell" in Settings)
avatar_urlstring or nullLogo URL
writingobjecttone, writing_style, audience, brand_voice (house rules, one per line)
scan_statusstringqueued, running, completed or failed
billed_fromstring or nullWhen a Studio site started billing
removes_atstring or nullA scheduled removal. The site runs until then
archived_atstring or nullWhen a Studio site stopped
dashboard_urlstringOpens this site in the dashboard
created_atstringWhen the site was added

A site is usable when status is active and removes_at is null or in the future.

List sites

HTTP
GET /sites

Returns every site on the account, primary first, then Studio sites in the order they were added, archived ones included.

JSON
{
  "sites": [
    {
      "id": "6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64",
      "kind": "primary",
      "status": "active",
      "brand_name": "Northwind Analytics",
      "website_url": "https://northwind.example",
      "product_description": "Product analytics for B2B SaaS teams: funnels, retention and feature adoption without SQL.",
      "avatar_url": "https://northwind.example/apple-touch-icon.png",
      "writing": {
        "tone": "Confident",
        "writing_style": "In-depth and data-driven",
        "audience": "Product managers at B2B SaaS companies",
        "brand_voice": "Say \"teams\", not \"users\".\nNever name competitors.\nUse US spelling."
      },
      "scan_status": "completed",
      "billed_from": null,
      "removes_at": null,
      "archived_at": null,
      "dashboard_url": "https://rankbox.xyz/dashboard?site=6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64",
      "created_at": "2026-10-02T14:03:11Z"
    }
  ],
  "next_cursor": null
}

Get a site

HTTP
GET /sites/{site_id}

Returns { "site": { … } } in the shape above.

Update a site

HTTP
PATCH /sites/{site_id}
FieldTypeNotes
brand_namestring1 to 120 characters
website_urlstringPublic http or https URL. Changing it doesn't re-run the scan
product_descriptionstringAt most 2,000 characters
avatar_urlstring or nullhttps URL of a logo
writing.tonestringAt most 80 characters. The dashboard offers Professional, Friendly, Confident, Conversational, Authoritative, Playful; any text works
writing.writing_stylestringAt most 80 characters. Offered: Balanced, Concise and actionable, In-depth and data-driven, Story-led, Step-by-step
writing.audiencestringAt most 160 characters. Offered: Founders / Entrepreneurs, Marketers, Small business owners, Developers, Agencies
writing.brand_voicestringHouse rules, one per line, at most 4,000 characters
HTTP
PATCH /sites/6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64
Content-Type: application/json

{ "writing": { "tone": "Confident", "audience": "Product managers at B2B SaaS companies" } }

Returns the updated { "site": { … } }. The writing settings feed every article written afterwards, by you or autopilot. See Brand voice and writing settings.

Add a Studio site

HTTP
POST /sites

Adds a Studio site: an extra site with its own plan allowance, billed at $49.50 a month on the same subscription. Rankbox charges the card on file at once for the rest of the current period (prorated), and only a successful charge creates the site. The scan then starts as it did for the first site. Needs a paid plan; check Get a Studio quote first. Send an Idempotency-Key.

FieldTypeRequiredNotes
site_urlstringYesPublic http or https URL
site_namestringYes1 to 120 characters
site_descriptionstringNoAt most 2,000 characters
logo_urlstringNohttps URL
proration_dateintegerNoFrom the quote. Locks the charge to the quoted amount for 30 minutes
JSON
{
  "site": {
    "id": "a3e9b1c7-5d2f-4c8a-9e6b-7f1a2d3c4b58",
    "kind": "studio",
    "status": "active",
    "brand_name": "Fernhill Coffee",
    "website_url": "https://fernhill.example",
    "product_description": "",
    "avatar_url": null,
    "writing": { "tone": "Professional", "writing_style": "Balanced", "audience": "Founders / Entrepreneurs", "brand_voice": "" },
    "scan_status": "running",
    "billed_from": "2026-10-20T10:00:00Z",
    "removes_at": null,
    "archived_at": null,
    "dashboard_url": "https://rankbox.xyz/dashboard?site=a3e9b1c7-5d2f-4c8a-9e6b-7f1a2d3c4b58",
    "created_at": "2026-10-20T10:00:00Z"
  },
  "scan_job_id": "4d6f8a0c-2e4b-4c6d-8e0f-1a3b5c7d9e21",
  "charged": { "amount": 24.75, "currency": "usd" }
}

Errors: 402 paid_plan_required during the trial or without a plan (see End the trial now), 402 payment_failed when the card is declined (no site is created), 403 human_required when the bank asks the cardholder to authenticate, 409 conflict while another site change on the account is in progress (retry after a few seconds).

Remove a Studio site

HTTP
DELETE /sites/{site_id}

Schedules a Studio site's removal at the end of the period already paid for. Nothing is refunded and nothing is deleted: the site keeps working until removes_at, then becomes archived, and its articles stay readable. The primary site can't be removed; the call returns 409 conflict (cancel the plan in the billing portal instead).

JSON
{ "site": { "id": "a3e9b1c7-5d2f-4c8a-9e6b-7f1a2d3c4b58", "kind": "studio", "status": "active", "removes_at": "2026-11-17T15:20:44Z" } }

The response holds the full site object; it is shortened here.

Restore a Studio site

HTTP
POST /sites/{site_id}/restore

Undoes a removal. If the site is still running with a removes_at in the future, this cancels the removal at no cost. If the site is archived, Rankbox charges the prorated price for the rest of the period, like adding a site, and brings it back with its articles. Body: optional proration_date from a quote. Returns { "site": { … }, "charged": null } or with charged set. Same errors as Add a Studio site.

Get the scan

HTTP
GET /sites/{site_id}/scan

The latest scan of the site's website: brand, analysis and the articles it added to the plan.

JSON
{
  "scan": {
    "site_id": "6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64",
    "job_id": "3c5e7a9b-1d3f-4b5c-8e7a-9c1b3d5f7a92",
    "status": "completed",
    "stage": "plan",
    "started_at": "2026-10-02T14:03:12Z",
    "finished_at": "2026-10-02T14:05:40Z",
    "brand": {
      "brand_name": "Northwind Analytics",
      "product_description": "Product analytics for B2B SaaS teams: funnels, retention and feature adoption without SQL.",
      "avatar_url": "https://northwind.example/apple-touch-icon.png"
    },
    "analysis": {
      "niche": "B2B product analytics",
      "audience": "Product managers and founders at B2B SaaS companies",
      "geo": "United States",
      "competitors": ["Lumen Metrics", "Quarry"]
    },
    "keywords_found": 20,
    "articles_planned": 20,
    "ideas": 4,
    "scheduled": 16,
    "error": null
  }
}

stage is brand, analysis or plan. Fields the scan couldn't ground in the website come back empty rather than guessed.

Run the scan again

HTTP
POST /sites/{site_id}/scan

Re-scans the website and rebuilds the plan from it. Returns 202 with a site.scan job. What it changes:

  • Brand fields that are empty are filled in; fields you set are kept.
  • Keywords it finds replace keywords with the same name; others are kept.
  • Unwritten articles (opportunity or scheduled, empty body) for the same keywords are replaced; written articles are never touched.

On an account with no trial or plan, at most 3 scans a day, the first one included.

Research and keywords

Run research

HTTP
POST /sites/{site_id}/research

Finds keywords buyers search for and article ideas for the site, using its brand and writing settings. Free; counts toward the AI rate limit. Returns 202 with a research job.

FieldTypeDefaultNotes
seedstringnoneA topic to research around, at most 200 characters. Without it, research works from the brand and product
ideasinteger10Article ideas to return, 0 to 30. Ideas never repeat titles already on the site
savebooleanfalseAdd the keywords to the site (source: discovered) and the ideas to the plan (status: opportunity)

The finished job's result:

JSON
{
  "keywords": [
    { "name": "feature adoption metrics", "tag": "High Intent", "search_volume": 1300, "traffic_estimate": 230, "intent": "Informational", "trend": "Rising" }
  ],
  "ideas": [
    { "title": "Feature adoption metrics: the 6 that predict expansion revenue", "description": "Define each metric, show how to compute it, and when it matters.", "keyword": "feature adoption metrics", "traffic_estimate": 900, "competition": "Low", "ai_signal": 84 }
  ],
  "saved": { "keywords": 18, "articles": 10 }
}

Up to 24 keywords per run. search_volume, traffic_estimate, competition and ai_signal are model estimates for planning, not measured data. saved is null when save is false. On an account with no trial or plan, at most 10 research runs a day. See Research.

The keyword object

FieldTypeDescription
idstringKeyword ID
site_idstringThe site
namestringThe search phrase, lowercase
sourcestringlibrary (tracked by you or the scan) or discovered (found by research)
tagstring or nullA short label such as High Intent
intentstring or nullCommercial, Informational, Transactional or Navigational
search_volumeintegerEstimated monthly searches
traffic_estimateintegerEstimated monthly visits once answered
trendstringRising, Steady or Declining
created_atstringWhen it was added

List keywords

HTTP
GET /sites/{site_id}/keywords?source=library

source is optional. Returns { "keywords": [ … ], "next_cursor": null }, highest search_volume first.

Add a keyword

HTTP
POST /sites/{site_id}/keywords
Content-Type: application/json

{ "name": "product analytics for startups", "intent": "Commercial", "search_volume": 880 }
FieldTypeRequiredNotes
namestringYes2 to 200 characters. Stored lowercase
intentstringNoOne of the four intents
search_volumeintegerNoYour own estimate or a figure from your keyword tool
tagstringNoAt most 40 characters
trendstringNoRising, Steady or Declining

Returns 201 with { "keyword": { … "source": "library" … } }. A keyword with the same name on the site returns 409 conflict. Tracked keywords drive Get Rank.

Delete a keyword

HTTP
DELETE /sites/{site_id}/keywords/{keyword_id}

Returns { "deleted": true, "id": "…" }. Articles that target the keyword are kept.

Articles

The article object

FieldTypeDescription
idstringArticle ID
site_idstringThe site
statusstringopportunity, scheduled, generating or finished. See Article statuses
titlestringTitle
slugstringThe title in URL form plus the first 8 characters of the ID, the same slug the public API returns
keywordstring or nullThe target keyword
descriptionstringThe brief before writing; the meta description after
bodystringThe stored Markdown, including the writer's notes. What PATCH writes
body_markdownstringPublish-ready Markdown: writer's notes removed
body_htmlstringPublish-ready HTML, rendered from body_markdown
tagsarray of stringsTags
seo_scoreinteger0 to 100, the score stored when the article was written or last scored
traffic_estimateintegerEstimated monthly visits
competitionstring or nullLow, Medium or High, estimated
ai_signalinteger0 to 100, how likely the topic is to come up in AI answers, estimated
scheduled_datestring or nullThe day autopilot writes it, for scheduled articles
queue_positioninteger or nullOrder in the autopilot queue; lowest goes first
notesstringFree-form notes, never published
published_urlstring or nullWhere the article is live, once known
published_atstring or nullWhen published_url was recorded
dashboard_urlstringOpens the article in the dashboard editor
created_at, updated_atstringTimestamps

The writer's notes in body are image ideas written as **[Image: …] (alt: "…")** and internal-link suggestions written as [anchor](#internal: target). Destinations never receive them: body_markdown and body_html drop the image ideas and keep only the anchor text of internal-link suggestions. An agent that builds the website can use them to add images and real internal links. The body usually starts with an H1 that repeats the title; drop it if your template prints the title itself.

JSON
{
  "article": {
    "id": "f2b8c1d4-7e3a-4b9c-a6d5-3e8f1a2b4c76",
    "site_id": "6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64",
    "status": "finished",
    "title": "How to choose a product analytics tool for B2B SaaS",
    "slug": "how-to-choose-a-product-analytics-tool-for-b2b-saas-f2b8c1d4",
    "keyword": "product analytics tool for b2b saas",
    "description": "A buyer's checklist for B2B SaaS teams: the events to track, the questions to ask vendors and the traps to avoid.",
    "body": "# How to choose a product analytics tool for B2B SaaS\n\nChoosing a product analytics tool comes down to three things…\n\n**[Image: A checklist on a desk] (alt: \"Product analytics buying checklist\")**\n\n## What a B2B SaaS team needs to measure\n…see [retention analysis](#internal: retention guide)…",
    "body_markdown": "# How to choose a product analytics tool for B2B SaaS\n\nChoosing a product analytics tool comes down to three things…\n\n## What a B2B SaaS team needs to measure\n…see retention analysis…",
    "body_html": "<h1>How to choose a product analytics tool for B2B SaaS</h1>\n<p>Choosing a product analytics tool comes down to three things…</p>\n<h2>What a B2B SaaS team needs to measure</h2>\n<p>…see retention analysis…</p>",
    "tags": ["product analytics", "b2b saas"],
    "seo_score": 100,
    "traffic_estimate": 1400,
    "competition": "Medium",
    "ai_signal": 86,
    "scheduled_date": null,
    "queue_position": null,
    "notes": "",
    "published_url": null,
    "published_at": null,
    "dashboard_url": "https://rankbox.xyz/dashboard/editor/f2b8c1d4-7e3a-4b9c-a6d5-3e8f1a2b4c76",
    "created_at": "2026-10-02T14:05:40Z",
    "updated_at": "2026-10-02T15:25:31Z"
  }
}

Article statuses

statusDashboard labelMeaningCan be set with
opportunityIdeaIn the plan, not on the schedulePOST, PATCH
scheduledScheduledIn the autopilot queue with a scheduled_datePOST, PATCH
generatingWritingBeing written by generate or autopilotOnly by Rankbox
finishedPublishedWritten and ready. The only status destinations and site keys ever seegenerate, finish, autopilot

finished means ready in Rankbox, not necessarily live on the website. published_url says where it is live. See How publishing works.

List articles

HTTP
GET /sites/{site_id}/articles
Query parameterTypeNotes
statusstringOne status or several, comma-separated, for example opportunity,scheduled
updated_sincestringOnly articles changed after this timestamp
includestringbody adds body, body_markdown and body_html. Left out by default
limit, cursorPagination

Sorted by updated_at, newest first. Returns { "articles": [ … ], "next_cursor": … }.

Create an article

HTTP
POST /sites/{site_id}/articles

Adds a topic to the plan, or an article you wrote yourself.

FieldTypeRequiredNotes
titlestringYes1 to 200 characters
keywordstringNoAt most 200 characters. Without it, the title is the keyword
descriptionstringNoThe brief, at most 2,000 characters. The writer follows it
statusstringNoopportunity (default) or scheduled
scheduled_datestringNoYYYY-MM-DD. For scheduled, defaults to the day after the last scheduled article
queue_positionintegerNoDefaults to the end of the queue
bodystringNoYour own Markdown. Then call Finish an article instead of generating
tagsarrayNoAt most 10 tags
notesstringNoNever published

Returns 201 with { "article": { … } } and fires article.created.

Get an article

HTTP
GET /sites/{site_id}/articles/{article_id}

Returns { "article": { … } } with all three bodies, as in The article object.

Update an article

HTTP
PATCH /sites/{site_id}/articles/{article_id}
FieldTypeNotes
title, keyword, description, tags, notesAs in Create
bodystringThe whole Markdown body
statusstringopportunity or scheduled only, and only for unwritten articles
scheduled_datestring or nullYYYY-MM-DD
queue_positioninteger or nullLower goes first. Use 0 or a negative number to move an article to the front

An article that is generating can't be changed (409 conflict). Changes to a finished article are saved and fire article.updated, but a connected Webflow or Shopify site keeps the previous version until you call Publish an article, like Publish changes in the editor. Site keys see the change at once.

Delete an article

HTTP
DELETE /sites/{site_id}/articles/{article_id}

Deletes the article permanently and returns { "deleted": true, "id": "…" }. A credit spent writing it isn't returned. Copies already pushed to Webflow or Shopify, or pulled by a website, aren't removed from those places. An article that is generating can't be deleted until the job ends.

Generate an article

HTTP
POST /sites/{site_id}/articles/{article_id}/generate

Writes the article, like Write now in the dashboard. It works on an opportunity or scheduled article and uses its title, keyword and description as the brief, plus the site's writing settings.

FieldTypeDefaultNotes
word_countinteger2750Target length, 800 to 6,000 words

What happens:

  1. Rankbox checks that the site may generate (an active trial with a card that passed the check, or the paid plan) and reserves 1 article credit.
  2. The article moves to generating and a job starts. The call returns 202 with { "job": { … "type": "article.generate" … } }.
  3. The writer researches the pages that rank for the keyword, drafts the article, scores it and rewrites what failed. If the site takes part in the backlink exchange, one link to another member's page may be placed in it.
  4. On success the article becomes finished, its seo_score is stored, it is pushed to every connected Webflow or Shopify destination, and article.generated and article.finished fire. The job's result has article_id, status, seo_score, credits_spent and deliveries.
  5. On failure the article returns to its previous status, the credit is refunded and article.generation_failed fires.

Errors: 402 subscription_required, 402 insufficient_credits, 409 conflict (already finished or generating), 429 rate_limited (the AI limit, or 3 generations already in progress on this site). To rewrite a finished article, edit it with PATCH and rewrite-section. See How articles are written.

Rewrite a section

HTTP
POST /sites/{site_id}/articles/{article_id}/rewrite-section

Rewrites one passage, like the editor's AI actions on selected text. Synchronous, usually 5 to 15 seconds. No credits; counts toward the AI rate limit.

FieldTypeRequiredNotes
selectionstringYesThe exact passage, 1 to 10,000 characters
actionstringYesrewrite, expand, shorten, improve_seo, change_tone (apply the site's tone and voice) or ai_suggest
applybooleanNotrue replaces the first exact match of selection in body and saves. Default false
JSON
{
  "rewrite": {
    "action": "shorten",
    "result": "Pick the tool that answers your three core questions without SQL.",
    "applied": true
  },
  "article": { "id": "f2b8c1d4-7e3a-4b9c-a6d5-3e8f1a2b4c76", "updated_at": "2026-10-02T15:40:10Z" }
}

article is the full article when applied is true, and null otherwise. With apply: true, a selection that doesn't appear in body returns 422 validation_failed.

Score an article

HTTP
GET /sites/{site_id}/articles/{article_id}/score

Scores the current body exactly the way the editor does. Deterministic and free.

JSON
{
  "analysis": {
    "score": 92,
    "checks": [
      { "id": "kw-title", "label": "Keyword in title", "status": "pass", "detail": "\"product analytics tool for b2b saas\" appears in the title." },
      { "id": "faq", "label": "FAQ for AI engines", "status": "pass", "detail": "FAQ section helps AI answer engines cite you." },
      { "id": "meta", "label": "Meta description", "status": "warn", "detail": "172 characters — aim for 120–160." }
    ],
    "metrics": {
      "words": 2140,
      "reading_time": 10,
      "keyword_density": 0.9,
      "keyword_count": 19,
      "h2": 7,
      "h3": 9,
      "links": 6,
      "readability": 52,
      "readability_grade": "Standard"
    }
  }
}

Check IDs: kw-title, kw-intro, kw-density, words, h2, h3, lists, faq, meta, links, readability. Each status is pass, warn or fail. See The SEO and GEO score.

Finish an article

HTTP
POST /sites/{site_id}/articles/{article_id}/finish

Marks an article you wrote yourself as finished, like Publish in the editor on a hand-written draft. The body must not be empty. No credit is spent. The article leaves the queue (scheduled_date and queue_position become null), is pushed to connected destinations, becomes visible to site keys and fires article.finished.

JSON
{
  "article": { "id": "0c2e4a6b-8d1f-4a3c-9e5b-7d9f1b3d5e82", "status": "finished" },
  "deliveries": [
    { "destination": "webflow", "result": "created", "reason": null, "url": "https://northwind.example/blog/onboarding-metrics", "draft": false, "error": null }
  ]
}

article holds the full article; it is shortened here. Errors: 409 conflict when already finished or generating, 422 validation_failed when body is empty. Generated articles are finished already and don't need this call.

Publish an article

HTTP
POST /sites/{site_id}/articles/{article_id}/publish

For a finished article: pushes the current version to every connected push destination (like Publish changes) and, if you send it, records where the article is live.

FieldTypeRequiredNotes
published_urlstringNoThe article's live URL. Must be on the site's own domain: website_url, or the domain verified in the backlink exchange
HTTP
POST /sites/6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64/articles/f2b8c1d4-7e3a-4b9c-a6d5-3e8f1a2b4c76/publish
Content-Type: application/json

{ "published_url": "https://northwind.example/blog/how-to-choose-a-product-analytics-tool-for-b2b-saas" }
JSON
{
  "article": {
    "id": "f2b8c1d4-7e3a-4b9c-a6d5-3e8f1a2b4c76",
    "status": "finished",
    "published_url": "https://northwind.example/blog/how-to-choose-a-product-analytics-tool-for-b2b-saas",
    "published_at": "2026-10-02T16:02:44Z"
  },
  "deliveries": []
}

A delivery has destination (webflow or shopify), result (created, updated, unchanged, skipped or failed), reason for skipped deliveries (in_progress, edited_in_destination, deleted_in_destination, no_plan), url, draft and error. An article edited or deleted on the platform itself is never overwritten or re-created. Recording published_url fires article.published and is what the backlink exchange uses to verify links hosted in the article. Pushing needs a trial or plan; recording a URL doesn't. Errors: 409 conflict when the article isn't finished, 422 validation_failed when the URL isn't on the site's domain. See Live URLs and verification.

Jobs

The job object

FieldTypeDescription
idstringJob ID
typestringsite.scan, research, article.generate, integration.sync or reddit.sweep
statusstringqueued, running, completed or failed
site_idstringThe site it runs for
resourceobject or nullWhat it works on, for example { "type": "article", "id": "…" }
resultobject or nullThe outcome, once completed. Shape depends on type
errorobject or null{ "code", "message" }, once failed
created_at, started_at, finished_atstring or nullTimestamps

Jobs are kept for 7 days. A failed article.generate job's error.code is generation_failed or timeout; its credit is always refunded.

Get a job

HTTP
GET /jobs/{job_id}?wait=30

wait (0 to 30 seconds, default 0) holds the request open until the job finishes or the time runs out, whichever comes first. A loop of wait=30 calls is the cheapest way to wait for a job; each call counts as one request.

JSON
{
  "job": {
    "id": "9a7e5c3b-1d2f-4e6a-8b9c-0d1e2f3a4b5c",
    "type": "article.generate",
    "status": "completed",
    "site_id": "6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64",
    "resource": { "type": "article", "id": "f2b8c1d4-7e3a-4b9c-a6d5-3e8f1a2b4c76" },
    "result": {
      "article_id": "f2b8c1d4-7e3a-4b9c-a6d5-3e8f1a2b4c76",
      "status": "finished",
      "seo_score": 100,
      "credits_spent": 1,
      "deliveries": []
    },
    "error": null,
    "created_at": "2026-10-02T15:22:05Z",
    "started_at": "2026-10-02T15:22:06Z",
    "finished_at": "2026-10-02T15:25:31Z"
  }
}

List jobs

HTTP
GET /jobs?status=queued,running&site_id=6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64

Filters: status, type, site_id, plus limit and cursor. Newest first. Use it after a restart to pick up jobs you lost track of.

Autopilot

The autopilot object

FieldTypeDescription
site_idstringThe site
enabledbooleanWrite automatically in Settings. On by default for new sites
weekly_cadenceintegerArticles a week, 1 to 7. Default 7
last_run_atstring or nullWhen autopilot last wrote an article for this site
next_due_atstring or nullThe earliest time the next article is due. Autopilot runs once a day and writes at the first run after this
queue_lengthintegerScheduled articles waiting
blocked_reasonstring or nullpaused, no_plan, site_inactive, no_credits, queue_empty, or null when nothing blocks it

Autopilot writes the scheduled article with the lowest queue_position, then the earliest scheduled_date, spends 1 article credit, finishes it and pushes it to connected destinations. Articles it writes fire the same events as generate, with trigger: "autopilot". See Autopilot and the publishing schedule.

Get autopilot

HTTP
GET /sites/{site_id}/autopilot

Returns { "autopilot": { … } }.

Update autopilot

HTTP
PATCH /sites/{site_id}/autopilot
Content-Type: application/json

{ "enabled": true, "weekly_cadence": 3 }

Both fields are optional. Returns the updated object, as in step 12 of the quickstart.

Integrations

The integration object

FieldTypeDescription
idstringwebflow, shopify, framer, wordpress or rest_api
namestringDisplay name
deliverystringpush (Rankbox writes into the platform) or pull (the website or plugin reads with a site key)
availablebooleanWhether this integration can be connected on this account. rest_api and wordpress always can
statusstringnot_connected, pending, setup, active, idle, error or disconnected
connected_atstring or nullWhen it was connected
last_published_atstring or nullLast successful push, or last use of a site key for pull integrations
last_errorstring or nullThe last push error
settingsobject or nullPlatform settings, below
countsobject or nullPush only: in_destination, not_yet, edited_in_destination

pending means a connect link is waiting for the owner. setup means connected but not configured. idle is a pull integration whose keys haven't been used for 48 hours.

Settings by platform:

idsettings fields
webflowwebflow_site_id, webflow_site_name, collection_id, collection_name, field_map (body, summary, tags, published_at), publish_mode (live or draft)
shopifyshop, shop_name, shopify_blog_id, blog_title, publish_mode (visible or hidden), admin_url
framer, wordpress, rest_apikeys (active site keys), last_key_used_at

List integrations

HTTP
GET /sites/{site_id}/integrations
JSON
{
  "integrations": [
    { "id": "webflow", "name": "Webflow", "delivery": "push", "available": true, "status": "not_connected", "connected_at": null, "last_published_at": null, "last_error": null, "settings": null, "counts": null },
    { "id": "rest_api", "name": "REST API", "delivery": "pull", "available": true, "status": "active", "connected_at": "2026-10-02T15:30:12Z", "last_published_at": "2026-10-02T15:58:03Z", "last_error": null, "settings": { "keys": 1, "last_key_used_at": "2026-10-02T15:58:03Z" }, "counts": null }
  ],
  "next_cursor": null
}

All five integrations are always listed; this example is shortened. The publishing pages say which platform add-ons are open to every account.

Get an integration

HTTP
GET /sites/{site_id}/integrations/{integration_id}

Returns { "integration": { … }, "options": { … } }. options lists the choices for configuring a connected push integration:

  • webflow: webflow_sites (id, display_name, domain, published). Add ?webflow_site_id= to also get collections (id, display_name, slug), and add &collection_id= to get fields (slug, display_name, type, is_required) and suggested_field_map.
  • shopify: blogs (id, title).
  • Pull integrations: options is null.

Connect an integration

HTTP
POST /sites/{site_id}/integrations/{integration_id}/connect

Starts a connection. Needs a trial or plan. What comes back depends on the platform:

idResponseThe owner's part
webflowauthorize_url, valid 7 daysOpens it, then approves Rankbox on Webflow's own screen
shopifyinstall_url, valid 7 daysOpens it in the store's admin and approves the Rankbox app; the store then links to this site automatically
framersite_key with its secretOpens the Rankbox plugin in the Framer project and pastes the key
wordpress, rest_apisite_key with its secretNothing: deploy the key to the website's server
JSON
{
  "connection": {
    "integration_id": "webflow",
    "status": "pending",
    "authorize_url": "https://rankbox.xyz/connect/webflow/cr_xxxxxxxxxxxx",
    "install_url": null,
    "site_key": null,
    "expires_at": "2026-10-09T15:31:00Z"
  }
}

The connect page shows the owner which site and which agent asked. When they finish, integration.connected fires and status becomes setup (Webflow, Shopify) or active. Errors: 402 subscription_required, 409 integration_unavailable when available is false, 409 conflict when already connected.

Configure an integration

HTTP
PATCH /sites/{site_id}/integrations/{integration_id}

Finishes setup of a push integration. Pull integrations have nothing to configure (409 conflict).

idFields
webflowwebflow_site_id, collection_id, field_map with body (required, a rich-text field), summary, tags, published_at, and publish_mode. live needs a Webflow site that has been published at least once
shopifyshopify_blog_id, publish_mode (visible or hidden)
HTTP
PATCH /sites/6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64/integrations/webflow
Content-Type: application/json

{
  "webflow_site_id": "64f1a2b3c4d5e6f708192a3b",
  "collection_id": "64f1a2b3c4d5e6f708192c4d",
  "field_map": { "body": "post-body", "summary": "post-summary", "tags": null, "published_at": "published-date" },
  "publish_mode": "draft"
}

Returns { "integration": { … "status": "active" … } }. A field map the collection won't accept returns 422 validation_failed with one details entry per problem. See Webflow and Shopify.

Sync an integration

HTTP
POST /sites/{site_id}/integrations/{integration_id}/sync

Pushes every finished article that isn't on the platform yet and every article changed since its last push. Returns 202 with an integration.sync job. The result:

JSON
{ "created": 12, "updated": 1, "unchanged": 3, "skipped": 1, "failed": 0, "remaining": 0, "errors": [] }

When remaining is above 0, the run stopped at its time limit: call sync again. Pull integrations return 409 conflict; they sync from their own side.

Disconnect an integration

HTTP
DELETE /sites/{site_id}/integrations/{integration_id}

Stops pushing to the platform. For Webflow, Rankbox deletes its stored token. For Shopify, the site is unlinked; the app stays installed until the merchant removes it. Items already on the platform stay there. Returns { "integration": { … "status": "disconnected" … } }. To stop a pull integration, revoke its site keys.

Site keys

Site keys (rv_live_…) let a website or plugin read one site's finished articles through the public API. Create them for the website; never deploy the agent key.

List site keys

HTTP
GET /sites/{site_id}/keys
JSON
{
  "site_keys": [
    {
      "id": "7b9d1f3a-5c7e-4a2b-9d4f-6e8a0c2b4d61",
      "name": "northwind.example website",
      "prefix": "rv_live_d4e5f6…",
      "last_used_at": "2026-10-02T15:58:03Z",
      "revoked_at": null,
      "created_at": "2026-10-02T15:30:12Z"
    }
  ],
  "next_cursor": null
}

Revoked keys are included, with revoked_at set.

Create a site key

HTTP
POST /sites/{site_id}/keys
Content-Type: application/json

{ "name": "northwind.example website" }

name is optional, at most 60 characters, default API key. Returns 201 with { "site_key": { … "key": "rv_live_xxxxxxxxxxxx" … } }; the secret is shown only here. Needs a trial or plan (402 subscription_required), and a site key stops working while the account has neither.

Revoke a site key

HTTP
DELETE /sites/{site_id}/keys/{key_id}

Returns { "site_key": { … "revoked_at": "2026-10-03T09:00:00Z" } }. Revocation is immediate and permanent.

Credits

Get credits

HTTP
GET /sites/{site_id}/credits

Each site has its own balances.

JSON
{
  "credits": {
    "site_id": "6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64",
    "articles": { "total": 7, "used": 1, "remaining": 6, "period_end": "2026-10-09T15:20:44Z" },
    "backlinks": { "balance": 0, "escrowed": 0, "period_end": null },
    "reddit_replies": { "balance": 0, "period_end": null }
  }
}
BalanceGrantedResets
articles7 in the trial, 30 per paid periodEach period, no rollover
backlinks30 per paid period, as a top-up that never lifts the balance past 90; plus credits earned by hosting linksDoesn't reset
reddit_replies30 per paid periodEach period, no rollover

See Plans and credits.

Rank

Get Rank

HTTP
GET /sites/{site_id}/rank

The Rank page's data: how much of the site's tracked keyword market has a finished article. It doesn't measure rankings or AI citations; searches and traffic are estimates.

JSON
{
  "rank": {
    "site_id": "6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64",
    "coverage": { "basis": "searches", "total": 48200, "published": 6100, "planned": 21900, "open": 20200 },
    "market": [
      {
        "keyword": "product analytics tool for b2b saas",
        "searches": 1900,
        "intent": "Commercial",
        "trend": "Rising",
        "traffic_estimate": 1400,
        "status": "published",
        "article_id": "f2b8c1d4-7e3a-4b9c-a6d5-3e8f1a2b4c76",
        "score": 100
      }
    ]
  }
}

coverage.basis is searches when keywords have search volumes (the totals are monthly searches) and topics otherwise (the totals are keyword counts). A market row's status is published, writing, scheduled, idea or gap. See Rank: AI search visibility.

The backlink exchange is paid-only: reads work on any account, changes need a paid plan (402 paid_plan_required). Credits settle only when a link is verified live. See Backlink exchange.

HTTP
GET /sites/{site_id}/backlinks
JSON
{
  "backlinks": {
    "site_id": "6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64",
    "paid": true,
    "domain": "northwind.example",
    "status": "verified",
    "verified_at": "2026-10-17T11:02:00Z",
    "opted_in": true,
    "authority_score": 34,
    "tier": 2,
    "reputation": 100,
    "niche": "B2B product analytics",
    "topic_tags": ["product analytics", "saas metrics"],
    "blocked_categories": ["gambling", "crypto"],
    "max_links_per_article": 1,
    "credits": { "balance": 30, "escrowed": 1, "lifetime_earned": 0, "lifetime_spent": 0 },
    "counts": { "live_inbound": 0, "live_hosted": 0, "in_progress": 1 }
  }
}

status is unverified, verifying, verified or suspended, or null before a domain is set.

HTTP
PATCH /sites/{site_id}/backlinks
FieldTypeNotes
opted_inbooleanHost other members' links in this site's articles. Needs a verified domain
max_links_per_articleinteger0 to 2 member links per article
nichestringAt most 120 characters
topic_tagsarrayAt most 15 tags, 2 to 40 characters each
blocked_categoriesarrayAny of gambling, adult, crypto, cannabis, loans, pharma, weapons, tobacco, politics, dating

Returns the updated { "backlinks": { … } }.

Set the exchange domain

HTTP
POST /sites/{site_id}/backlinks/domain
Content-Type: application/json

{ "domain": "northwind.example" }

Returns the ways to prove ownership. Any one of them works:

JSON
{
  "verification": {
    "domain": "northwind.example",
    "status": "unverified",
    "token": "c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6",
    "methods": {
      "dns_txt": { "host": "_rankbox.northwind.example", "value": "rankbox-site-verification=c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6" },
      "meta_tag": { "tag": "<meta name=\"rankbox-site-verification\" content=\"c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6\">" },
      "well_known": { "url": "https://northwind.example/.well-known/rankbox-verification", "content": "c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6" }
    }
  }
}

Verify the exchange domain

HTTP
POST /sites/{site_id}/backlinks/domain/verify

Checks all three methods now. Counts toward the AI rate limit.

JSON
{
  "verification": {
    "domain": "northwind.example",
    "status": "verified",
    "checks": [
      { "method": "dns_txt", "ok": false, "seen": "no TXT record at _rankbox.northwind.example" },
      { "method": "well_known", "ok": false, "seen": "404" },
      { "method": "meta_tag", "ok": true, "seen": "meta tag matched" }
    ]
  }
}
HTTP
GET /sites/{site_id}/backlinks/targets

A target is a page on your site you want links to. Returns { "targets": [ … ], "next_cursor": null }. Each target has id, url, anchors, topic_tags, priority, max_new_links_per_month, active, live_count, last_placed_at and created_at.

HTTP
POST /sites/{site_id}/backlinks/targets
Content-Type: application/json

{
  "url": "https://northwind.example/features/retention",
  "anchors": ["retention analysis", "cohort retention tool", "Northwind retention reports"],
  "topic_tags": ["retention", "saas metrics"],
  "priority": 7,
  "max_new_links_per_month": 4
}
FieldTypeRequiredNotes
urlstringYesA page on the verified domain
anchorsarrayYes3 to 5 different anchor texts, 2 to 80 characters each
topic_tagsarrayNoAt most 10
priorityintegerNo1 to 10, default 5
max_new_links_per_monthintegerNo1 to 10, default 4
activebooleanNoDefault true

Returns 201 with { "target": { … } }. At most 25 targets per site.

HTTP
PATCH /sites/{site_id}/backlinks/targets/{target_id}

Any field of Create. Returns { "target": { … } }.

HTTP
DELETE /sites/{site_id}/backlinks/targets/{target_id}

Returns { "deleted": true, "id": "…" }. Links already live stay live.

List placements

HTTP
GET /sites/{site_id}/backlinks/placements?direction=inbound&status=live

direction is inbound (links to your targets) or hosted (links your articles carry for others). status is any of reserved, placed, live, lost, expired, cancelled.

JSON
{
  "placements": [
    {
      "id": "6a8c0e2f-4b6d-4e8a-9c1e-3f5b7d9a1c34",
      "direction": "inbound",
      "status": "live",
      "target_url": "https://northwind.example/features/retention",
      "anchor_used": "cohort retention tool",
      "host_url": "https://partner.example/blog/saas-churn-playbook",
      "host_article_id": null,
      "credits": 2,
      "reserved_at": "2026-10-18T09:00:00Z",
      "placed_at": "2026-10-18T09:04:00Z",
      "live_at": "2026-10-19T06:00:00Z",
      "ended_at": null,
      "expires_at": "2026-11-17T09:00:00Z",
      "last_checked_at": "2026-10-25T06:00:00Z",
      "end_reason": null
    }
  ],
  "next_cursor": null
}

host_article_id is set on hosted placements: the article that carries the link.

HTTP
DELETE /sites/{site_id}/backlinks/placements/{placement_id}

For a hosted placement only. Rankbox unlinks the anchor in your article (the words stay). A live link's earned credits are clawed back; a link not yet live is cancelled and its credits return to the member who requested it. Returns { "placement": { … "status": "cancelled" … } }.

Reddit

Reddit Presence finds Reddit threads where the site's buyers ask questions and drafts replies. Rankbox never posts to Reddit and never holds a Reddit login: a person posts the reply from their own account and the agent records the permalink. Paid-only: reads work on any account, actions need a paid plan. See Reddit Presence.

Get Reddit Presence

HTTP
GET /sites/{site_id}/reddit
JSON
{
  "reddit": {
    "site_id": "6f1c2a9e-4b7d-4e2a-8c3f-1d9e5b7a2c64",
    "access": "paid",
    "enabled": true,
    "sweep_enabled": true,
    "niche": "B2B product analytics",
    "topic_tags": ["product analytics", "saas metrics"],
    "disclosure_line": "Full disclosure: I work on {brand}.",
    "tone": "plain",
    "max_links_per_reply": 1,
    "allow_subreddits": [],
    "deny_subreddits": ["startups"],
    "keywords_per_sweep": 12,
    "last_sweep_at": "2026-10-18T06:00:00Z",
    "credits": { "balance": 28, "period_end": "2026-11-17T15:20:44Z" }
  }
}

access is paid, trial (shown the feature, can't use it yet), lapsed (history read-only) or none.

Update Reddit settings

HTTP
PATCH /sites/{site_id}/reddit
FieldTypeNotes
enabledbooleantrue turns Reddit Presence on for the site. The site needs a brand name
sweep_enabledbooleanAutomatic sweeps
disclosure_linestring10 to 200 characters. Must name the brand (or use {brand}) and say the writer works on it
nichestringAt most 160 characters
topic_tagsarrayAt most 12
tonestringAt most 80 characters, default plain
max_links_per_replyinteger0 or 1
allow_subreddits, deny_subredditsarrayAt most 50 subreddit names each, without r/
keywords_per_sweepinteger1 to 30, default 12

Returns { "reddit": { … } }.

Start a Reddit sweep

HTTP
POST /sites/{site_id}/reddit/sweeps

Searches Reddit now for threads matching the site's keywords. Free; counts toward the AI rate limit. Returns 202 with a reddit.sweep job whose result has threads_seen and opportunities_created. One sweep at a time per site (409 conflict).

List Reddit threads

HTTP
GET /sites/{site_id}/reddit/threads?status=new,saved

status is any of new, saved, drafted, posted, dismissed, dead, stale. Best fit first.

JSON
{
  "threads": [
    {
      "id": "0e2c4a6b-8d1f-4c3e-9a5b-7d9f1b3d5e70",
      "status": "new",
      "fit": 82,
      "matched_keyword": "product analytics tool",
      "channel": "serp",
      "blocked_reason": null,
      "thread": {
        "reddit_id": "1f3k9qz",
        "subreddit": "saas",
        "permalink": "https://www.reddit.com/r/saas/comments/1f3k9qz/which_product_analytics_tool_for_a_10person_b2b/",
        "title": "Which product analytics tool for a 10-person B2B SaaS?",
        "up_votes": 48,
        "num_comments": 31,
        "posted_at": "2026-10-16T19:12:00Z",
        "is_locked": false
      },
      "draft": null,
      "reply": null,
      "first_seen_at": "2026-10-18T06:00:00Z"
    }
  ],
  "next_cursor": null
}

fit is 0 to 100, null for blocked threads; blocked_reason is archived, likely_archived, locked, removed, subreddit_denied, promo_banned or off_topic.

Get a Reddit thread

HTTP
GET /sites/{site_id}/reddit/threads/{thread_id}

Returns { "thread": { … } } with the thread's body and top comments, the latest draft and the recorded reply.

Update a Reddit thread

HTTP
PATCH /sites/{site_id}/reddit/threads/{thread_id}
Content-Type: application/json

{ "status": "dismissed", "dismiss_reason": "Asking for free tools only" }

status can be set to saved, dismissed or new (restore). A posted thread can't be dismissed.

Draft a Reddit reply

HTTP
POST /sites/{site_id}/reddit/threads/{thread_id}/drafts
Content-Type: application/json

{ "instructions": "Mention the free plan only if asked." }

The first call drafts a reply and spends 1 Reddit reply credit. Later calls on the same thread rewrite the latest draft: 1 credit each, at most 3 rewrites, and the first rewrite is free when the draft failed a compliance check. instructions is optional, at most 500 characters. Synchronous.

JSON
{
  "draft": {
    "id": "8c0e2a4b-6d8f-4b1c-9e3a-5c7e9b1d3f46",
    "thread_id": "0e2c4a6b-8d1f-4c3e-9a5b-7d9f1b3d5e70",
    "body": "For a 10-person team, start from the three questions you need answered weekly…\n\nFull disclosure: I work on Northwind Analytics.",
    "edited_body": null,
    "compliance": {
      "pass": true,
      "failures": 0,
      "unknowns": 1,
      "checks": [
        { "id": "disclosure", "label": "Discloses that you work there", "state": "pass", "detail": "Says who you are up front." },
        { "id": "answers_first", "label": "Answers the question first", "state": "pass", "detail": "Helps before it mentions you." },
        { "id": "subreddit_rules", "label": "Subreddit rules", "state": "unknown", "detail": "Couldn't read this subreddit's rules. Check them before posting." }
      ]
    },
    "credits_spent": 1,
    "regen_count": 0,
    "created_at": "2026-10-18T09:20:00Z",
    "updated_at": "2026-10-18T09:20:00Z"
  }
}

Errors: 402 paid_plan_required, 402 insufficient_credits, 409 conflict after 3 rewrites or for a blocked thread. A refused or failed draft refunds its credit.

Edit a Reddit draft

HTTP
PATCH /sites/{site_id}/reddit/drafts/{draft_id}
Content-Type: application/json

{ "edited_body": "For a 10-person team, start from the three questions…" }

Free. Returns { "draft": { … } }. The text to hand to a person is edited_body when set, otherwise body.

Record a posted reply

HTTP
POST /sites/{site_id}/reddit/replies
Content-Type: application/json

{
  "thread_id": "0e2c4a6b-8d1f-4c3e-9a5b-7d9f1b3d5e70",
  "draft_id": "8c0e2a4b-6d8f-4b1c-9e3a-5c7e9b1d3f46",
  "permalink": "https://www.reddit.com/r/saas/comments/1f3k9qz/comment/lq2x8ab/"
}

Call this after a person has posted the reply from their own Reddit account. permalink must be a https://www.reddit.com/ link to a comment in that thread. Leave it out to record that a reply was posted before the link is known (status: "claimed"), then call again with it. One reply per thread per account.

JSON
{
  "reply": {
    "id": "2a4c6e8b-0d2f-4a4c-8e6b-9d1f3a5c7e58",
    "thread_id": "0e2c4a6b-8d1f-4c3e-9a5b-7d9f1b3d5e70",
    "draft_id": "8c0e2a4b-6d8f-4b1c-9e3a-5c7e9b1d3f46",
    "permalink": "https://www.reddit.com/r/saas/comments/1f3k9qz/comment/lq2x8ab/",
    "status": "posted",
    "score": null,
    "posted_at": "2026-10-18T10:05:00Z",
    "confirmed_at": null,
    "removed_at": null,
    "last_checked_at": null
  }
}

Rankbox then checks the comment on its own: status moves to confirmed (reddit.reply_verified), or removed or not_found (reddit.reply_removed). Recording and verifying never spend credits.

List Reddit replies

HTTP
GET /sites/{site_id}/reddit/replies?status=posted,confirmed

Returns { "replies": [ … ], "next_cursor": … }.

Billing

The billing object

FieldTypeDescription
statusstringnone, trialing, active, past_due or canceled
paidbooleanWhether an invoice has been paid and the plan is current. Opens backlinks, Reddit and Studio
card_verifiedboolean or nullThe trial's card check. false blocks generation until the card is updated
trial_ends_atstring or nullEnd of the trial
current_period_endstring or nullWhen the current period renews or ends
cancel_at_period_endbooleanCancellation is scheduled
past_due_sincestring or nullWhen a payment first failed. Generation keeps working for 48 hours after it
planobjectname (Business), amount (49.5), currency, interval
studio_sitesintegerStudio sites billed on top of the plan
monthly_totalnumberThe monthly bill, plan plus Studio sites

Get billing

HTTP
GET /billing

Returns { "billing": { … } }, as in step 7 of the quickstart.

HTTP
POST /billing/checkout

Creates a link for the owner to start the plan. The owner enters their card on Stripe's checkout page; Rankbox never sees the card number.

JSON
{
  "checkout": {
    "mode": "trial",
    "checkout_url": "https://rankbox.xyz/checkout/co_xxxxxxxxxxxx",
    "expires_at": "2026-10-09T14:07:02Z",
    "trial_days": 7,
    "price": { "amount": 49.5, "currency": "usd", "interval": "month" }
  }
}

mode is trial (7 days free, then $49.50 a month) for an account that has never had a subscription, and plan (charged today) for one whose earlier subscription ended. The link is valid for 7 days and can be opened more than once until checkout completes. When it does, billing.trial_started (or billing.activated) fires. An account with a current subscription gets 409 conflict; use the portal link instead.

HTTP
POST /billing/portal

Returns { "portal": { "portal_url": "https://rankbox.xyz/billing/portal/po_xxxxxxxxxxxx", "expires_at": "…" } }, valid for 24 hours. Stripe's billing portal is where the owner updates the card, downloads invoices and cancels. Hand it to the owner. Needs an existing subscription (409 conflict otherwise).

End the trial now

HTTP
POST /billing/activate

Ends the trial today and charges the first $49.50 to the card on file, like Unlock everything now in Plan & Billing. The site's article balance resets to 30, and the backlink exchange, Reddit Presence and Studio open. Send an Idempotency-Key.

JSON
{
  "billing": { "status": "active", "paid": true, "current_period_end": "2026-11-04T10:00:00Z" },
  "charged": { "amount": 49.5, "currency": "usd" }
}

billing holds the full object; it is shortened here. Errors: 409 conflict when not trialing, 402 payment_failed with action_url when the card is declined (the trial continues), 403 human_required when the bank asks the cardholder to authenticate.

Get a Studio quote

HTTP
GET /billing/studio-quote

What one more Studio site costs today, previewed by Stripe.

JSON
{
  "studio_quote": {
    "block": null,
    "message": null,
    "price_per_site": 49.5,
    "due_today": 24.75,
    "proration_date": 1760954400,
    "renews_at": "2026-11-04T10:00:00Z",
    "allowance": { "articles": 15, "backlink_credits": 15, "reddit_replies": 15 },
    "studio_sites": 0,
    "monthly_after": 99,
    "card": { "brand": "visa", "last4": "4242" }
  }
}

block says why a site can't be added now: no_plan, trialing, past_due, canceling or lapsed, with message in words. Pass proration_date to Add a Studio site within 30 minutes to be charged exactly due_today.

Events and webhooks

The payloads, signatures and retry rules are on Events and webhooks. These are the endpoints.

List events

HTTP
GET /events?since=2026-10-02T14:00:00Z
Query parameterTypeNotes
sincestringStart after this timestamp. Use on the first call
cursorstringContinue from a previous next_cursor. Use on every later call
typesstringComma-separated event types, for example article.finished,credits.low
site_idstringOnly events about this site
limitinteger1 to 100, default 50

Oldest first. Returns { "events": [ … ], "next_cursor": "…", "has_more": false }. next_cursor is always set, even when no events came back, so the next poll starts where this one ended. Events are kept for 30 days.

List webhooks

HTTP
GET /webhooks

Returns { "webhooks": [ … ], "next_cursor": null }, without secrets.

Create a webhook

HTTP
POST /webhooks
FieldTypeRequiredNotes
urlstringYesPublic https URL
eventsarrayYesEvent types, or ["*"] for all
descriptionstringNoAt most 200 characters
JSON
{
  "webhook": {
    "id": "5e3c1a9f-7b2d-4f8e-a1c3-6d4b2e9f8a17",
    "url": "https://agent.northwind.example/hooks/rankbox",
    "events": ["article.finished", "article.published", "credits.low"],
    "description": "Atlas",
    "enabled": true,
    "disabled_reason": null,
    "secret": "whsec_xxxxxxxxxxxx",
    "created_by": { "type": "agent", "id": "2d4f6a8c-1e3b-4c5d-8f7a-9b0c1d2e3f45", "name": "Atlas" },
    "created_at": "2026-10-02T16:10:00Z",
    "last_delivery_at": null,
    "last_delivery_status": null
  }
}

secret is shown only here. At most 10 endpoints per account.

Update a webhook

HTTP
PATCH /webhooks/{webhook_id}

Change url, events, description or enabled. Setting enabled: true re-enables an endpoint Rankbox disabled after repeated failures. Returns { "webhook": { … } } without the secret.

Delete a webhook

HTTP
DELETE /webhooks/{webhook_id}

Returns { "deleted": true, "id": "…" }. Deliveries in flight are dropped.

Send a test event

HTTP
POST /webhooks/{webhook_id}/test

Sends a signed webhook.test event to the endpoint now and returns { "delivery": { "status": 200, "duration_ms": 184, "error": null } }.