semust logo

API Platform

Domain Research API

See which keywords any domain ranks for, who its organic competitors are, the keyword gap against a competitor, which pages and subdomains earn its traffic, its ranking history, and traffic estimates for up to 1000 domains at once.

See which keywords any domain ranks for, who its organic competitors are, the keywords a competitor ranks for and you do not, which pages and subdomains earn its traffic, how its visibility has moved month by month, and traffic estimates for up to 1000 domains in one call. One endpoint serves eight reports, selected with the type parameter. Turkish data covers more than 61 million keywords.

Requests longer than 60 seconds

This endpoint runs requests as tasks. A request that runs longer than 60 seconds returns 202 with a task_id; collect the result from GET /v1/tasks/{task_id}. Every response, including a normal 200, carries the task id in the X-Task-Id header. Use ?wait= to change how long the request waits, and an Idempotency-Key header to retry safely.

Authentication

All requests require header-based authentication. Include your API credentials in every request.

HeaderTypeDescription
SEMUST-API-USERstringYour API user's username
SEMUST-API-PASSWORDstringYour API user's password
Accept-LanguagestringOptional. Send tr to receive error messages in Turkish

Create API credentials from the API Access page.

SEMUST-API-USER: your_username
SEMUST-API-PASSWORD: your_password
Content-Type: application/json

Report Types

typeReturnsBilled rows
overviewOne summary: keyword count, estimated traffic and its value, how many keywords sit in the top 3 / 10 / 20 / 100, new and lost keywords1
keywordsThe keywords the domain ranks for, with volume, CPC, difficulty, intent, position, ranking URL and estimated trafficrows returned
competitorsDomains competing for the same keywords, ranked by how many they share. The domain itself is never listedrows returned
top_pagesThe domain's pages ranked by the organic traffic they earnrows returned
keyword_gapKeywords compared against a competitor: the ones it ranks for and you do not (default), the ones you share, or the ones only you rank forrows returned
subdomainsThe domain's subdomains ranked by the organic traffic they earnrows returned
traffic_estimateEstimated organic and paid traffic for a list of up to 1000 domains in one callone per domain sent
historyThe overview numbers month by month, up to 60 months backone per month returned, on the history price

overview is the default. It returns a single row, so a request without a type costs the minimum.

{
  "type": "keywords",
  "domain": "example.com.tr",
  "limit": 100
}

Request Parameters

Send an application/json body. A parameter that does not apply to the chosen type is rejected rather than ignored, and a misspelled one is named in the error with a suggestion, so a typo never silently returns something other than what you asked for. A JSON null is treated as "not sent".

Body

domainstringrequired

The domain to research. https://, www., a port, any path and the Search Console sc-domain: prefix are removed for you. Internationalised names (örnek.com.tr) are accepted. Every type except traffic_estimate, which takes domains.

typestringdefault: overview

overview, keywords, competitors, top_pages, keyword_gap, subdomains, traffic_estimate or history.

competitorstring

keyword_gap only, and required there. The domain to compare against, cleaned up the same way as domain. It cannot be the same domain.

modestringdefault: missing

keyword_gap only. missing: keywords the competitor ranks for and you do not — your content gap. shared: keywords you both rank for, with both positions. unique: keywords only you rank for.

domainsstring[]

traffic_estimate only, and required there. 1–1000 domains, cleaned up like domain. Duplicates are merged, and rows come back in the order you sent them. Each domain is billed as a row, including one we have no data for.

monthsintegerdefault: 12

history only. How many whole months back from the current one, 1–60. The current month is not included until it is complete.

countrystringdefault: TR

Two-letter country code of the search market. See Markets below.

languagestring

Language of the market. Defaults to the market's main language (tr for TR).

limitintegerdefault: 100

Rows to return, 1–1000. Paged reports only: keywords, competitors, top_pages, keyword_gap, subdomains.

pageintegerdefault: 1

Page number. page × limit can be at most 10,000. Paged reports only.

sortstring

keywords: traffic (default), volume, position, difficulty, cpc. competitors: common_keywords (default), avg_position. top_pages: traffic. keyword_gap: volume (default), difficulty, cpc. subdomains: traffic (default), keywords.

directionstring

desc or asc. Defaults to asc for position and avg_position (best first) and desc for everything else.

min_volumeinteger

keywords and keyword_gap. Keep keywords with at least this monthly search volume.

max_positioninteger

keywords only. Keep keywords where the domain ranks at this position or better, 1–100.

intentstring

keywords and keyword_gap. informational, navigational, commercial or transactional.

containsstring

keywords and keyword_gap. Keep keywords containing this text, 1–80 characters, case-insensitive (Turkish İ/ı handled correctly).

idempotency_keystring

Optional, up to 128 characters. Repeating a request with the same key returns the same task — its 202 while it runs, its stored result once it finishes — and it is charged once, so a timed-out request can be retried safely. The key is reserved until 72 hours after the task finishes. Same as the Idempotency-Key header; if you send both, this field wins.

Query

waitintegerdefault: 60

Seconds to wait for the result, 0–80. If the request is still running after that, the response is 202 with a task_id. 0 answers 202 at once.

