CitationBenchTalk to Sales
API referenceResearch

Research · Keyword API — Discover, Label & Manage SEO Keywords with 2D Intent × Relevance Labels

Run agentic keyword discovery jobs, then label every keyword on the 2D intent × relevance axes and manage your full persistent keyword library with CRUD, rank tracking, and bulk operations.

Discover keywords, label them on the 2D intent × relevance axes, and manage your full keyword library. This page covers the agentic research endpoint plus complete CRUD over the persistent Keyword resource.

Conceptual overview

Keywords in CitationBench are first-class persistent objects. Every one carries:

  • The keyword string (e.g., "project management software for engineering teams")
  • A 2D label: intent (URGENCY, PURCHASE, LOCATION, SPECIFICATION, PRODUCT, PROBLEM_SOLUTION, ALTERNATIVE) and relevance (OFFERING, ALTERNATIVE, COMPLEMENTARY, INCUMBENT) — both with confidence scores
  • A source (DATAFORSEO, GOOGLE_SEARCH_CONSOLE, AHREFS, AI_SUGGESTION, USER_INPUT)
  • A lifecycle status (RAW → LABELLING → LABELED → FOCUSED → ARCHIVED)
  • Optional pillar, tags, priority
  • A rank history (KeywordRank records over time)

The headline endpoint, POST /v1/research/keyword, runs a discovery + labeling job. Everything else is normal CRUD over the resulting Keyword records.

