example

Find the content you're missing

Keywords a competitor ranks for that you don't, with volume and the questions people ask.

A competitor's rankings are a to-do list someone else wrote for you. One gap pull becomes outlines, ordered by volume and difficulty.

Don't know who to run the gap against? Start there. It ends with the closest competitor picked out.

language
prerequisites

Export your key once.

$ setupcurl
export SEOFETCH_KEY=sof_live_…
# the scripts below use jq -- brew install jq / apt-get install jq
step 1

Pull the gap: what do they rank for that you don't?

→ requestcurl
curl https://api.seofetch.com/v1/domains/gap \
  -H "Authorization: Bearer $SEOFETCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domains": ["semrush.com", "ahrefs.com"], "mode": "gap", "engine": "google", "location": 2840, "language": "en", "device": "desktop", "limit": 100, "offset": 0}' \
  -o gap.json
← response200
{
  "id": "doma_6ulpwpudmvigzhrj4wf4jm2k",
  "request_id": "req_5xqa6h3lmbmv7d3zqj55xkrzlu",
  "object": "domain_gap",
  "created_at": "2026-07-29T12:00:00Z",
  "elapsed_ms": 244,
  "credits": {
    "charged": 15,
    "balance": 9857
  },
  "data": {
    "mode": "gap",
    "domains": [
      "semrush.com",
      "ahrefs.com"
    ],
    "items": [
      {
        "keyword": "search engine optimization",
        "volume": 18100,
        "ranks": [
          null,
          {
            "rank": 2,
            "url": "https://ahrefs.com/blog/what-is-seo/",
            "date": "2026-06-28"
          }
        ]
      },
      "…"
    ],
    "totals": [
      2834,
      1315
    ],
    "matched_count": 409,
    "coverage": {
      "from": "2026-06-28",
      "observations": 409
    },
    "limit": 100,
    "offset": 0
  }
}

Volume comes attached. A null on your side of ranks is the article you haven't written.

step 2

Floor it by volume and take the top of the list.

$ localrun locally
jq -c '[.data.items[] | select(.volume >= 1000)] | sort_by(-.volume) | .[:20]' gap.json

The 1,000 floor is a default. Niche sites live happily below it.

step 3

Pull the questions people actually ask for your top gap keyword — shown here for a keyword from another niche; same call, any keyword.

→ requestcurl
curl https://api.seofetch.com/v1/keywords/questions \
  -H "Authorization: Bearer $SEOFETCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"keyword": "running shoes", "location": 2840, "language": "en", "limit": 50}'
← response200
{
  "id": "keyw_e2xp2us5fjowpovkxd3kg5ox",
  "request_id": "req_klhjkyye3fn47cl5uvqmv2z3g4",
  "object": "keyword_questions",
  "created_at": "2026-07-29T12:00:00Z",
  "elapsed_ms": 244,
  "credits": {
    "charged": 20,
    "balance": 9857
  },
  "data": {
    "keyword": "running shoes",
    "items": [
      {
        "question": "How often should you replace running shoes?",
        "first_seen": "2026-06-11",
        "last_seen": "2026-07-25",
        "times_seen": 18
      },
      {
        "question": "Are carbon plate shoes worth it?",
        "first_seen": "2026-07-02",
        "last_seen": "2026-07-27",
        "times_seen": 7
      },
      "…"
    ],
    "count": 42,
    "coverage": {
      "from": "2026-07-01",
      "observations": 130
    }
  }
}

People-Also-Ask, deduplicated, with first- and last-seen dates. An outline that answers what people ask beats one that guesses.

step 4

Score the shortlist; write the winnable ones first.

→ requestcurl
curl https://api.seofetch.com/v1/keywords/difficulty \
  -H "Authorization: Bearer $SEOFETCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"keywords": ["buy running shoes", "cold keyword"], "location": 2840, "language": "en"}'
← response200
{
  "id": "keyw_y4zlhnwzb5oofgqslo64leja",
  "request_id": "req_wtumkpqpubojpcq43dh5hehjja",
  "object": "keyword_difficulty",
  "created_at": "2026-07-29T12:00:00Z",
  "elapsed_ms": 244,
  "credits": {
    "charged": 110,
    "balance": 9857
  },
  "data": {
    "items_count": 2,
    "items": [
      {
        "keyword": "buy running shoes",
        "difficulty": 42,
        "status": "available"
      },
      {
        "keyword": "cold keyword",
        "difficulty": null,
        "status": "pending"
      }
    ]
  }
}

Difficulty and volume order the shortlist. Difficulty is comparative; your authority and intent match decide the rest. A keyword still "pending" gets its score on a later call, so re-ask rather than dropping it.

script

Run the whole thing.

Each language below is the full pipeline. Swap in your domain and a competitor's, export SEOFETCH_KEY, run it. gaps.json comes out sorted by difficulty, pending scores last, questions attached to your pick.

→ rungaps.sh
#!/usr/bin/env bash
# gaps.sh -- gap pull -> volume floor/top 20 -> questions on top pick -> difficulty batch -> gaps.json.
set -euo pipefail
: "${SEOFETCH_KEY:?export SEOFETCH_KEY first}"
YOU="${TARGET_DOMAIN:-yourdomain.com}"
THEM="${COMPETITOR_DOMAIN:-competitor.com}"
api() { curl -sS --fail-with-body "https://api.seofetch.com$1" \
  -H "Authorization: Bearer $SEOFETCH_KEY" \
  -H "Content-Type: application/json" \
  -d "$2"; }

# 1. what do they rank for that you don't
api /v1/domains/gap \
  "$(jq -cn --arg y "$YOU" --arg t "$THEM" '{domains: [$y, $t], mode: "gap", engine: "google", location: 2840, language: "en", device: "desktop", limit: 100, offset: 0}')" \
  > gap.json

# 2. floor by volume (1000 -- policy knob), top 20
jq -c '[.data.items[] | select(.volume >= 1000)] | sort_by(-.volume) | .[:20]' gap.json > top20.json
[ "$(jq length top20.json)" -gt 0 ] || { echo "nothing clears the volume floor"; exit 0; }

# 3. questions for the top pick
PICKS=1   # probe more picks: +20cr each
: > questions.jsonl
jq -r ".[0:${PICKS}][].keyword" top20.json | while IFS= read -r kw; do
  api /v1/keywords/questions \
    "$(jq -cn --arg k "$kw" '{keyword: $k, location: 2840, language: "en", limit: 50}')" |
    jq -c --arg k "$kw" '{keyword: $k, questions: [.data.items[].question]}' >> questions.jsonl
done

# 4. difficulty on the whole shortlist
api /v1/keywords/difficulty \
  "$(jq -s '{keywords: [.[].keyword], location: 2840, language: "en"}' top20.json)" > difficulty.json

# 5. merge: keyword, volume, difficulty, questions[] -- winnable first
jq -n --slurpfile top20 top20.json --slurpfile diff difficulty.json --slurpfile qs <(jq -s '.' questions.jsonl) '
  ($diff[0].data.items | map({(.keyword): .}) | add) as $dmap |
  ($qs[0] | map({(.keyword): .questions}) | add // {}) as $qmap |
  [$top20[0][] | {
    keyword, volume,
    difficulty: ($dmap[.keyword].difficulty // null),
    status: ($dmap[.keyword].status // "pending"),
    questions: ($qmap[.keyword] // [])
  }] | sort_by(if .difficulty == null then 999 else .difficulty end)
' > gaps.json
echo "gaps.json: $(jq length gaps.json) keywords, winnable first"