Compare URLs¶
POST 2–5 URLs and get a side-by-side row per page: SEO score, meta title/description and their lengths, H1 count, image stats, canonical/viewport/OG presence, and a body word count for content depth.
Best for "is the new page actually better than the old one?" comparisons during a redesign, A/B-style content rewrites, or competitor benchmarks. For a single page, use /v1/seo/audit. For a flat batch of up to 10 URLs without word counts, use /v1/seo/bulk-audit.
Endpoint¶
POST /v1/seo/compare
Base URL: https://seo.toolkitapi.io
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
urls |
string[] | Yes | 2–5 absolute URLs (http:// or https://) to compare. |
{
"urls": [
"https://example.com/v1",
"https://example.com/v2"
]
}
Submitting fewer than 2 or more than 5 URLs returns a
422 Unprocessable Entity.
Response Fields¶
Top-level:
| Field | Type | Description |
|---|---|---|
total |
integer | Number of URLs submitted. |
best_score |
integer | Highest score across successful rows (0 if all failed). |
worst_score |
integer | Lowest score across successful rows (0 if all failed). |
results |
object[] | One row per URL, in input order. |
Each results[i]:
| Field | Type | Description |
|---|---|---|
url |
string | Final URL after redirects (or input URL if the fetch failed). |
score |
integer (0–100) | Overall SEO score (0 on failure). |
meta_title |
string | null | <title> value, or null if missing. |
meta_title_length |
integer | Character length of meta_title (0 if missing). |
meta_description |
string | null | <meta name="description"> content. |
meta_description_length |
integer | Character length of meta_description. |
h1_count |
integer | Number of <h1> tags found. |
image_count |
integer | Total <img> tags. |
images_without_alt |
integer | Images missing or with empty alt. |
has_canonical |
boolean | <link rel="canonical"> present. |
has_viewport |
boolean | <meta name="viewport"> present. |
has_og_tags |
boolean | At least an og:title was found. |
word_count |
integer | Approximate body text word count (visible text, scripts/styles stripped). |
error |
string | null | null on success; otherwise a short failure message. |
A failed row keeps its place in results with error populated and zeros/nulls elsewhere — the HTTP status of the request itself stays 200.
Examples¶
curl¶
curl -X POST "https://seo.toolkitapi.io/v1/seo/compare" \
-H "x-api-key: $TOOLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"https://example.com/v1",
"https://example.com/v2"
]
}'
Python¶
import requests
resp = requests.post(
"https://seo.toolkitapi.io/v1/seo/compare",
json={"urls": ["https://example.com/v1", "https://example.com/v2"]},
headers={"x-api-key": API_KEY},
timeout=60,
)
data = resp.json()
print(f"best={data['best_score']} worst={data['worst_score']}")
for row in data["results"]:
if row["error"]:
print(f" {row['url']}: ERROR {row['error']}")
continue
print(
f" {row['url']}: score={row['score']}, "
f"title_len={row['meta_title_length']}, "
f"desc_len={row['meta_description_length']}, "
f"h1={row['h1_count']}, "
f"alt_missing={row['images_without_alt']}, "
f"words={row['word_count']}"
)
JavaScript¶
const resp = await fetch("https://seo.toolkitapi.io/v1/seo/compare", {
method: "POST",
headers: {
"x-api-key": process.env.TOOLKIT_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
urls: ["https://example.com/v1", "https://example.com/v2"],
}),
});
const data = await resp.json();
const winner = data.results
.filter((r) => !r.error)
.reduce((a, b) => (b.score > a.score ? b : a));
console.log(`Winner: ${winner.url} (score ${winner.score})`);
Example Response¶
{
"total": 2,
"best_score": 88,
"worst_score": 71,
"results": [
{
"url": "https://example.com/v1",
"score": 71,
"meta_title": "Example v1",
"meta_title_length": 10,
"meta_description": "An older landing page.",
"meta_description_length": 22,
"h1_count": 1,
"image_count": 4,
"images_without_alt": 1,
"has_canonical": false,
"has_viewport": true,
"has_og_tags": false,
"word_count": 312,
"error": null
},
{
"url": "https://example.com/v2",
"score": 88,
"meta_title": "Example v2 — Faster, friendlier onboarding for teams",
"meta_title_length": 52,
"meta_description": "Spin up shared workspaces in under a minute. ...",
"meta_description_length": 148,
"h1_count": 1,
"image_count": 6,
"images_without_alt": 0,
"has_canonical": true,
"has_viewport": true,
"has_og_tags": true,
"word_count": 587,
"error": null
}
]
}
Notes¶
- URLs are fetched sequentially; expect ~5–15 s for typical pages. Set client timeouts to ≥ 60 s for the max 5 URLs.
word_countis computed from the body text (after stripping<script>/<style>and collapsing whitespace) — it's a rough comparator, not a precise content metric.best_score/worst_scoreonly consider rows whereerrorisnull. If every URL fails they are both0.- Each URL passes through the same SSRF guard as the rest of the SEO toolkit; private/loopback targets are rejected per row.