semust logo

API Platform

Domain Research API

See which keywords any domain ranks for, who its organic competitors are, which pages earn its traffic, and a one-call summary of its search visibility.

See which keywords any domain ranks for, who its organic competitors are, which pages earn its traffic, and a one-call summary of its search visibility. One endpoint serves four reports, selected with the type parameter. Turkish data covers more than 61 million keywords.

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

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.

typestringdefault: overview

overview, keywords, competitors or top_pages.

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. Not accepted on overview.

pageintegerdefault: 1

Page number. page × limit can be at most 10,000. Not accepted on overview.

sortstring

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

directionstring

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

min_volumeinteger

keywords only. 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 only. informational, navigational, commercial or transactional.

containsstring

keywords only. 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 within 24 hours returns the stored response at no charge, so a timed-out request can be retried safely.

{
  "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.

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

The list reports return 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.

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
totalintegerRows that exist for the query (list reports)
countintegerRows in this response (list reports)
page, limit, sort, directionThe paging and order applied (list reports)
itemsarrayThe rows (list reports)
dataobjectThe summary (overview)
costnumberWhat this request cost, in USD
cachedbooleantrue when served from cache
request_idstringQuote 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

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 → market → values.

StatusCodeMeaningWhat to do
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
400TYPE_UNAVAILABLEA report that is announced but not open yetUse 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
400INVALID_DOMAINNot a domain (an IP, localhost, no TLD…)Send 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
409REQUEST_IN_PROGRESSThe same idempotency_key is still runningRetry when it finishes
413REQUEST_TOO_LARGEBody over 16 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)
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 request took too longRetry; 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), billed at the base fee.

{
  "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

The balance hold is the cost of limit rows; 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.

Last updated: 2026-11-05