v1 betaComing soon · Max plan

KeywordQuasar API docs

Keyword search volume for any city or country, keyword ideas, cost per click, Bing, a Google Trends link and the businesses there to sell to, as JSON. The API is in beta: during the beta, LeadQuasar issues keys by hand to an existing account, and fields may still be added.

Overview

  • Base address: https://leadquasar.com/api/v1. Requests and answers are JSON, and every answer is sent with Cache-Control: no-store.
  • Status: Coming soon · Max plan · Beta. At launch, keys are for accounts on Max.
  • Call it from a server, a script or a workflow tool. CORS is off: a browser page cannot call it, and a key in a web page is a key anyone can use.
  • One successful report is one lookup against the key’s daily limit. Errors are never charged.

Authentication

Send the key on every request as a Bearer token. A key starts with lq_live_, is shown once when it is issued, and is stored by LeadQuasar only as a hash, so keep it somewhere safe, such as an environment variable.

Authorization: Bearer lq_live_…

The endpoint needs a key with the keywordquasar scope. During the beta only keys issued by LeadQuasar have it; the keys you make for the account API in Settings → Integrations do not, and are answered 403 insufficient_scope. A paused, revoked or expired key is answered 403 too, and so is a key whose account is suspended (account_suspended).

A KeywordQuasar key is issued to your LeadQuasar account by LeadQuasar. It is not listed among the keys you make in Settings → Integrations → REST API, and is paused or revoked by LeadQuasar: ask us if it needs to change.

A missing or unknown key is answered 401 unauthorized and counted against your address: after 30 of them in 10 minutes, every request from that address is refused with 429 rate_limited for up to 10 minutes, even one with a valid key. Check the key before you retry in a loop.

Keyword report

POST /api/v1/keyword-research

Everything KeywordQuasar knows about one keyword in one place: Google’s monthly searches, ideas, cost per click and competition, Bing, the businesses there, the best cities and a Google Trends link. The report is the full one, as on the Max plan: nothing in it is locked.

Request body

Request fields
FieldTypeRequiredWhat it is
keywordstringYesThe search to measure, 2 to 80 characters after spaces are trimmed: "plumber", "roof repair".
locationstringYesA city, county, state, region or country, up to 120 characters: "Austin, TX", "Texas", "United Kingdom".
countrystringNoAn ISO 3166-1 alpha-2 code ("US", "GB", "DE") to look for the place in one country only.
geoIdstringNoGoogle's id for the place, digits only, from a location_ambiguous answer or the locations endpoint. Send it with country.
languagestringNoA Google Ads language code ("en", "de"). Left out, the place's main language is used; an unknown code is ignored.
Example body
{
  "keyword": "plumber",
  "location": "Austin, TX",
  "country": "US"
}

Response

Example: "plumber" in Austin, TX. The place and Google's figures are Google's own answer, measured 30 Oct 2026; Bing, business and usage values change daily, so they are shown as their types.

