# Keyword Gap & Opportunity Report - Competitor Keyword Gap

> Keywords up to 3 competitors rank for on Google and your client's domain doesn't: volume, difficulty, intent, competing URLs and why each was picked. Top 20 re-checked on the live Google top 10. CSV-ready dataset + HTML report. One flat price per report.

- Category: Keyword data
- Price: $9 per keyword gap report, billed per result by Apify (no subscription); any other billed events are listed on the Store page. Cap any run with `maxTotalChargeUsd`.
- Store page: https://apify.com/wrenfield-data/keyword-gap-report
- Human page: https://wrenfielddata.com/tools/keyword-gap-report/
- OpenAPI 3 spec: https://api.apify.com/v2/acts/wrenfield-data~keyword-gap-report/builds/default/openapi.json
- MCP tool: https://mcp.apify.com?tools=wrenfield-data/keyword-gap-report (appears as `wrenfield-data--keyword-gap-report`)

## Call it

Synchronous run that returns the dataset rows as JSON (use `/runs` for long jobs and poll; see the OpenAPI spec):

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/wrenfield-data~keyword-gap-report/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"clientDomain": "helpscout.com", "competitors": ["zendesk.com", "freshworks.com", "intercom.com"], "location": "United States", "language": "English"}'
```

## Input

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `clientDomain` | string | yes | `"helpscout.com"` | The site you want new keywords for, e.g. helpscout.com. https://, www. and paths are removed. |
| `competitors` | array | yes | `["zendesk.com", "freshworks.com", "intercom.com"]` | 1 to 3 competitors that rank where you want to rank, e.g. zendesk.com. Pick real search competitors (the sites that outrank you), not only business rivals. |
| `location` | string |  | `"United States"` | Country of the Google results: name (United States), 2-letter code (GB) or location code (2840). |
| `language` | string |  |  | Language name or code (English, de). Empty = main language of the country. |
| `excludeTerms` | array |  |  | Drop keywords that contain any of these words or phrases, e.g. jobs, free download, your own brand. Case-insensitive. |
| `excludeCompetitorBrands` | boolean |  | `true` | Drop keywords containing a competitor's name taken from its domain (for example zendesk from zendesk.com). Usually what you want: you can't win "zendesk pricing". |
| `topicTerms` | array |  |  | Keep only keywords containing at least one of these words, e.g. help desk, support, ticket. Useful when competitors are broad sites. |
| `minSearchVolume` | integer |  | `30` | Skip keywords with fewer Google searches a month. 0 also keeps keywords whose volume is unavailable. |
| `maxKeywordDifficulty` | integer |  | `100` | Skip keywords harder than this (0-100). 100 = no limit. |
| `maxCompetitorPosition` | integer |  | `20` | Only count a competitor ranking at this position or better. 20 = first two pages (recommended); 10 = page one only. |
| `includeNavigational` | boolean |  | `false` | Keep brand and login searches ("x login", "x support phone"). Off by default: they are rarely winnable. |
| `includeCompetitorSubdomains` | boolean |  | `false` | Also count rankings on competitor subdomains. Off by default, because hosted subdomains (for example customer help centers on *.zendesk.com) are not the competitor's own content. |
| `candidatesPerCompetitor` | integer |  | `100` | How many gap keywords to pull per competitor (highest competitor traffic first), before merging duplicates. Max 100, so a report has up to 300. |
| `liveSerpChecks` | integer |  | `20` | How many of the best candidates to re-check on the live Google top 10 (confirms the gap is still open today). Max 20. 0 = database only. |
| `preparedBy` | string |  |  | Your name or agency, shown on the HTML report. No Wrenfield branding appears in the report. |
| `maxSpendUsd` | integer |  |  | Optional. The run stops before collecting anything, and charges nothing, if the report price is above this. Same effect as "Maximum cost per run" in Run options. |

## Example input

```json
{
  "clientDomain": "helpscout.com",
  "competitors": [
    "zendesk.com",
    "freshworks.com",
    "intercom.com"
  ],
  "location": "United States",
  "language": "English"
}
```

## Output fields

One flat row per result:

| Field | Type | Description |
|---|---|---|
| `rank` | integer | Position in this report (live-confirmed gaps first, then by opportunity score) |
| `keyword` | string | Keyword at least one competitor ranks for on Google and the client domain does not |
| `opportunity_score` | number | 0-100: 25% demand, 20% ease (100 - KD), 20% topic fit, 15% competitor overlap, 10% intent, 10% best competitor position |
| `search_volume` | integer | Average monthly Google searches (Google Ads data). null = unavailable |
| `search_volume_status` | string | measured, zero (Google reports no searches) or unavailable (no value; not the same as zero) |
| `search_volume_month` | string | Latest month covered by the volume data (YYYY-MM) |
| `cpc_usd` | number | Average Google Ads cost per click, USD |
| `competition_level` | string | Google Ads competition: LOW, MEDIUM, HIGH |
| `topic_fit` | number | 0-1: share of the keyword's words found on the client's homepage (title, headings, links). null = site could not be read |
| `topic_fit_status` | string | measured or unavailable |
| `keyword_difficulty` | integer | Keyword difficulty 0-100. null = unavailable |
| `keyword_difficulty_status` | string | measured or unavailable |
| `search_intent` | string | Main intent: informational, commercial, transactional, navigational |
| `search_intent_status` | string | measured or unavailable |
| `competitors_ranking` | integer | How many of the competitors rank for the keyword (database) |
| `competitor_positions` | string | Each ranking competitor with its Google position, e.g. 'zendesk.com #4; intercom.com #7' |
| `best_competitor` | string | Competitor with the best position |
| `best_competitor_position` | integer | That competitor's Google position (database) |
| `best_competitor_url` | string | The competitor page that ranks |
| `competing_urls` | string | All ranking competitor URLs, separated with ' \| ' |
| `competitor_traffic_estimate` | integer | Modeled monthly visits competitors get from the keyword (provider estimate) |
| `serp_features_labs` | string | SERP features seen in the database snapshot (AI Overview, People Also Ask, ...) |
| `selection_reasons` | string | Why the keyword was picked, in plain language, separated with '; ' |
| `live_checked` | boolean | Whether the keyword was re-checked on the live Google top 10 |
| `live_verdict` | string | open gap, client already ranks, competitors not in top 10 now, check failed, or not checked |
| `client_position_live` | integer | Client's live Google position if in the top 10, else null |
| `competitor_positions_live` | string | Competitors in the live top 10 with positions |
| `top10_domains_live` | string | Domains in the live top 10, in order |
| `serp_features_live` | string | SERP features on the live page |
| `ai_overview_live` | boolean | An AI Overview was shown on the live page |
| `ai_overview_cites_client` | boolean | The live AI Overview cites the client domain (null if no AI Overview) |
| `labs_serp_observed_at` | string | When the provider last observed the Google results behind the database rankings (observation time) |
| `labs_retrieved_at` | string | When this run queried the database (retrieval time) |
| `serp_checked_at` | string | When the live Google results were observed |
| `serp_retrieved_at` | string | When this run requested the live check |
| `client_domain` | string | Client domain |
| `competitors` | string | Competitors used |
| `location` | string | Country |
| `language` | string | Language |

## Notes

- Auth: an Apify API token as `Authorization: Bearer <token>`; the MCP server also supports OAuth sign-in.
- Failed or empty results are not charged on most tools; check the pricing on the Store page.
- Questions: hello@wrenfielddata.com
