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 (
KeywordRankrecords 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
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/research/keyword | Run a research job (agentic, async) |
| GET | /v1/keywords | List keywords with filters |
| POST | /v1/keywords/search | Richer label/filter search |
| GET | /v1/keywords/{id} | Get one keyword + rank history |
| POST | /v1/keywords | Create one or many |
| POST | /v1/keywords/bulk | Bulk create (up to 5,000) |
| PATCH | /v1/keywords/{id} | Update labels, status, pillar, tags |
| DELETE | /v1/keywords/{id} | Archive (soft delete) |
| POST | /v1/keywords/relabel | Re-run the labeling pass on a scope |
| POST | /v1/keywords/{id}/check-rank | Trigger a fresh rank check |
| GET | /v1/keywords/{id}/rank-history | Historical 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
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
seed | string | yes | — | A keyword or a domain (e.g., acme.com) |
depth | "fast" | "thorough" | no | "thorough" | fast runs 2 methods; thorough runs all available |
methods | string[] | no | depth defaults | Override depth. Available: related_keywords, keyword_ideas, ahrefs_matching, ahrefs_related, ahrefs_suggestions, llm_mentions, serp_neighbors |
limit | number | no | 500 | Max keywords kept after dedup |
country | ISO-3166-2 | no | workspace default | — |
language | ISO-639-1 | no | workspace default | — |
label | boolean | no | true | Run 2D labeling pass |
saveTo | "workspace" | pillar_id | null | no | "workspace" | Where to persist results |
tags | string[] | no | [] | Tags applied to every created keyword |
dryRun | boolean | no | false | Estimate 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
| Param | Type | Notes |
|---|---|---|
intent | csv | URGENCY, PURCHASE, LOCATION, SPECIFICATION, PRODUCT, PROBLEM_SOLUTION, ALTERNATIVE |
relevance | csv | OFFERING, ALTERNATIVE, COMPLEMENTARY, INCUMBENT |
pillar | string | Pillar ID |
tag | string | Tag slug |
status | csv | RAW, LABELLING, LABELED, FOCUSED, ARCHIVED |
priority | csv | CRITICAL, HIGH, MEDIUM, LOW, BACKLOG |
minVolume | number | — |
maxKd | number | — |
q | string | Substring match on the keyword |
hasContent | boolean | Filter to keywords with / without a linked blog post or landing page |
limit | number (max 500) | Default 100 |
cursor | string | Pagination |
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
| Status | Code | Cause |
|---|---|---|
| 400 | validation_error | Missing seed or invalid label values |
| 402 | insufficient_credits | — |
| 404 | keyword_not_found | — |
| 409 | duplicate_keyword | Exact-match keyword already exists in this workspace |
| 422 | pillar_not_found | pillarId doesn't exist or isn't in this workspace |
| 429 | rate_limited | — |
| 503 | external_unavailable | DataForSEO or Ahrefs down (partial results may have been saved) |
Cost
| Action | Credits |
|---|---|
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/relabel | 0.05 per keyword |
POST /v1/keywords/{id}/check-rank | 0.5 |
GET endpoints | free |
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\"}"
doneB. 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.
Related
- Concept: 2D keyword labelling
- Concept: Keywords
- API: Research · serp
- API: Research · competitor
- API: Production · blog post
- API: Production · landing page
- API: Agent · invoke
- Playbook: Keyword research for a brand in 20 minutes
- Playbook: From competitor URL to content plan
Approval
REST and MCP endpoints for the CitationBench agent approval queue. List pending approvals, approve (optionally with edits), reject (optionally with rerun config), or comment without deciding.
SERP
Fetch the full SERP for any query — organic results, AI Overviews, featured snippets, People Also Ask, Reddit blocks, video carousels, local pack — parsed, persisted, and diffable over time.