edit.pink
Pink Editor docs

Research, keywords and SERP

Web research, keyword data, the keyword and competitor landscape, backlink data and competitor tracking.

API v1

These tools do paid work for you: they pull keyword, SERP or backlink data, or research the web. See Credits and pricing for what each call costs and Limits for the caps.

Each description below is the text your AI client receives for that tool, word for word. It is written for the model, which is why it reads like instructions.

research

SLOW (30–60s per call). Four modes: keywords (seed is 3–5 short seeds, each 1–3 words, comma-separated; returns volume/CPC in USD (cpc_usd)/intent), web (web search + scrape + synthesis of facts/opinions, retrievable later via recall), reddit (same as web but reddit-scoped), url (USER PROVIDES THE URL in seed; scrape that one page + synthesis; question is optional context). Fire only after recall came back empty. Concurrency: max 10 web/url/reddit calls in-flight GLOBALLY; excess returns {error:"concurrency_limit_exceeded", max_concurrent:10, retry_after_ms:30000} — back off and retry.

Parameters

ParameterTypeRequiredDescription
modekeywords, web, reddit, urlyes
seedstringyesKeywords mode: 3–5 short seeds (each 1–3 words) comma-separated. Web/reddit: natural-language query. URL mode: the URL to scrape (https://…).
questionstringURL mode only: optional context — what question are you trying to answer from this URL?
page_idinteger or nullOptional: link keyword-mode results to this page.

Input schema

{
  "type": "object",
  "properties": {
    "mode": {
      "type": "string",
      "enum": [
        "keywords",
        "web",
        "reddit",
        "url"
      ]
    },
    "seed": {
      "type": "string",
      "description": "Keywords mode: 3–5 short seeds (each 1–3 words) comma-separated. Web/reddit: natural-language query. URL mode: the URL to scrape (https://…)."
    },
    "question": {
      "type": "string",
      "description": "URL mode only: optional context — what question are you trying to answer from this URL?"
    },
    "page_id": {
      "type": [
        "integer",
        "null"
      ],
      "description": "Optional: link keyword-mode results to this page."
    }
  },
  "required": [
    "mode",
    "seed"
  ]
}

get_keywords

Pull fresh keyword metrics (volume/CPC/competition) for seeds[], topic_id, or keyword_id. Upserts into cm_keywords; if topic_id, attaches to cm_topic_keywords. Locale = keyword_id's row locale (if provided) → site default → WW/en. Use keyword_id when back-filling metrics for a specific cm_keywords row — that locks the call to the row's locale so you don't accidentally populate a sibling row at a different locale. With keyword_id or topic_id, a seed row the data has nothing for is stored as volume 0 (checked, no volume data — not re-fetched) and listed in no_data[].

Parameters

ParameterTypeRequiredDescription
seedsarray of string
topic_idinteger
keyword_idintegerTarget a specific cm_keywords row — locks locale to that row.

Input schema

{
  "type": "object",
  "properties": {
    "seeds": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "topic_id": {
      "type": "integer"
    },
    "keyword_id": {
      "type": "integer",
      "description": "Target a specific cm_keywords row — locks locale to that row."
    }
  }
}

serp_landscape

The keyword + competitor intelligence tool. Pick ONE scope (keyword, topic_id, or page_id) and a depth: metrics (default, read-only) returns keyword demand for every committed keyword plus stored SERP competition, with a coverage header and an SOV verdict that is suppressed below 50% volume coverage — so a partial read never reads as "who owns the theme". list also fetches current SERP positions for the scope (caps: keyword=1, page=all, topic=top 10 by score; pass limit or keyword_ids to change; already-fetched keywords are skipped). analyze also runs web research (facts/opinions) per keyword INLINE — this call does the research before responding, so it can take ~45–60 SECONDS. WAIT for it; do not abort or assume it hung. The research comes back IN THE SAME RESPONSE under research.facts_by_keyword. Hard-capped at 5 keywords/call (on a topic, defaults to the top 5 by score; pass keyword_ids=[…] to choose which). Already-researched keywords and re-calls return INSTANTLY. If research.status is partial, some keywords are still in process — re-call with the same params to collect them. Set ideas: true (metrics only) to also pull up to 100 keyword ideas (is_idea:true) — these are candidates, not committed, and are never list/analyze targets. cpc is USD.

Parameters

ParameterTypeRequiredDescription
keywordstringScope: a single keyword. Pass exactly ONE of keyword / topic_id / page_id.
topic_idintegerScope: a topic — its keyword links + its pages' primary/secondary keywords, with the per-domain SOV rollup.
page_idintegerScope: a page — its primary/secondary keywords.
depthmetrics, list, analyzemetrics (default, read) | list (fetch SERP positions) | analyze (list + INLINE web research; SLOW — blocks ~45–60s while it researches, then returns facts in research.facts_by_keyword. Wait for it. status:"partial" → some keywords still in process, re-call to collect them).
ideasbooleanmetrics-only: also pull up to 100 keyword ideas (is_idea:true). Default false.
limitintegertopic list/analyze: how many keywords to fetch (top N by score). Default 10 for list; analyze defaults to and is capped at 5.
keyword_idsarray of integertopic list/analyze: explicit committed keyword_ids to fetch instead of top-by-score.

Input schema

{
  "type": "object",
  "properties": {
    "keyword": {
      "type": "string",
      "description": "Scope: a single keyword. Pass exactly ONE of keyword / topic_id / page_id."
    },
    "topic_id": {
      "type": "integer",
      "description": "Scope: a topic — its keyword links + its pages' primary/secondary keywords, with the per-domain SOV rollup."
    },
    "page_id": {
      "type": "integer",
      "description": "Scope: a page — its primary/secondary keywords."
    },
    "depth": {
      "type": "string",
      "enum": [
        "metrics",
        "list",
        "analyze"
      ],
      "description": "metrics (default, read) | list (fetch SERP positions) | analyze (list + INLINE web research; SLOW — blocks ~45–60s while it researches, then returns facts in research.facts_by_keyword. Wait for it. status:\"partial\" → some keywords still in process, re-call to collect them)."
    },
    "ideas": {
      "type": "boolean",
      "description": "metrics-only: also pull up to 100 keyword ideas (is_idea:true). Default false."
    },
    "limit": {
      "type": "integer",
      "description": "topic list/analyze: how many keywords to fetch (top N by score). Default 10 for list; analyze defaults to and is capped at 5."
    },
    "keyword_ids": {
      "type": "array",
      "items": {
        "type": "integer"
      },
      "description": "topic list/analyze: explicit committed keyword_ids to fetch instead of top-by-score."
    }
  }
}

Backlink profiles — ONE tool, the scope arg picks the shape (like serp_landscape's depth). scope:"all" (default) or "site" → the site's own profile (+ its tracked competitors): referring domains (the metric that predicts rankings, not raw backlinks), rank, backlinks:referring ratio, spam score, broken links, dofollow ratio, anchor-text distribution; the trend over time for the site AND each competitor; the link gap (domains linking competitors but not you, ranked for outreach); and a health[] array of interpretation flags (ratio_below_3:1, spam_clean, velocity_below_competitors, …). scope:"" → the same profile for any domain, tracked or not. A lookup does NOT add the domain to your tracked competitors — to monitor a domain continuously use add_competitor instead. DEPTH — set depth:"links" (with a scope domain, or "site" for your own) to drill into that domain's individual links: each linking page, the page it points to, the anchor, and an authority score (0–1000, higher = stronger), ordered by authority. depth:"overview" (default) returns the profiles above. Returns { empty:true } when the site has no backlink data yet. trend_months default 18 (max 36); links limit default 100 (max 1000).

Parameters

ParameterTypeRequiredDescription
scopestring"all" (site + competitors, default), "site" (own only), or any domain.
depthoverview, links"overview" (default) = profiles + trend + gap. "links" = the individual linking pages for the scope domain, ranked.
trend_monthsnumberMonths of trend history to return (default 18, max 36).
limitnumberFor depth:"links", how many linking pages to return (default 100, max 1000).

Input schema

{
  "type": "object",
  "properties": {
    "scope": {
      "type": "string",
      "description": "\"all\" (site + competitors, default), \"site\" (own only), or any domain."
    },
    "depth": {
      "type": "string",
      "enum": [
        "overview",
        "links"
      ],
      "description": "\"overview\" (default) = profiles + trend + gap. \"links\" = the individual linking pages for the scope domain, ranked."
    },
    "trend_months": {
      "type": "number",
      "description": "Months of trend history to return (default 18, max 36)."
    },
    "limit": {
      "type": "number",
      "description": "For depth:\"links\", how many linking pages to return (default 100, max 1000)."
    }
  }
}

serp_keyword_refresh

SLOW. Force a fresh single-keyword SERP pull (capped at 20/day/site) — the current top-20 organic results plus any untracked competitors that show up there. Use this when you're about to write about a keyword and want current top-10 data, or when serp_landscape shows no competition data for it yet. Currently US/English Google only. Returns the stored row plus an untracked_competitors list of domains that appeared in the top 10 but aren't tracked yet — add them with add_competitor if worth tracking.

Parameters

ParameterTypeRequiredDescription
keywordstringyesThe exact keyword string to refresh.

Input schema

{
  "type": "object",
  "properties": {
    "keyword": {
      "type": "string",
      "description": "The exact keyword string to refresh."
    }
  },
  "required": [
    "keyword"
  ]
}

add_competitor

Track a competitor domain so it appears in the keyword-landscape and backlinks views with its share-of-voice and backlink profile populated. Use THIS instead of db_insert on cm_competitors, which would leave an incomplete row. Enforces the per-site cap of 10 active competitors. Adding a competitor starts ongoing monitoring of it — confirm with the user before adding. To stop monitoring, db_delete the cm_competitors row.

Parameters

ParameterTypeRequiredDescription
domainstringyesCompetitor domain. Scheme/path are stripped automatically.
display_namestringOptional friendly label shown in the dashboard.
colorstringOptional hex color for the competitor in charts.

Input schema

{
  "type": "object",
  "properties": {
    "domain": {
      "type": "string",
      "description": "Competitor domain. Scheme/path are stripped automatically."
    },
    "display_name": {
      "type": "string",
      "description": "Optional friendly label shown in the dashboard."
    },
    "color": {
      "type": "string",
      "description": "Optional hex color for the competitor in charts."
    }
  },
  "required": [
    "domain"
  ]
}

On this page