API Platform
Backlinks API
Retrieve backlink profiles, referring domains, anchor texts, competitors and historical link growth for any domain, subdomain or page.
Retrieve backlink profiles, referring domains, anchor texts, competitors and historical link growth for any domain, subdomain or page. One endpoint serves eight different reports, selected with the type parameter.
Product page: Backlinks API.
Authentication
All requests require header-based authentication. Include your API credentials in every request.
| Header | Type | Description |
|---|---|---|
SEMUST-API-USER | string | Your API user's username |
SEMUST-API-PASSWORD | string | Your API user's password |
Create API credentials from the API Access page.
SEMUST-API-USER: your_username
SEMUST-API-PASSWORD: your_passwordReport Types
The type parameter selects which report you get back. All eight share the same endpoint, authentication and pricing.
type | Returns |
|---|---|
summary | One metric block: totals, referring domains, spam scores and breakdowns |
history | Monthly series of those same metrics |
backlinks | The individual backlinks |
referring_domains | Domains linking to the target, aggregated |
anchors | Anchor texts, aggregated |
competitors | Domains with overlapping backlink profiles |
pages | The target's own pages, ranked by backlinks received |
timeseries | New and lost counts over time |
summary is the default. It returns a single row, so an accidental request without a type costs the minimum.
{
"target": "example.com",
"type": "backlinks",
"limit": 100
}Request Parameters
Send an application/json body. Parameters that do not apply to the chosen type are rejected rather than ignored, so a typo never silently returns something other than what you asked for.
Body
targetstringrequiredDomain, subdomain or page URL. Scheme and www. are removed automatically for domain lookups.
typestringdefault: summaryWhich report to return. See Report Types above.
scopestringdefault: domaindomain includes every subdomain, host restricts to the exact host, url restricts to one page. With url the target must include http:// or https://.
statusstringdefault: livelive returns links still in place, lost returns links that disappeared, all returns both. Not accepted on history, timeseries or competitors — those reports have no link-status dimension.
limitintegerdefault: 100Rows to return, 1–1000. Applies to the row-returning reports.
offsetintegerdefault: 0Rows to skip, 0–20000. Use cursor to page beyond that.
cursorstringOpaque pagination token from next_cursor. Only type: "backlinks" supports it. Cannot be combined with offset.
group_bystringdefault: nonenone returns every backlink, domain returns one per referring domain, anchor returns one per anchor text. Only for type: "backlinks".
include_subdomainsbooleandefault: trueInclude links pointing at subdomains of the target. Not accepted on history.
exclude_internalbooleandefault: trueExclude links coming from the target's own domain. Not accepted on history or timeseries.
include_redirectsbooleandefault: trueInclude links that reach the target through a redirect or canonical. Not accepted on history, timeseries or competitors.
filtersarrayFilter expression over the response fields. See Filters below.
order_byarrayUp to 3 sort rules, each "field,asc" or "field,desc".
date_fromstringStart date, YYYY-MM-DD. For history and timeseries. Defaults to 12 months ago; earlier than 2019-02-01 is clamped.
date_tostringEnd date, YYYY-MM-DD. Defaults to today.
granularitystringdefault: monthBucket size for timeseries: day, week, month or year.
include_breakdownsbooleandefault: falseInclude the links_by_* maps on every point of a history response. Off by default because it makes the payload considerably larger.
breakdown_limitintegerdefault: 25How many entries to keep in each links_by_* map, 1–1000. Kept by descending count.
idempotency_keystringOptional. 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.
Date range affects cost
history bills one row per month in the range. Leaving the dates out gives you the last 12 months rather than the full archive, which keeps an unspecified request cheap.
{
"target": "example.com",
"type": "backlinks",
"scope": "domain",
"status": "live",
"limit": 100,
"filters": [
["is_dofollow", "=", true],
"and",
["source_domain_rate", ">=", 40]
],
"order_by": ["source_domain_rate,desc"]
}Filters
filters takes a condition, or an array of conditions joined by "and" / "or". A condition is [field, operator, value]. Nest arrays to combine both operators — a single group cannot mix them, which keeps precedence unambiguous.
Operators available depend on the field's type:
| Type | Operators |
|---|---|
| boolean | =, <> |
| integer | <, <=, >, >=, =, <>, in, not_in |
| string | =, <>, in, not_in, like, not_like, ilike, not_ilike, match, not_match, regex, not_regex |
| enum | =, <>, in, not_in |
| datetime | <, <=, >, >=, = |
like patterns use % as the wildcard. Datetime values are RFC3339.
Limits: 8 conditions, 3 sort rules, 3 levels of nesting, 1000 characters for a regex, 255 for any other string.
GET /v1/backlinks/filters returns the filterable and sortable fields for every report type, with the operators each accepts — add ?type=backlinks for one report.
{
"target": "example.com",
"type": "backlinks",
"filters": ["source_domain", "like", "%.edu"]
}{
"target": "example.com",
"type": "backlinks",
"filters": [
["is_dofollow", "=", true],
"and",
["source_spam_score", "<", 20]
]
}{
"target": "example.com",
"type": "backlinks",
"filters": [
["source_domain_rate", ">=", 50],
"and",
[
["link_position", "=", "content"],
"or",
["anchor_text", "ilike", "%seo%"]
]
]
}{
"target": "example.com",
"type": "backlinks",
"filters": ["link_type", "in", ["text", "image"]]
}Code Examples
Complete examples fetching a backlink list in different languages.
curl -X POST https://data.semust.com/v1/backlinks \
-H "SEMUST-API-USER: your_username" \
-H "SEMUST-API-PASSWORD: your_password" \
-H "Content-Type: application/json" \
-d '{
"target": "example.com",
"type": "backlinks",
"limit": 100,
"filters": [["is_dofollow", "=", true]],
"order_by": ["source_domain_rate,desc"]
}'import requests
response = requests.post(
"https://data.semust.com/v1/backlinks",
headers={
"SEMUST-API-USER": "your_username",
"SEMUST-API-PASSWORD": "your_password",
},
json={
"target": "example.com",
"type": "backlinks",
"limit": 100,
"filters": [["is_dofollow", "=", True]],
"order_by": ["source_domain_rate,desc"],
},
timeout=120,
)
if response.status_code != 200:
raise SystemExit(f"Request failed: {response.status_code} {response.text}")
data = response.json()
print(f"{data['count']} of ~{data['total']} backlinks, cost ${data['cost']:.6f}")
for link in data["results"]:
print(link["source_domain"], "->", link["anchor_text"], link["source_domain_rate"])const response = await fetch("https://data.semust.com/v1/backlinks", {
method: "POST",
headers: {
"SEMUST-API-USER": "your_username",
"SEMUST-API-PASSWORD": "your_password",
"Content-Type": "application/json",
},
body: JSON.stringify({
target: "example.com",
type: "backlinks",
limit: 100,
filters: [["is_dofollow", "=", true]],
order_by: ["source_domain_rate,desc"],
}),
});
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const data = await response.json();
console.log(`${data.count} of ~${data.total} backlinks, cost $${data.cost}`);
for (const link of data.results) {
console.log(link.source_domain, "->", link.anchor_text, link.source_domain_rate);
}package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]any{
"target": "example.com",
"type": "backlinks",
"limit": 100,
"filters": []any{[]any{"is_dofollow", "=", true}},
"order_by": []string{"source_domain_rate,desc"},
})
req, _ := http.NewRequest("POST", "https://data.semust.com/v1/backlinks", bytes.NewReader(body))
req.Header.Set("SEMUST-API-USER", "your_username")
req.Header.Set("SEMUST-API-PASSWORD", "your_password")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
panic(fmt.Sprintf("request failed: %d", resp.StatusCode))
}
var data struct {
Count int `json:"count"`
Total int64 `json:"total"`
Cost float64 `json:"cost"`
Results []struct {
SourceDomain string `json:"source_domain"`
AnchorText string `json:"anchor_text"`
SourceDomainRate float64 `json:"source_domain_rate"`
} `json:"results"`
}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Printf("%d of ~%d backlinks, cost $%.6f\n", data.Count, data.Total, data.Cost)
for _, l := range data.Results {
fmt.Println(l.SourceDomain, "->", l.AnchorText, l.SourceDomainRate)
}
}<?php
$payload = json_encode([
"target" => "example.com",
"type" => "backlinks",
"limit" => 100,
"filters" => [["is_dofollow", "=", true]],
"order_by" => ["source_domain_rate,desc"],
]);
$ch = curl_init("https://data.semust.com/v1/backlinks");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
"SEMUST-API-USER: your_username",
"SEMUST-API-PASSWORD: your_password",
"Content-Type: application/json",
],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
exit("Request failed: $status\n");
}
$data = json_decode($response, true);
printf("%d of ~%d backlinks, cost $%.6f\n", $data["count"], $data["total"], $data["cost"]);
foreach ($data["results"] as $link) {
echo $link["source_domain"], " -> ", $link["anchor_text"], "\n";
}Pagination
Row-returning reports accept offset up to 20,000. Beyond that, use the next_cursor value from the previous response as cursor on the next request. Cursors are tied to your account and to the exact query that produced them, so filters cannot change mid-scroll, and they expire after 30 minutes.
next_cursor is null when there are no further rows. Only type: "backlinks" returns one.
{
"target": "example.com",
"type": "backlinks",
"limit": 1000
}{
"target": "example.com",
"type": "backlinks",
"limit": 1000,
"cursor": "bl_4f2c8a91d0e34b7c9a15e8f60b2d7c43"
}Response
Every report returns the same envelope. results holds the rows; summary returns a single result object instead.
total is an approximate count of everything matching your query, not the number of rows returned — use count for that.
{
"success": true,
"cached": false,
"cost": 0.030360,
"type": "backlinks",
"target": "example.com",
"scope": "domain",
"total": 337785,
"count": 100,
"limit": 100,
"next_cursor": "bl_4f2c8a91d0e34b7c9a15e8f60b2d7c43",
"results": [
{
"source_url": "https://blog.example.de/seo-tools",
"source_domain": "blog.example.de",
"target_url": "https://example.com/",
"target_domain": "example.com",
"source_tld": "de",
"anchor_text": "example seo tools",
"anchor_type": "branded",
"image_url": null,
"image_alt": null,
"context_before": "we recommend",
"context_after": "for link research",
"link_type": "text",
"link_position": "content",
"is_dofollow": true,
"is_ugc": false,
"is_sponsored": false,
"rel_attributes": [],
"via_redirect": false,
"identical_links_count": 1,
"is_new": false,
"is_live": true,
"is_broken": false,
"first_seen": "2024-02-12T03:09:59Z",
"last_seen": "2026-08-16T18:51:04Z",
"link_rate": 70,
"source_page_rate": 81,
"source_domain_rate": 65,
"source_spam_score": 0,
"target_spam_score": 0,
"source_type": "blog",
"source_country": "DE",
"source_language": "de",
"source_page_title": "The best SEO tools in 2026",
"source_page_status_code": 200,
"source_page_size_bytes": 28259,
"source_page_internal_links": 31,
"source_page_external_links": 4,
"target_status_code": 200,
"target_redirect_url": null
}
]
}{
"success": true,
"cached": false,
"cost": 0.026440,
"type": "summary",
"target": "example.com",
"scope": "domain",
"total": 1,
"count": 1,
"next_cursor": null,
"result": {
"target": "example.com",
"first_seen": "2019-04-11T08:22:13Z",
"lost_at": null,
"is_live": true,
"semust_rate": 78,
"total_backlinks": 337785,
"dofollow_backlinks": 291204,
"nofollow_backlinks": 46581,
"broken_backlinks": 812,
"broken_pages": 96,
"avg_spam_score": 6,
"referring_domains": 12840,
"referring_domains_dofollow": 9903,
"referring_domains_nofollow": 2937,
"referring_root_domains": 11002,
"referring_root_domains_nofollow": 2410,
"referring_ips": 9781,
"referring_subnets": 6204,
"referring_pages": 210433,
"referring_pages_nofollow": 40122,
"text_links_count": 298110,
"image_links_count": 31245,
"redirect_links_count": 8430,
"internal_links_count": 54221,
"outgoing_links_count": 9877,
"links_by_tld": { "com": 201334, "de": 30122, "org": 18400 },
"links_by_type": { "text": 298110, "image": 31245, "redirect": 8430 },
"links_by_attribute": { "nofollow": 46581, "noopener": 12034 },
"links_by_position": { "content": 240188, "footer": 51203, "sidebar": 30122 },
"links_by_country": { "US": 140233, "DE": 30122, "unknown": 4021 },
"links_by_source_type": { "blog": 90233, "news": 41022, "other": 120544 },
"target_ip": "93.184.216.34",
"target_country": "US",
"target_cms": null,
"target_server": "nginx",
"target_spam_score": 3,
"organic_traffic_estimate": null,
"organic_keywords_count": null
}
}{
"success": true,
"cached": false,
"cost": 0.026876,
"type": "history",
"target": "example.com",
"scope": "domain",
"date_from": "2025-09-01",
"date_to": "2026-08-29",
"total": 12,
"count": 12,
"next_cursor": null,
"results": [
{
"date": "2026-08-01",
"semust_rate": 78,
"total_backlinks": 337785,
"dofollow_backlinks": 291204,
"referring_domains": 12840,
"new_backlinks": 4210,
"lost_backlinks": 3894,
"new_referring_domains": 312,
"lost_referring_domains": 287
}
]
}Response Fields Reference
Envelope
| Field | Type | Description |
|---|---|---|
success | boolean | true when the request succeeded |
cached | boolean | true when served from cache, at the reduced rate |
cost | number | What this request cost, in USD |
type | string | The report returned |
target | string | The normalized target |
scope | string | The scope applied |
total | integer | Approximate number of matching rows |
count | integer | Rows in this response |
limit | integer | The limit applied |
offset | integer | The offset applied |
next_cursor | string | null | Pass as cursor for the next page |
results | array | The rows, for every report except summary |
result | object | The single metric block, for summary |
Backlink
| Field | Type | Description |
|---|---|---|
source_url | string | Page containing the link |
source_domain | string | Host of the linking page, without www. |
target_url | string | URL that was linked to |
target_domain | string | Host that was linked to |
source_tld | string | TLD of the linking domain |
anchor_text | string | Visible clickable text |
anchor_type | string | exact, branded, naked_url, generic, partial, image, empty or other |
image_url | string | null | Source of the linked image, for image links |
image_alt | string | null | alt text of the linked image |
context_before | string | null | Text immediately before the anchor |
context_after | string | null | Text immediately after the anchor |
link_type | string | text, image, redirect, canonical, alternate, meta, feed, form or other |
link_position | string | content, nav, header, footer or sidebar |
is_dofollow | boolean | Whether the link passes authority |
is_ugc | boolean | Marked as user-generated |
is_sponsored | boolean | Marked as paid |
rel_attributes | array | rel values present on the anchor |
via_redirect | boolean | Reached through a redirect or canonical |
identical_links_count | integer | Copies of this same link on the source page |
is_new | boolean | Appeared in the most recent crawl window |
is_live | boolean | Link is still in place |
is_broken | boolean | Points at a page returning 4xx or 5xx |
first_seen | string | null | RFC3339 timestamp of first discovery |
last_seen | string | null | RFC3339 timestamp of most recent sighting |
link_rate | number | Authority passed by this link, 0–100 |
source_page_rate | number | Authority of the linking page, 0–100 |
source_domain_rate | number | Authority of the linking domain, 0–100 |
source_spam_score | integer | Spam likelihood of the source, 0–100 |
target_spam_score | integer | null | Spam likelihood of the linked page |
source_type | string | blog, forum, news, business, wiki, ecommerce or other |
source_country | string | null | ISO 3166-1 alpha-2 country of the linking server |
source_language | string | null | Detected language of the linking page |
source_page_title | string | null | Title of the linking page |
source_page_status_code | integer | null | HTTP status of the linking page |
source_page_size_bytes | integer | null | Byte size of the linking page |
source_page_internal_links | integer | null | Internal links on the linking page |
source_page_external_links | integer | null | External links on the linking page |
target_status_code | integer | null | HTTP status of the linked page |
target_redirect_url | string | null | Where the linked page redirects |
A null here means unknown, not zero — a page the crawler has not fetched has no status code, and treating that as 0 would be misleading.
Summary and history metrics
| Field | Type | Description |
|---|---|---|
semust_rate | number | Authority of the target, 0–100 |
total_backlinks | integer | All backlinks |
dofollow_backlinks | integer | Backlinks passing authority |
nofollow_backlinks | integer | Backlinks marked nofollow |
broken_backlinks | integer | Backlinks pointing at broken pages |
broken_pages | integer | Target pages returning 4xx or 5xx |
avg_spam_score | integer | Average spam score across backlinks |
referring_domains | integer | Unique domains linking to the target |
referring_domains_dofollow | integer | Domains linking exclusively with dofollow links |
referring_domains_nofollow | integer | Domains with at least one nofollow link |
referring_root_domains | integer | Unique root domains |
referring_ips | integer | Unique IP addresses |
referring_subnets | integer | Unique subnets |
referring_pages | integer | Unique pages linking to the target |
text_links_count | integer | Text links |
image_links_count | integer | Image links |
redirect_links_count | integer | Redirect links |
internal_links_count | integer | Internal links across the target |
outgoing_links_count | integer | Outgoing links from the target |
links_by_tld | object | Link count by TLD |
links_by_type | object | Link count by link type |
links_by_attribute | object | Link count by rel attribute |
links_by_position | object | Link count by position on the page |
links_by_country | object | Link count by country |
links_by_source_type | object | Link count by kind of site |
referring_domains_dofollow
referring_domains_nofollow counts every domain with at least one nofollow link, so referring_domains_dofollow is the number of domains linking only with dofollow links — not the number with at least one.
history adds date, new_backlinks, lost_backlinks, new_referring_domains and lost_referring_domains to each point. The links_by_* maps are empty unless include_breakdowns is true.
Error Codes
| Status | Code | Description |
|---|---|---|
| 400 | INVALID_REQUEST | Body is malformed, or a parameter does not apply to this type |
| 400 | TARGET_REQUIRED | The target field is missing |
| 400 | INVALID_TARGET | target is not a valid domain or URL |
| 400 | INVALID_TYPE | Unknown type |
| 400 | INVALID_FILTER | Unknown field, wrong operator for the field, or too many conditions |
| 400 | INVALID_ORDER_BY | Unknown sort field, or more than 3 rules |
| 400 | INVALID_DATE_RANGE | date_from is after date_to, or the dates are unparseable |
| 400 | INVALID_CURSOR | The cursor is unknown, expired, or not valid for this request |
| 401 | INVALID_CREDENTIALS | Username or password is wrong |
| 402 | INSUFFICIENT_CREDITS | Not enough balance for this request |
| 403 | SERVICE_NOT_ENABLED | Backlinks is not enabled for your account |
| 403 | IP_NOT_WHITELISTED | Your IP address is not whitelisted |
| 409 | REQUEST_IN_PROGRESS | The same idempotency_key is already running |
| 413 | REQUEST_TOO_LARGE | Request body exceeds 64 KB |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests |
| 502 | REQUEST_FAILED | The request could not be completed |
| 504 | TIMEOUT | The request timed out |
Every 4xx above is checked before anything is charged, so a rejected request costs nothing.
Failures carry a request_id. Quote it when contacting support and we can find the exact cause.
{
"error": "invalid filter: unknown field \"domain_rating\"",
"code": "INVALID_FILTER"
}Credits & Rate Limits
Credits
Pricing is a base fee per request plus a fee for each row returned:
cost = $0.0264 + (rows / 1000) x $0.0396| Rows returned | Cost |
|---|---|
| 0 | $0.026400 |
1 (summary) | $0.026440 |
| 100 | $0.030360 |
| 500 | $0.046200 |
| 1000 (maximum) | $0.066000 |
A request that matches nothing still costs the base fee. Requesting fewer rows costs less, so set limit to what you actually need.
history and timeseries bill one row per period in the range.
Caching
Identical requests are cached and served at a flat $0.005, with "cached": true in the response. Most reports cache for 24 hours; history caches for 7 days because the underlying data only changes monthly.
Rate Limits
Rate limits are per API user and are set on your account. Exceeding them returns 429 RATE_LIMIT_EXCEEDED. See Rate Limits for details.
