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_count is 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_score only consider rows where error is null. If every URL fails they are both 0.
  • Each URL passes through the same SSRF guard as the rest of the SEO toolkit; private/loopback targets are rejected per row.