200 OK
{
  "data": {
    "keyword": "plumber",
    "location": {
      "name": "Austin",
      "city": "Austin",
      "state": "Texas",
      "country": "US",
      "geoId": "1026201",
      "targetType": "City"
    },
    "language": "en",
    "google": {
      "status": "ok",
      "currency": "USD",
      "main": {
        "keyword": "plumber",
        "searches": 3600,
        "competition": "LOW",
        "bidLow": 33.09,
        "bidHigh": 113.13
      },
      "ideas": [
        {
          "keyword": "emergency plumber",
          "searches": 480,
          "competition": "LOW",
          "bidLow": 52.29,
          "bidHigh": 178.5
        },
        <more ideas, up to 500 in all>
      ]
    },
    "bing": {
      "volume30d": <integer | null>,
      "cityPhrase30d": <integer | null>,
      "related": [
        <string>,
        <…>
      ]
    },
    "businesses": {
      "total": <integer>,
      "capped": <boolean>,
      "noWebsite": <integer | null>,
      "withEmail": <integer | null>,
      "scope": "city",
      "scopeName": "Austin"
    },
    "notCovered": false,
    "bestCities": [
      {
        "city": <string>,
        "state": <string | null>,
        "businesses": <integer>
      },
      <up to 50 cities>
    ],
    "bestCitiesCapped": <boolean>,
    "leadListUrl": <URL | null>,
    "trends": {
      "exploreUrl": "https://trends.google.com/trends/explore?date=today+12-m&geo=US-TX&q=plumber",
      "embedUrl": "https://trends.google.com/trends/embed/explore/TIMESERIES?req=%7B%22comparisonItem%22%3A%5B%7B%22keyword%22%3A%22plumber%22%2C%22geo%22%3A%22US-TX%22%2C%22time%22%3A%22today%2012-m%22%7D%5D%2C%22category%22%3A0%2C%22property%22%3A%22%22%7D&tz=0&eq=q%3Dplumber%26geo%3DUS-TX%26date%3Dtoday%2012-m"
    },
    "generatedAt": <ISO 8601 time>
  },
  "usage": {
    "usedToday": <integer>,
    "limitToday": <integer>
  },
  "meta": {
    "apiVersion": "v1",
    "source": "Google Ads Keyword Planner, Bing Webmaster Tools, LeadQuasar business data"
  }
}
Response fields
FieldTypeWhat it is
data.keywordstringThe keyword as it was looked up.
data.locationobjectGoogle's place: name, city (null unless a city), state, country, geoId and targetType (City, State, Country, County…).
data.languagestringThe Google Ads language the lookup was asked in.
data.google.statusstring"ok". When Google is busy, at its daily cap or down the API answers 503 upstream_unavailable instead, with no data, and the lookup is not charged; busy adds Retry-After: 60.
data.google.currencystring or nullThe currency every bid is in.
data.google.mainobject or nullThe keyword itself: searches (average month, null when Google gives no figure), competition (LOW, MEDIUM, HIGH or null), bidLow and bidHigh (top-of-page cost per click).
data.google.ideasarrayRelated searches in the same place, same shape as main, most searched first: up to 500.
data.bingobject or nullBing Webmaster data: volume30d (Bing searches over the last four weeks, country-wide), cityPhrase30d (the keyword typed with the city) and related keywords. Null where Bing has no market.
data.businessesobject or nullLeadQuasar's businesses of that kind in the area: total, noWebsite and withEmail (each null when not counted), capped (true: each figure is a floor, read it as "N+"), scope (city, state or country) and scopeName, the area counted. Null when LeadQuasar does not cover the niche or place.
data.notCoveredbooleanTrue when LeadQuasar does not cover the niche or place yet.
data.bestCitiesarrayThe cities in the country with the most of these businesses: city, state (null where Google names none) and businesses, up to 50.
data.bestCitiesCappedbooleanTrue when the city counts are floors ("N+").
data.leadListUrlstring or nullA public LeadQuasar page listing those businesses: Find Leads for the niche and place in the US, the country's industry page elsewhere. Null when there is none.
data.trendsobjectexploreUrl (the chart on Google Trends) and embedUrl (Google's official embed of it), for the past 12 months.
data.generatedAtstringWhen the report was built (ISO 8601), not when Google or Bing measured.
usageobjectusedToday and limitToday: this key's lookups today, and its daily limit.
metaobjectapiVersion ("v1") and the data sources.

Places

GET /api/v1/keyword-research/locations?q=<text>&country=<ISO-2>

The places Google Ads can target whose name matches q, optionally in one country, so you can send a geoId straight away. Each call uses one of the key’s place searches for the day, the same count its lookups use (see Limits), and never uses up a lookup.

curl
curl "https://leadquasar.com/api/v1/keyword-research/locations?q=Springfield&country=US" \
  -H "Authorization: Bearer $LEADQUASAR_API_KEY"
200 OK
{
  "data": {
    "choices": [
      {
        "geoId": <string>,
        "name": "Springfield",
        "city": "Springfield",
        "state": "Illinois",
        "country": "US",
        "targetType": "City"
      },
      <…>
    ]
  }
}

Errors

Every error has the same shape as LeadQuasar’s other /api/v1 endpoints: an error code to branch on and a message to show a person. The message may change; the code will not. Every 429 carries a Retry-After header, in seconds.

429
{
  "error": "rate_limited",
  "message": "This key's 100 lookups for today are used. They reset at 00:00 UTC (in 5 h 12 min)."
}
Errors
StatuserrorWhenCharged
400invalid_requestA field is missing or out of range, or the body is not JSON or is larger than 4,096 bytes; the message names the field.No
401unauthorizedNo key, a malformed one or one that does not exist (the same answer for all three). Counted against your address.No
403key_pausedThe key is paused.No
403key_revokedThe key was revoked.No
403key_expiredThe key is past its expiry date.No
403insufficient_scopeThe key has no keywordquasar scope (an account API key, for example).No
403account_suspendedThe account the key belongs to is suspended.No
404location_not_foundGoogle knows no place by that name.No
409location_ambiguousMore than one place matches. data.choices lists up to 8; send the request again with the geoId and country of one.No
429rate_limitedWith Retry-After. One of: the key's lookups for today are used; its place searches for today are used; more than 60 calls a minute or 5,000 a day on the key; or too many requests with an invalid key from your address (refused for up to 10 minutes, even with a valid key). The message says which and when it resets.No
500internal_errorSomething failed on LeadQuasar's side. Try again.No
503upstream_unavailableGoogle is busy (its per-minute limit: the answer then carries Retry-After: 60, so try again in a minute), at its daily cap or down. The answer is the error alone: no data and no usage. Try again later.No

Google unavailable

When Google is busy, at its daily cap or down, the answer is a 503 and the error alone: no data, no usage, no partial report. Nothing is charged; try again later. When Google is only busy (its per-minute limit), the 503 carries Retry-After: 60: a minute is enough.

503
{
  "error": "upstream_unavailable",
  "message": "Google's search data is unavailable just now (busy, or today's limit reached). This lookup was not charged. Try again later."
}

An ambiguous place

A 409 lists the places that match in data.choices. Send the request again with the geoId and country of the one you mean.

409
{
  "error": "location_ambiguous",
  "message": "That location matches more than one place. Send the request again with the geoId and country of one of data.choices.",
  "data": {
    "choices": [
      {
        "geoId": <string>,
        "name": "Springfield",
        "city": "Springfield",
        "state": "Illinois",
        "country": "US",
        "targetType": "City"
      },
      {
        "geoId": <string>,
        "name": "Springfield",
        "city": "Springfield",
        "state": "Missouri",
        "country": "US",
        "targetType": "City"
      },
      <up to 8 places>
    ]
  }
}

Limits

Limits
Lookups a daySet per key when it is issued: 100 unless agreed otherwise, and at most 300 during the beta. One successful report is one lookup, and the day resets at 00:00 UTC. When it is spent: 429 rate_limited, with Retry-After.
Calls per key60 a minute and 5,000 a day, whatever the endpoint. Past either: 429 rate_limited, with Retry-After; the message says which and when it resets (the next minute, or 00:00 UTC).
Place searches a dayEvery lookup uses one, and so does every call to the locations endpoint; the two share the key's day: three per lookup in its day, at most 300 (300 for a key with the default 100 lookups). A place search never uses up a lookup. When they are spent: 429 rate_limited until 00:00 UTC, with Retry-After.
Invalid keysAfter 30 requests with a missing or unknown key from one address in 10 minutes, that address is refused (429 rate_limited, Retry-After) for up to 10 minutes, even with a valid key. A paused, revoked or expired key does not count.
Not chargedEvery error: invalid requests, unknown or ambiguous places, refused keys, 500, and 503 when Google is busy or down (which carries no data and no usage).
Repeat lookupsGoogle and Bing answers are kept 30 days and shared, so a repeat is fast. It still counts as a lookup.
Request bodyUp to 4,096 bytes of JSON. A larger body is answered 400 invalid_request.
Keyword2 to 80 characters
LocationUp to 120 characters
Keyword ideas per reportUp to 500
Best cities per reportUp to 50
Places offered when a name is ambiguousUp to 8
Where to call fromYour server. CORS is off, and a key in a web page is a key anyone can use.

Examples

One lookup in each language. Set LEADQUASAR_API_KEY in your environment first.

curl
curl -X POST https://leadquasar.com/api/v1/keyword-research \
  -H "Authorization: Bearer $LEADQUASAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"keyword":"plumber","location":"Austin, TX","country":"US"}'
JavaScript (fetch)
// Node 18+ (fetch is built in). Keep the key on your server.
const res = await fetch("https://leadquasar.com/api/v1/keyword-research", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LEADQUASAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ keyword: "plumber", location: "Austin, TX", country: "US" }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error}: ${body.message}`);

const { main, ideas } = body.data.google;
console.log(main.searches, main.bidLow, main.bidHigh, ideas.length);
Python (requests)
import os
import requests

res = requests.post(
    "https://leadquasar.com/api/v1/keyword-research",
    headers={"Authorization": f"Bearer {os.environ['LEADQUASAR_API_KEY']}"},
    json={"keyword": "plumber", "location": "Austin, TX", "country": "US"},
    timeout=60,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{body['error']}: {body['message']}")

main = body["data"]["google"]["main"]
print(main["searches"], main["bidLow"], main["bidHigh"])

A helper, and picking a place after a 409

The workflows on the API page call this lookup(). It returns the 200’s body and throws any error with its code.

JavaScript
async function lookup(body) {
  const res = await fetch("https://leadquasar.com/api/v1/keyword-research", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.LEADQUASAR_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw Object.assign(new Error(json.message), { code: json.error, data: json.data });
  return json;
}

let report;
try {
  report = await lookup({ keyword: "plumber", location: "Springfield" });
} catch (err) {
  if (err.code !== "location_ambiguous") throw err;
  const place = err.data.choices.find((c) => c.state === "Illinois");
  report = await lookup({ keyword: "plumber", location: "Springfield", geoId: place.geoId, country: place.country });
}

Changelog

  • v1 beta · Nov 2026

    First version: POST /api/v1/keyword-research and GET /api/v1/keyword-research/locations. Keys are issued by LeadQuasar to an existing account during the beta.