API Platform
Technical SEO API
Run a comprehensive technical SEO audit on any URL.
Run a comprehensive technical SEO audit on any URL. Checks 27 on-page factors across meta tags, headings, links, images, content, URL structure, and social tags — returns a scored report with actionable issues.
Product page: Technical SEO 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_passwordRequest Parameters
Send a application/json body with the following parameters:
Body
urlstringrequiredThe URL to audit. Must start with http:// or https:// (e.g., "https://example.com", "https://example.com/page").
render_jsbooleandefault: falseWhen true, uses a headless browser to render JavaScript before auditing. Use this for SPAs and JS-heavy pages. May increase cost.
proxy_countrystringdefault: nullISO 3166-1 alpha-2 country code for proxy selection (e.g., "US", "TR", "DE"). Use when the target site geo-blocks requests.
JavaScript Rendering
Enabling render_js uses a headless browser which increases processing time and cost. Only enable it for pages that require JavaScript to render their content (SPAs, React/Vue apps).
{
"url": "https://example.com",
"render_js": false,
"proxy_country": "US"
}Code Examples
Complete examples showing how to run a Technical SEO audit in different languages.
curl -X POST https://data.semust.com/v1/technical-seo \
-H "Content-Type: application/json" \
-H "SEMUST-API-USER: your_username" \
-H "SEMUST-API-PASSWORD: your_password" \
-d '{
"url": "https://example.com",
"render_js": false
}'import requests
import json
response = requests.post(
"https://data.semust.com/v1/technical-seo",
headers={
"SEMUST-API-USER": "your_username",
"SEMUST-API-PASSWORD": "your_password",
},
json={
"url": "https://example.com",
"render_js": False,
},
)
if response.status_code == 200:
data = response.json()
summary = data.get("summary", {})
print(f"Score: {summary.get('score', 0)}/100")
print(f"Issues: {summary.get('total_issues', 0)} total "
f"({summary.get('critical_issues', 0)} critical, "
f"{summary.get('warning_issues', 0)} warning, "
f"{summary.get('notice_issues', 0)} notice)")
# Print failed audits by category
for category, audits in data.get("audit_results", {}).items():
issues = {k: v for k, v in audits.items() if v and v.get("status") == "fail"}
if issues:
print(f"\n[{category.upper()}]")
for name, details in issues.items():
print(f" - {name}: {details.get('issue', '')}")
else:
print(f"Error [{response.status_code}]: {response.text}")const response = await fetch("https://data.semust.com/v1/technical-seo", {
method: "POST",
headers: {
"Content-Type": "application/json",
"SEMUST-API-USER": "your_username",
"SEMUST-API-PASSWORD": "your_password",
},
body: JSON.stringify({
url: "https://example.com",
render_js: false,
}),
});
if (response.ok) {
const data = await response.json();
const { summary } = data;
console.log(`Score: ${summary.score}/100`);
console.log(`Issues: ${summary.total_issues} total`);
// Print failed audits
for (const [category, audits] of Object.entries(data.audit_results)) {
for (const [name, result] of Object.entries(audits)) {
if (result?.status === "fail") {
console.log(`[${category}] ${name}: ${result.issue}`);
}
}
}
} else {
const text = await response.text();
console.error(`Error [${response.status}]: ${text}`);
}package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
payload := map[string]interface{}{
"url": "https://example.com",
"render_js": false,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST",
"https://data.semust.com/v1/technical-seo",
bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("SEMUST-API-USER", "your_username")
req.Header.Set("SEMUST-API-PASSWORD", "your_password")
resp, err := (&http.Client{}).Do(req)
if err != nil {
fmt.Printf("Request failed: %v\n", err)
return
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 200 {
var data map[string]interface{}
json.Unmarshal(respBody, &data)
if summary, ok := data["summary"].(map[string]interface{}); ok {
fmt.Printf("Score: %v/100\n", summary["score"])
fmt.Printf("Issues: %v total\n", summary["total_issues"])
}
} else {
fmt.Printf("Error %d: %s\n",
resp.StatusCode, string(respBody))
}
}<?php
$payload = json_encode([
"url" => "https://example.com",
"render_js" => false,
]);
$ch = curl_init("https://data.semust.com/v1/technical-seo");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"SEMUST-API-USER: your_username",
"SEMUST-API-PASSWORD: your_password",
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
$summary = $data["summary"] ?? [];
echo "Score: " . ($summary["score"] ?? 0) . "/100\n";
echo "Issues: " . ($summary["total_issues"] ?? 0) . " total\n";
} else {
echo "Error [{$httpCode}]: {$response}\n";
}Response
Returns a JSON object with categorized audit results and a summary score.
Root Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | Always true on successful response |
url | string | The audited URL |
cost | float | Credits deducted for this request (USD) |
audit_results | object | Categorized audit results grouped by category (meta, headings, links, images, content, url_analysis, social) |
summary | object | Aggregated statistics: total checks, issues by severity, and overall score |
Audit Result Structure
Each audit within a category follows the same structure:
| Field | Type | Description |
|---|---|---|
status | string | "pass", "fail", or "error" — the audit outcome |
severity | string | "critical", "warning", or "notice" — impact level |
issue | string | Issue code identifier (only present when status is "fail") |
data | object | Audit-specific data with details about the check (varies per audit) |
Summary Object Fields
| Field | Type | Description |
|---|---|---|
total_checks | integer | Total number of audits performed |
passed_checks | integer | Number of audits that passed |
total_issues | integer | Number of audits that failed |
critical_issues | integer | Number of critical severity failures |
warning_issues | integer | Number of warning severity failures |
notice_issues | integer | Number of notice severity failures |
score | integer | Overall score (0-100) — percentage of passed checks |
{
"success": true,
"url": "https://example.com",
"cost": 0.0012,
"audit_results": {
"meta": {
"title": {
"status": "fail",
"severity": "warning",
"issue": "long_title",
"data": { "title": "Very Long Page Title...", "length": 66 }
},
"meta_description": {
"status": "pass",
"severity": "warning",
"data": { "meta_description": "Page description...", "length": 93 }
},
"canonical_url": {
"status": "pass",
"severity": "warning",
"data": { "current_url": "https://example.com", "canonical_url": "https://example.com" }
},
"robots_noindex": { "status": "pass", "severity": "critical", "data": {} },
"viewport": {
"status": "pass",
"severity": "critical",
"data": { "content": "width=device-width, initial-scale=1" }
}
},
"headings": {
"missing_h1": {
"status": "pass",
"severity": "critical",
"data": { "h1_count": 1, "h1_text": "Welcome" }
},
"broken_heading_structure": {
"status": "fail",
"severity": "warning",
"issue": "broken_heading_structure",
"data": { "total_count": 12, "skipped_levels": 1 }
}
},
"images": {
"images_without_alt": { "status": "pass", "severity": "warning", "data": { "total_images": 10 } },
"missing_image_dimensions": {
"status": "fail",
"severity": "warning",
"issue": "missing_image_dimensions",
"data": { "total_images": 10, "count": 5 }
}
},
"content": {
"low_word_count": { "status": "pass", "severity": "warning", "data": { "word_count": 684 } },
"low_html_text_ratio": {
"status": "fail",
"severity": "warning",
"issue": "low_html_text_ratio",
"data": { "text_to_html_ratio": 8.08 }
}
}
},
"summary": {
"total_checks": 27,
"passed_checks": 23,
"total_issues": 4,
"critical_issues": 0,
"warning_issues": 4,
"notice_issues": 0,
"score": 85
}
}Audit Checks (27 Total)
Meta Tags (8)
| Audit | Severity | Description |
|---|---|---|
title | critical | Checks for missing, too short (≤10 chars), or too long (>60 chars) title tag |
meta_description | warning | Checks for missing, too short (<50 chars), or too long (>160 chars) meta description |
canonical_url | warning | Checks for missing, multiple, or mismatched canonical URL |
robots_noindex | critical | Detects noindex directive in robots or googlebot meta tags |
viewport | critical | Checks for missing or invalid viewport meta tag (must include width=device-width) |
missing_lang | warning | Checks that the HTML tag has a lang attribute |
noindex_canonical_conflict | critical | Detects conflict when page has both noindex and canonical URL |
hreflang | warning | Validates hreflang implementation: lang codes, x-default, self-referential tags |
Headings (4)
| Audit | Severity | Description |
|---|---|---|
missing_h1 | critical | Checks that the page has at least one H1 tag |
multiple_h1_tags | warning | Checks that the page has exactly one H1 tag (not multiple) |
duplicate_h1_tags | warning | Checks that H1 tags don't have identical text |
broken_heading_structure | warning | Validates heading hierarchy (H1→H2→H3) with no skipped levels or empty headings |
Links (5)
| Audit | Severity | Description |
|---|---|---|
empty_anchor_text | warning | Checks that anchor tags have text content (not empty) |
non_descriptive_anchors | notice | Checks for non-descriptive anchor text like "click here" or "read more" |
internal_links_nofollow | warning | Checks that internal links don't have nofollow attribute |
too_many_internal_links | warning | Warns if page has too many internal links (threshold varies) |
too_many_external_links | warning | Warns if page has too many external links (threshold: 100) |
Images (2)
| Audit | Severity | Description |
|---|---|---|
images_without_alt | warning | Checks that images have alt attributes for accessibility |
missing_image_dimensions | warning | Checks that images have width and height attributes (prevents CLS) |
Content (4)
| Audit | Severity | Description |
|---|---|---|
low_word_count | warning | Checks that main content has at least 200 words |
low_html_text_ratio | warning | Checks that text-to-HTML ratio is above 10% |
lorem_ipsum | notice | Detects placeholder "lorem ipsum" text in page content |
page_size | warning | Checks HTML size (<3MB) and DOM element count (<1500) |
URL Analysis (2)
| Audit | Severity | Description |
|---|---|---|
http_url | critical | Checks that the URL uses HTTPS, not HTTP |
long_url | notice | Warns if URL exceeds 75 characters |
Social Tags (2)
| Audit | Severity | Description |
|---|---|---|
open_graph | warning | Checks for required Open Graph tags: og:title, og:description, og:image, og:url, og:type |
twitter_card | notice | Checks for Twitter Card meta tags: card, title, description, image |
Error Codes
All errors return a JSON object with a human-readable message and an error code.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_REQUEST | Request body is malformed or missing required fields |
| 400 | URL_REQUIRED | The url field is required |
| 400 | INVALID_URL | Invalid URL format or scheme — must start with http:// or https:// |
| 401 | INVALID_API_KEY | Authentication failed — invalid username or password |
| 402 | INSUFFICIENT_CREDITS | Your account does not have enough credits |
| 500 | INTERNAL_ERROR | An internal server error occurred |
| 502 | AUDIT_FAILED | Failed to audit the target URL |
| 502 | TARGET_BLOCKED | Target website actively blocks automated access (403 Forbidden) |
| 502 | TARGET_UNREACHABLE | Target is unreachable — DNS failure, connection timeout, or network error |
| 502 | SCRAPE_FAILED | Scraping failed — general error fetching the page |
| 504 | TIMEOUT | The request timed out (max 2 minutes) |
{
"error": "Your account does not have enough credits",
"code": "INSUFFICIENT_CREDITS"
}Credits & Rate Limits
Credits
Each request consumes credits from your balance. Cost varies based on the render_js parameter (JS rendering costs more). Failed requests are automatically refunded.
Rate Limits
Requests are subject to per-minute and per-day limits based on your plan. Exceeding limits returns HTTP 429.