Headers

Idempotency-Keystring

Optional. Works like idempotency_key above; if you send both, the body field wins.

{
  "type": "keywords",
  "domain": "example.com.tr",
  "limit": 50,
  "min_volume": 100,
  "max_position": 10,
  "intent": "commercial",
  "contains": "avukat",
  "sort": "volume"
}

Markets

GET /v1/domain-research/markets lists every supported country and its languages. It makes no data lookup and is free.

{
  "success": true,
  "default_country": "TR",
  "markets": [
    { "country": "TR", "name": "Turkiye", "languages": ["tr"] },
    { "country": "US", "name": "United States", "languages": ["en", "es"] }
  ]
}

Code Examples

Fetching a domain's top keywords.

A request still running after 60 seconds returns 202. The call() helper on Tasks & Long-Running Requests polls until the result is ready.

curl -X POST https://data.semust.com/v1/domain-research \
  -H "SEMUST-API-USER: your_username" \
  -H "SEMUST-API-PASSWORD: your_password" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "keywords",
    "domain": "example.com.tr",
    "limit": 100
  }'

Response

Every report except overview returns rows in items; overview returns a single data object instead. total is how many rows exist for your query, count how many are in this response. traffic_estimate and history are not paged: they return everything at once, with no page, limit or sort.

Position counts in overview are cumulative: top10 includes top3. Traffic is an estimate of monthly organic visits. A value we do not have is null, never 0 — "cpc": null means no CPC data, not a CPC of zero.

The examples below are real responses for semust.com in Turkey.

{
  "success": true,
  "type": "overview",
  "domain": "semust.com",
  "country": "TR",
  "language": "tr",
  "data": {
    "organic": {
      "keywords": 136,
      "traffic": 1233,
      "traffic_value_usd": 1541.15,
      "top3": 6,
      "top10": 36,
      "top20": 89,
      "top100": 136,
      "new": 86,
      "lost": 29,
      "improved": 25,
      "declined": 23
    },
    "paid": { "keywords": 0, "traffic": 0, "traffic_value_usd": 0 }
  },
  "cost": 0.013332,
  "cached": false,
  "request_id": "3f1c2a9e-5b7d-4c1e-9a2f-8d6e4b0c7a11"
}

Response Fields Reference

Envelope

FieldTypeDescription
successbooleantrue when the request succeeded
type, domain, country, languagestringWhat was actually queried, after normalisation and defaults. traffic_estimate has no domain: each row names its own
competitor, modestringThe comparison applied (keyword_gap)
monthsintegerThe months asked for (history)
totalintegerRows that exist for the query (list reports)
countintegerRows in this response (list reports)
page, limit, sort, directionThe paging and order applied (paged reports)
itemsarrayThe rows (list reports)
dataobjectThe summary (overview)
costnumberWhat this request cost, in USD
cachedbooleantrue when served from cache
request_idstringThe task id. GET /v1/tasks/{request_id} returns this response again for 72 hours. Quote it to support to trace the request

overview → data.organic

FieldTypeDescription
keywordsintegerKeywords the domain ranks for in the top 100
trafficintegerEstimated monthly organic visits
traffic_value_usdnumberWhat that traffic would cost as paid search, in USD
top3, top10, top20, top100integerKeywords ranking at or above that position (cumulative)
new, lostintegerKeywords that entered or left the rankings since the previous update
improved, declinedintegerKeywords whose position went up or down

data.paid has keywords, traffic and traffic_value_usd for paid search.

keywords row

FieldTypeDescription
keywordstringThe search query
volumeinteger | nullAverage monthly searches
cpcnumber | nullAverage cost per click, USD
difficultyinteger | nullRanking difficulty, 0–100
intentstring | nullinformational, navigational, commercial or transactional
positioninteger | nullThe domain's position
urlstring | nullThe page that ranks
trafficinteger | nullEstimated monthly visits from this keyword

competitors row

FieldTypeDescription
domainstringThe competing domain
common_keywordsintegerKeywords both domains rank for
avg_positionnumber | nullThe competitor's average position on those shared keywords
keywordsinteger | nullEverything the competitor ranks for
trafficinteger | nullThe competitor's total estimated monthly traffic

top_pages row

FieldTypeDescription
urlstringThe page
keywordsintegerKeywords the page ranks for
trafficintegerEstimated monthly visits to the page
top3, top10integerOf those keywords, how many rank in the top 3 / top 10

keyword_gap row

FieldTypeDescription
keywordstringThe search query
volume, cpc, difficulty, intentAs in the keywords row
position, urlinteger | null, string | nullYour domain's position and ranking page. Always null in mode: missing
competitor_position, competitor_urlinteger | null, string | nullThe competitor's. Always null in mode: unique

subdomains row

FieldTypeDescription
subdomainstringThe subdomain, e.g. blog.example.com (the bare domain appears as itself)
keywordsintegerKeywords it ranks for
trafficintegerEstimated monthly organic visits
top3, top10integerOf those keywords, how many rank in the top 3 / top 10

traffic_estimate row

