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#
| Field | Type | Required | Description |
|---|---|---|---|
keyword | string | Yes | 1–400 chars |
domain | string | Yes | 3–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 |
depth | number | No | How many results to search (10–100, default: 100), read in pages of 10. depth 25 reads 3 pages and costs 9 |
gl | string | No | Country, 2 letters (default: us) |
hl | string | No | Language, 2–5 chars (default: en) |
device | string | No | desktop or mobile (default: desktop). Positions are read from Google's desktop results page for both today; device is recorded with the check as asked |
location | string | No | A 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) |
cache | boolean | No | Reuse a matching cached results page (default: true) |
Check Response#
{ "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: falsewithposition: nullmeans the domain was not in the topsearch_depthresults.search_depthis what was asked for, so a short results page does not change whatfound: falsemeans.competitors_abovelists up to 20 domains ranking above yours, top first. When the domain was not found, it lists the domains on the page.serp_featuresuses the SERP features vocabulary. A rank check skips the AI Overview, soai_overviewnever appears here.- A failed results-page read returns
429(AT_CAPACITY) or503(UPSTREAM_UNAVAILABLE), as on/serp, and is refunded. An unparseabledomainis400INVALID_DOMAIN.
Batch Endpoint#
https://api.searlo.tech/api/v1/rankings/batchOne 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.
{ "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#
https://api.searlo.tech/api/v1/rankings/historyCredits: Free (0).
| Parameter | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | The domain you checked |
keyword | string | No | One keyword. Set it to get a single series and change |
gl | string | No | 2-letter country |
device | string | No | desktop or mobile |
from | string | No | ISO date: earliest checked_at |
to | string | No | ISO date: latest checked_at |
limit | number | No | 1–1000 (default: 100), newest first |
{ "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.