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 withCache-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
| Field | Type | Required | What it is |
|---|---|---|---|
| keyword | string | Yes | The search to measure, 2 to 80 characters after spaces are trimmed: "plumber", "roof repair". |
| location | string | Yes | A city, county, state, region or country, up to 120 characters: "Austin, TX", "Texas", "United Kingdom". |
| country | string | No | An ISO 3166-1 alpha-2 code ("US", "GB", "DE") to look for the place in one country only. |
| geoId | string | No | Google's id for the place, digits only, from a location_ambiguous answer or the locations endpoint. Send it with country. |
| language | string | No | A Google Ads language code ("en", "de"). Left out, the place's main language is used; an unknown code is ignored. |
{
"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.
{
"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"
}
}| Field | Type | What it is |
|---|---|---|
| data.keyword | string | The keyword as it was looked up. |
| data.location | object | Google's place: name, city (null unless a city), state, country, geoId and targetType (City, State, Country, County…). |
| data.language | string | The Google Ads language the lookup was asked in. |
| data.google.status | string | "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.currency | string or null | The currency every bid is in. |
| data.google.main | object or null | The 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.ideas | array | Related searches in the same place, same shape as main, most searched first: up to 500. |
| data.bing | object or null | Bing 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.businesses | object or null | LeadQuasar'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.notCovered | boolean | True when LeadQuasar does not cover the niche or place yet. |
| data.bestCities | array | The cities in the country with the most of these businesses: city, state (null where Google names none) and businesses, up to 50. |
| data.bestCitiesCapped | boolean | True when the city counts are floors ("N+"). |
| data.leadListUrl | string or null | A 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.trends | object | exploreUrl (the chart on Google Trends) and embedUrl (Google's official embed of it), for the past 12 months. |
| data.generatedAt | string | When the report was built (ISO 8601), not when Google or Bing measured. |
| usage | object | usedToday and limitToday: this key's lookups today, and its daily limit. |
| meta | object | apiVersion ("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 "https://leadquasar.com/api/v1/keyword-research/locations?q=Springfield&country=US" \
-H "Authorization: Bearer $LEADQUASAR_API_KEY"{
"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.
{
"error": "rate_limited",
"message": "This key's 100 lookups for today are used. They reset at 00:00 UTC (in 5 h 12 min)."
}| Status | error | When | Charged |
|---|---|---|---|
| 400 | invalid_request | A 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 |
| 401 | unauthorized | No key, a malformed one or one that does not exist (the same answer for all three). Counted against your address. | No |
| 403 | key_paused | The key is paused. | No |
| 403 | key_revoked | The key was revoked. | No |
| 403 | key_expired | The key is past its expiry date. | No |
| 403 | insufficient_scope | The key has no keywordquasar scope (an account API key, for example). | No |
| 403 | account_suspended | The account the key belongs to is suspended. | No |
| 404 | location_not_found | Google knows no place by that name. | No |
| 409 | location_ambiguous | More than one place matches. data.choices lists up to 8; send the request again with the geoId and country of one. | No |
| 429 | rate_limited | With 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 |
| 500 | internal_error | Something failed on LeadQuasar's side. Try again. | No |
| 503 | upstream_unavailable | Google 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.
{
"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.
{
"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
| Lookups a day | Set 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 key | 60 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 day | Every 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 keys | After 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 charged | Every 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 lookups | Google and Bing answers are kept 30 days and shared, so a repeat is fast. It still counts as a lookup. |
| Request body | Up to 4,096 bytes of JSON. A larger body is answered 400 invalid_request. |
| Keyword | 2 to 80 characters |
| Location | Up to 120 characters |
| Keyword ideas per report | Up to 500 |
| Best cities per report | Up to 50 |
| Places offered when a name is ambiguous | Up to 8 |
| Where to call from | Your 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 -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"}'// 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);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.
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.