→ Concept: 2D keyword labelling (read first if you haven't)

Endpoints

MethodPathPurpose
POST/v1/research/keywordRun a research job (agentic, async)
GET/v1/keywordsList keywords with filters
POST/v1/keywords/searchRicher label/filter search
GET/v1/keywords/{id}Get one keyword + rank history
POST/v1/keywordsCreate one or many
POST/v1/keywords/bulkBulk create (up to 5,000)
PATCH/v1/keywords/{id}Update labels, status, pillar, tags
DELETE/v1/keywords/{id}Archive (soft delete)
POST/v1/keywords/relabelRe-run the labeling pass on a scope
POST/v1/keywords/{id}/check-rankTrigger a fresh rank check
GET/v1/keywords/{id}/rank-historyHistorical rank data

POST /v1/research/keyword

The agentic endpoint. Pass a seed (keyword or domain) and CitationBench runs a configurable set of discovery methods, merges and deduplicates results, then labels every keyword on the 2D axes.

Request

POST /v1/research/keyword HTTP/1.1
Host: api.citationbench.com
Authorization: Bearer sk_live_***
X-Workspace-Id: ws_acme
Content-Type: application/json
Idempotency-Key: 7f3a1b18-7d8b-4f3e-9c4b-2c1a3e0a9b8f

{
  "seed": "project management software",
  "depth": "thorough",
  "methods": ["related_keywords", "keyword_ideas", "ahrefs_matching", "llm_mentions"],
  "limit": 500,
  "country": "us",
  "language": "en",
  "label": true,
  "saveTo": "pil_pricing",
  "tags": ["q2-2026", "engineering-icp"]
}

Parameters

FieldTypeRequiredDefaultNotes
seedstringyesA keyword or a domain (e.g., acme.com)
depth"fast" | "thorough"no"thorough"fast runs 2 methods; thorough runs all available
methodsstring[]nodepth defaultsOverride depth. Available: related_keywords, keyword_ideas, ahrefs_matching, ahrefs_related, ahrefs_suggestions, llm_mentions, serp_neighbors
limitnumberno500Max keywords kept after dedup
countryISO-3166-2noworkspace default
languageISO-639-1noworkspace default
labelbooleannotrueRun 2D labeling pass
saveTo"workspace" | pillar_id | nullno"workspace"Where to persist results
tagsstring[]no[]Tags applied to every created keyword
dryRunbooleannofalseEstimate cost only

Response

HTTP/1.1 202 Accepted
Content-Type: application/json
X-Request-Id: req_01HVZ...

{
  "invocationId": "inv_01HVZRJZ3T8M7E0B0V0Z6KQ3A4",
  "agentId":      "agt_01HVZRJZ3T8M7E0B0V0Z6KQ3A5",
  "skill":        "research.keyword",
  "status":       "RUNNING",
  "estimatedCost": { "credits": 27, "durationSeconds": 90 },
  "links": {
    "self":   "https://api.citationbench.com/v1/agent/invocations/inv_01HVZ...",
    "events": "https://api.citationbench.com/v1/agent/invocations/inv_01HVZ.../events"
  }
}

Final result (when complete)

GET /v1/agent/invocations/{invocationId}:

{
  "invocationId": "inv_01HVZ...",
  "agentId": "agt_01HVZ...",
  "skill": "research.keyword",
  "skillsUsed": ["research.keyword"],
  "status": "SUCCEEDED",
  "creditsUsed": 24,
  "durationMs": 87420,
  "result": {
    "totalDiscovered": 1842,
    "afterDedup": 612,
    "afterLimit": 500,
    "perMethodBreakdown": {
      "related_keywords": 423,
      "keyword_ideas": 687,
      "ahrefs_matching": 512,
      "llm_mentions": 94
    },
    "keywords": [
      {
        "id": "kw_01HVZ...",
        "keyword": "project management software for engineering teams",
        "intentLabels": ["SPECIFICATION"],
        "intentConfidence": 0.91,
        "relevanceLabel": "OFFERING",
        "relevanceConfidence": 0.87,
        "isHighIntent": true,
        "isHighRelevance": true,
        "volume": 1300,
        "kd": 38,
        "sources": ["keyword_ideas", "ahrefs_matching"],
        "pillarId": "pil_pricing",
        "tags": ["q2-2026", "engineering-icp"],
        "createdAt": "2026-05-24T08:01:42Z"
      }
      // ... 499 more
    ]
  },
  "raw": "I started with the seed \"project management software\" and ran four discovery methods in parallel. DataForSEO's related_keywords surfaced 423 matches; keyword_ideas added 687 with broader semantic coverage. Ahrefs matching contributed 512, mostly long-tail. The LLM-mentions pass added 94 high-intent terms that ranking tools miss ...",
  "files": [
    "agent-workspace/keywords-full.csv",
    "agent-workspace/labeling-notes.md",
    "agent-workspace/dedup-decisions.csv"
  ]
}

The result is the typed structured output; raw is the agent's narration of its own reasoning; files are the working artifacts (the agent often writes a CSV of every keyword it considered with reject reasons, plus its labeling notes). See Agent · invoke § Universal response envelope.


GET /v1/keywords

List keywords with simple filters.

curl -G "https://api.citationbench.com/v1/keywords" \
  -H "Authorization: Bearer sk_live_***" \
  -H "X-Workspace-Id: ws_acme" \
  --data-urlencode "intent=PURCHASE,ALTERNATIVE" \
  --data-urlencode "relevance=OFFERING" \
  --data-urlencode "pillar=pil_pricing" \
  --data-urlencode "status=LABELED,FOCUSED" \
  --data-urlencode "minVolume=100" \
  --data-urlencode "limit=50"

Query parameters

ParamTypeNotes
intentcsvURGENCY, PURCHASE, LOCATION, SPECIFICATION, PRODUCT, PROBLEM_SOLUTION, ALTERNATIVE
relevancecsvOFFERING, ALTERNATIVE, COMPLEMENTARY, INCUMBENT
pillarstringPillar ID
tagstringTag slug
statuscsvRAW, LABELLING, LABELED, FOCUSED, ARCHIVED
prioritycsvCRITICAL, HIGH, MEDIUM, LOW, BACKLOG
minVolumenumber
maxKdnumber
qstringSubstring match on the keyword
hasContentbooleanFilter to keywords with / without a linked blog post or landing page
limitnumber (max 500)Default 100
cursorstringPagination

Response

{
  "data": [
    {
      "id": "kw_01HVZ...",
      "keyword": "project management software for engineering teams",
      "intentLabels": ["SPECIFICATION"],
      "intentConfidence": 0.91,
      "relevanceLabel": "OFFERING",
      "relevanceConfidence": 0.87,
      "status": "LABELED",
      "priority": "HIGH",
      "pillarId": "pil_pricing",
      "tags": ["q2-2026", "engineering-icp"],
      "volume": 1300,
      "kd": 38,
      "source": "DATAFORSEO",
      "currentRank": {
        "position": 14,
        "url": "https://acme.com/engineering",
        "checkedAt": "2026-05-23T08:00:00Z"
      },
      "createdAt": "2026-05-24T08:01:42Z",
      "updatedAt": "2026-05-24T08:01:42Z"
    }
  ],
  "nextCursor": "kw_01HVZS...",
  "total": 612
}

POST /v1/keywords/search

For richer queries (multiple label combinations, content gaps).

curl -X POST "https://api.citationbench.com/v1/keywords/search" \
  -H "Authorization: Bearer sk_live_***" \
  -d '{
    "intent": ["ALTERNATIVE"],
    "relevance": ["INCUMBENT", "ALTERNATIVE"],
    "minConfidence": 0.85,
    "pillarSlugs": ["pricing"],
    "missingFromContent": true
  }'