FieldTypeDescription
domainstringThe domain, as you sent it after clean-up
keywords, trafficintegerOrganic keywords and estimated monthly organic visits
paid_keywords, paid_trafficintegerThe same for paid search

A domain we have no data for gets a row of zeros, in its place in the list.

history row

FieldTypeDescription
monthstringYYYY-MM. Rows are oldest first
organic, paidobjectThe same fields as overview's data.organic and data.paid, for that month. new, lost, improved and declined compare with the month before

Errors

Every error has the same shape: a stable code to branch on, a human-readable error, and retryable, which says whether sending the same request again can succeed. Where it helps, the error also names the field at fault, a hint on how to fix it, and the allowed values.

Errors in the 4xx range are found before anything is charged, so they cost nothing. A failure after that point is refunded in full. You are never charged for an error.

Checks run in a fixed order and the first problem is reported: JSON → unknown parameters → type → parameters the type does not take → domain / domains / competitor → market → values.

StatusCodeMeaningWhat to do
202—Not an error: still running after wait secondsPoll GET /v1/tasks/{task_id}; see Tasks
400INVALID_JSONThe body is not a JSON objectSend a JSON object
400UNKNOWN_PARAMETERA parameter we do not know; hint suggests the closest matchFix the spelling
400INVALID_TYPEUnknown type; allowed lists the valid onesUse a value from allowed
400PARAMETER_NOT_ALLOWEDThe parameter exists but not for this type; hint names the types that take itRemove it or change type
400DOMAIN_REQUIREDdomain is missingSend a domain
400COMPETITOR_REQUIREDkeyword_gap without a competitorSend the domain to compare against
400INVALID_DOMAINNot a domain (an IP, localhost, no TLD…). field says which: domain, competitor or domainsSend a registered domain
400UNSUPPORTED_MARKETCountry not supported, or language not available there; allowed lists that country's languagesSee Markets
400INVALID_PARAMETERA value is out of range or of the wrong type; field names itSee the parameter's rules
400PAGE_OUT_OF_RANGEpage × limit is over 10,000Narrow the query with filters
401INVALID_CREDENTIALSUsername or password is wrongCheck your credentials
402INSUFFICIENT_CREDITSYour balance does not cover the request; required and balance say by how muchLower limit or add funds
403SERVICE_NOT_ENABLEDDomain Research is not enabled for this API userEnable it on your account
403IP_NOT_WHITELISTEDYour IP is not on the allow-listAdd it on the API Access page
413REQUEST_TOO_LARGEBody over 128 KBSend a smaller body
429RATE_LIMIT_EXCEEDEDYour per-minute or per-day limit is reachedSlow down
429TOO_MANY_CONCURRENT_REQUESTSMore than 5 requests running at onceWait for one to finish (Retry-After)
429TOO_MANY_PENDING_TASKS100 tasks are still running for this API userRetry after Retry-After seconds
502REQUEST_FAILEDThe data could not be retrieved; retryable says whether trying again can helpRetry if retryable is true
503SERVICE_BUSYTemporarily overloadedRetry after Retry-After seconds
503SERVICE_UNAVAILABLETemporarily unavailableRetry after Retry-After seconds
504TIMEOUTThe task did not finish in time, e.g. it passed its 10-minute deadline. Not chargedRetry; it is safe
500INTERNAL_ERRORSomething went wrong on our sideRetry, or contact support with request_id

A domain with no data is not an error: the response is 200 with "total": 0 and an empty items list (or zeros in overview, or a row of zeros in traffic_estimate), billed like any other answer.

{
  "error": "Unknown parameter 'limt'",
  "code": "UNKNOWN_PARAMETER",
  "field": "limt",
  "hint": "Did you mean 'limit'?",
  "retryable": false
}

Credits & Limits

Credits

A base fee per request plus a fee for each row returned:

cost = $0.0132 + (rows / 1000) x $0.132
RequestCost
overview (always 1 row)$0.013332
No data (0 rows)$0.013200
10 rows$0.014520
100 rows$0.026400
1000 rows (maximum)$0.145200

In traffic_estimate every domain sent is a row, found or not: 3 domains cost $0.013596, 1000 cost $0.145200.

history reads a monthly archive that is priced higher, per month returned:

cost = $0.132 + months x $0.00132
history requestCost
3 months$0.135960
12 months (default)$0.147840
60 months (maximum)$0.211200

The balance hold is the cost of the worst case — limit rows, every domain sent, or every month asked for; anything not returned is refunded immediately. Requesting fewer rows costs less, so set limit to what you need.

Caching

Results are cached for 7 days — the underlying index refreshes monthly. A cached answer is marked "cached": true and is billed at the same price as a live one.

Limits

Per-minute and per-day limits are set on your account (429 RATE_LIMIT_EXCEEDED, see Rate Limits). Independently, at most 5 requests per API user run at the same time; a sixth gets 429 TOO_MANY_CONCURRENT_REQUESTS with a Retry-After header. Cached answers do not count toward it, and a request that has returned 202 keeps its place until its task finishes. At most 100 tasks per API user can be unfinished at once (429 TOO_MANY_PENDING_TASKS). Polling has its own limit and does not count against any of these; see Tasks.

Last updated: 2026-11-07