SERP & rankings

Rank Checks

Where a domain ranks for a keyword, read from a live Google results page: the position, the page that ranks and the domains above it. Every check is stored, so the history builds up from your own checks.

Endpoints#

  • POST/rankings/check
  • POST/rankings/batch
  • GET/rankings/history

Credits: 3 per 10 results of depth, because each 10 results is one results page read. The default depth of 100 costs 30 credits; depth 10 costs 3. A batch costs that for each distinct keyword. History is free.

Check Parameters#

FieldTypeRequiredDescription
keywordstringYes1–400 chars
domainstringYes3–253 chars. Reduced to its registrable domain (blog.example.com becomes example.com): any subdomain counts as a match, and url says which page ranked
depthnumberNoHow many results to search (10–100, default: 100), read in pages of 10. depth 25 reads 3 pages and costs 9
glstringNoCountry, 2 letters (default: us)
hlstringNoLanguage, 2–5 chars (default: en)
devicestringNodesktop or mobile (default: desktop). Positions are read from Google's desktop results page for both today; device is recorded with the check as asked
locationstringNoA country, by name, two-letter code or uule (max 300 chars). Applied as gl and echoed in the result as location. A city or state returns 400 CITY_LOCATION_UNAVAILABLE before any credit is taken, for a batch too (see Location)
cachebooleanNoReuse a matching cached results page (default: true)

Check Response#

JSON
{  "schema": "ranking-check.v1",  "result": {    "keyword": "project management software",    "domain": "example.com",    "position": 4,    "url": "https://www.example.com/features",    "title": "Project management features | Example",    "found": true,    "search_depth": 30,    "organic_count": 30,    "pages_fetched": 3,    "gl": "US",    "hl": "en",    "device": "desktop",    "serp_features": ["organic", "ads", "people_also_ask", "related_searches", "sitelinks"],    "competitors_above": ["asana.com", "monday.com", "clickup.com"],    "checked_at": "2026-09-29T10:00:00.000Z",    "cached": false  },  "credits": { "used": 9 }}
  • found: false with position: null means the domain was not in the top search_depth results. search_depth is what was asked for, so a short results page does not change what found: false means.
  • competitors_above lists up to 20 domains ranking above yours, top first. When the domain was not found, it lists the domains on the page.
  • serp_features uses the SERP features vocabulary. A rank check skips the AI Overview, so ai_overview never appears here.
  • A failed results-page read returns 429 (AT_CAPACITY) or 503 (UPSTREAM_UNAVAILABLE), as on /serp, and is refunded. An unparseable domain is 400 INVALID_DOMAIN.

Batch Endpoint#

POSThttps://api.searlo.tech/api/v1/rankings/batch

One domain, up to 20 keywords: keywords (1–20, each 1–400 chars) and domain, plus the same depth, gl, hl, device, location and cache fields as a check. Each keyword is a separate check and a separate history entry. meta.max_sync_batch reports the cap in force.

JSON
{  "schema": "ranking-batch.v1",  "results": [    { "success": true, "keyword": "project management software", "domain": "example.com", "position": 4, "url": "https://www.example.com/features", "title": "Project management features | Example", "found": true, "search_depth": 30, "organic_count": 30, "pages_fetched": 3, "gl": "US", "hl": "en", "device": "desktop", "serp_features": ["organic", "ads"], "competitors_above": ["asana.com"], "checked_at": "2026-09-29T10:00:00.000Z", "cached": false },    { "keyword": "kanban board software", "success": false, "code": "UPSTREAM_UNAVAILABLE", "retryable": true, "message": "Google SERP is temporarily unavailable. Please try again shortly." }  ],  "credits": { "used": 18 },  "meta": { "domain": "example.com", "requested": 2, "distinct": 2, "measured": 1, "ranking": 1, "failed": 1, "max_sync_batch": 20 }}

A keyword that fails inside a 200 batch is still billed.

History Endpoint#

GEThttps://api.searlo.tech/api/v1/rankings/history

Credits: Free (0).

ParameterTypeRequiredDescription
domainstringYesThe domain you checked
keywordstringNoOne keyword. Set it to get a single series and change
glstringNo2-letter country
devicestringNodesktop or mobile
fromstringNoISO date: earliest checked_at
tostringNoISO date: latest checked_at
limitnumberNo1–1000 (default: 100), newest first
JSON
{  "schema": "ranking-history.v1",  "series": [    { "keyword": "project management software", "domain": "example.com", "gl": "US", "hl": "en", "device": "desktop", "position": 4, "url": "https://www.example.com/features", "search_depth": 30, "serp_features": ["organic", "ads"], "checked_at": "2026-09-29T10:00:00.000Z" },    { "keyword": "project management software", "domain": "example.com", "gl": "US", "hl": "en", "device": "desktop", "position": 6, "url": "https://www.example.com/features", "search_depth": 30, "serp_features": ["organic", "ads"], "checked_at": "2026-09-28T10:00:00.000Z" }  ],  "change": { "delta": 2, "from": 6, "to": 4 },  "credits": { "used": 0 },  "meta": { "domain": "example.com", "returned": 2, "limit": 100 }}

change appears when keyword is set and the series has at least two checks. A positive delta means the domain moved up. When one of the two checks found nothing, delta is null and event is entered or dropped_out. History holds only the checks you ran (batch rows and async rankings/check tasks included). It is not a back-filled index, and it is kept for a configurable retention period.

Examples

# Top 30 results, 9 creditscurl -X POST "https://api.searlo.tech/api/v1/rankings/check" \  -H "x-api-key: sk_your_api_key" \  -H "Content-Type: application/json" \  -d '{ "keyword": "project management software", "domain": "example.com", "depth": 30, "gl": "us" }' # The stored series for that keyword (free)curl "https://api.searlo.tech/api/v1/rankings/history?domain=example.com&keyword=project+management+software" \  -H "x-api-key: sk_your_api_key"
const API = "https://api.searlo.tech/api/v1";const headers = { "x-api-key": process.env.SEARLO_API_KEY, "Content-Type": "application/json" }; const res = await fetch(`${API}/rankings/batch`, {  method: "POST",  headers,  body: JSON.stringify({    domain: "example.com",    keywords: ["project management software", "kanban board software", "gantt chart tool"],    depth: 20, // 2 pages per keyword = 6 credits per keyword    gl: "us",  }),});const { results, meta, credits } = await res.json(); for (const r of results) {  if (!r.success) console.log(r.keyword, "not measured:", r.message);  else console.log(r.keyword, r.found ? `#${r.position} ${r.url}` : `not in top ${r.search_depth}`);}console.log(`${meta.ranking}/${meta.distinct} ranking, ${credits.used} credits`);
import osimport requests API = "https://api.searlo.tech/api/v1"HEADERS = {"x-api-key": os.environ["SEARLO_API_KEY"]} check = requests.post(    f"{API}/rankings/check",    json={"keyword": "project management software", "domain": "example.com", "depth": 10},    headers=HEADERS,    timeout=120,).json()["result"]print(check["found"], check["position"], check["url"], check["competitors_above"][:3]) history = requests.get(    f"{API}/rankings/history",    params={"domain": "example.com", "keyword": "project management software", "limit": 30},    headers=HEADERS,).json()print(history["change"])for row in history["series"]:    print(row["checked_at"], row["position"])

Stuck? Write to [email protected], or check live status.