Returns the same shape as GET /v1/keywords.


GET /v1/keywords/{id}

curl -H "Authorization: Bearer sk_live_***" \
  "https://api.citationbench.com/v1/keywords/kw_01HVZ..."

Response includes the keyword + the most recent 30 rank checks + any linked content.

{
  "id": "kw_01HVZ...",
  "keyword": "project management software for engineering teams",
  "intentLabels": ["SPECIFICATION"],
  "...": "...",
  "rankHistory": [
    {
      "checkDate": "2026-05-23T08:00:00Z",
      "position": 14,
      "url": "https://acme.com/engineering",
      "isOwnedDomain": true
    },
    {
      "checkDate": "2026-05-16T08:00:00Z",
      "position": 11,
      "url": "https://acme.com/engineering",
      "isOwnedDomain": true
    }
  ],
  "links": {
    "blogPosts": ["bp_***"],
    "landingPages": ["lp_***"]
  }
}

POST /v1/keywords

Create one or many synchronously (no LLM labeling unless label: true).

curl -X POST "https://api.citationbench.com/v1/keywords" \
  -H "Authorization: Bearer sk_live_***" \
  -d '{
    "keywords": [
      { "keyword": "open source project management" },
      { "keyword": "monday.com vs asana", "intentLabels": ["ALTERNATIVE"] }
    ],
    "label": true,
    "pillarId": "pil_pricing"
  }'

Response

{
  "created": [
    {
      "id": "kw_***",
      "keyword": "open source project management",
      "status": "LABELLING"
    },
    { "id": "kw_***", "keyword": "monday.com vs asana", "status": "LABELLING" }
  ],
  "duplicatesSkipped": [],
  "labelingInvocationId": "inv_01HVZ..."
}

If label: true, a background job runs the 2D labeling pass and the keywords transition to LABELED when done.


POST /v1/keywords/bulk

Up to 5,000 keywords in one request. Same parameters as POST /v1/keywords, batched.


PATCH /v1/keywords/{id}

curl -X PATCH "https://api.citationbench.com/v1/keywords/kw_***" \
  -H "Authorization: Bearer sk_live_***" \
  -d '{
    "intentLabels": ["PURCHASE"],
    "relevanceLabel": "OFFERING",
    "priority": "HIGH",
    "pillarId": "pil_pricing",
    "tags": ["enterprise", "q2-2026"],
    "status": "FOCUSED"
  }'

Any combination of fields. Returns the updated keyword.


DELETE /v1/keywords/{id}

Soft delete — sets status: "ARCHIVED". Hard delete requires admin scope.

curl -X DELETE -H "Authorization: Bearer sk_live_***" \
  "https://api.citationbench.com/v1/keywords/kw_***"

Response: 204 No Content.


POST /v1/keywords/relabel

Re-run the 2D labeling pass across a scope. Useful after editing your product description.

curl -X POST "https://api.citationbench.com/v1/keywords/relabel" \
  -H "Authorization: Bearer sk_live_***" \
  -d '{
    "scope": { "status": "RAW" },
    "model": "claude-sonnet-4-6"
  }'

Returns an invocationId (relabeling is async; same response envelope as POST /v1/research/keyword).


POST /v1/keywords/{id}/check-rank

Trigger a fresh rank check for one keyword. For bulk rank checks, use the dedicated rank-tracking endpoint (covered in Indexing · gsc index — rank tracking is its own page).

curl -X POST -H "Authorization: Bearer sk_live_***" \
  "https://api.citationbench.com/v1/keywords/kw_***/check-rank" \
  -d '{ "device": "desktop", "location": "us" }'

Returns an invocationId. On completion, the keyword's currentRank is updated and a KeywordRank record is added to its history.


MCP

Each endpoint has a corresponding MCP tool.

> Research keywords for project management software, focused on PROBLEM_SOLUTION intent.

Claude calls research.keyword.research with intentFilter: ["PROBLEM_SOLUTION"].

> Show me every PURCHASE keyword in the pricing pillar with KD under 30.

Claude calls research.keyword.list.

> Tag every keyword in the pricing pillar with "q2-2026".

Claude loops research.keyword.update.


Errors

StatusCodeCause
400validation_errorMissing seed or invalid label values
402insufficient_credits
404keyword_not_found
409duplicate_keywordExact-match keyword already exists in this workspace
422pillar_not_foundpillarId doesn't exist or isn't in this workspace
429rate_limited
503external_unavailableDataForSEO or Ahrefs down (partial results may have been saved)

Cost

ActionCredits
POST /v1/research/keyword (fast, 2 methods, per 100 results)3
POST /v1/research/keyword (thorough, all methods, per 100 results)5
POST /v1/research/keyword + llm_mentions+2 per 100
POST /v1/keywords (no labeling)free
POST /v1/keywords (with labeling)0.05 per keyword
POST /v1/keywords/relabel0.05 per keyword
POST /v1/keywords/{id}/check-rank0.5
GET endpointsfree

Use ?dryRun=true to estimate without running.


Use cases (string things together)

A. Discover, filter, and write content for the best keywords

# 1. Discover
INV=$(curl -sf -X POST .../v1/research/keyword \
  -d '{"seed": "project management software", "limit": 500}' | jq -r '.invocationId')

# 2. Wait
until [[ "$(curl -sf .../v1/agent/invocations/$INV | jq -r '.status')" == "SUCCEEDED" ]]; do sleep 5; done

# 3. Pick the best PROBLEM_SOLUTION keywords for blog content
KW_IDS=$(curl -sf -X POST .../v1/keywords/search -d '{
  "intent": ["PROBLEM_SOLUTION"],
  "relevance": ["OFFERING"],
  "minConfidence": 0.85,
  "maxKd": 35,
  "missingFromContent": true
}' | jq -r '.data[] | .id')

# 4. Fire one blog post job per keyword
echo "$KW_IDS" | while read KW; do
  curl -X POST .../v1/produce/blog-post \
    -d "{\"keywordId\": \"$KW\", \"pillarId\": \"pil_pricing\"}"
done

B. Run keyword research as part of the bootstrap agent

# The bootstrap_brand agent calls research.keyword internally as one of its 7 steps.
curl -X POST .../v1/agent/invoke -d '{
  "agent": "bootstrap_brand",
  "input": { "domain": "acme.com" }
}'

→ See the full playbook: Keyword research for a brand in 20 minutes

C. Monitor a new competitor by stealing their keywords

# 1. Add competitor
COMP_ID=$(curl -sf -X POST .../v1/research/competitor \
  -d '{"domain": "monday.com"}' | jq -r '.id')

# 2. Pull their keywords (creates CompetitorKeyword rows)
curl -X POST .../v1/research/competitor/$COMP_ID/keywords

# 3. Run our own keyword research using their domain as the seed
curl -X POST .../v1/research/keyword \
  -d "{\"seed\": \"monday.com\", \"methods\": [\"ahrefs_matching\"]}"

D. Conversational keyword management via MCP

In Claude Code:

> What PURCHASE-intent keywords are we missing landing pages for?
> Drop the ones where KD is above 40.
> Tag the rest "priority-q2" and assign them to the pricing pillar.
> Fire a landing_page generation for each.

Four turns. Six MCP tool calls (research.keyword.search × 2, research.keyword.update × N, produce.landing_page.create × N). The same flow takes 45 minutes in a spreadsheet.


On this page