Bulk SEO Audit¶
Submit up to 10 URLs in a single POST and get back a flat per-URL summary — score, meta presence, viewport, H1, image counts, and a per-row error field for pages that failed to fetch.
This is the batch counterpart to /v1/seo/audit. The same scoring engine runs for every URL, but the response is intentionally trimmed (no full heading hierarchy, no OG/Twitter detail) so it fits in a dashboard table or a CI summary. For a deeper look at any one row, re-run that URL through /v1/seo/audit. For side-by-side metric comparison, see /v1/seo/compare.
Endpoint¶
POST /v1/seo/bulk-audit
Base URL: https://seo.toolkitapi.io
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
urls |
string[] | Yes | 1–10 absolute URLs (http:// or https://) to audit. |
{
"urls": [
"https://example.com",
"https://example.com/pricing",
"https://example.com/blog"
]
}
The endpoint enforces
min_length=1andmax_length=10. Submitting more than 10 URLs returns a422 Unprocessable Entity.
Response Fields¶
| Field | Type | Description |
|---|---|---|
total |
integer | Number of URLs submitted (always equals len(urls)). |
results |
object[] | One entry per submitted URL, in input order. |
Each results[i] object:
| Field | Type | Description |
|---|---|---|
url |
string | Final URL after redirects (or the input URL if the fetch failed). |
score |
integer (0–100) | Overall SEO score from the shared audit engine. 0 if the page failed to load. |
meta_title_present |
boolean | Page has a non-empty <title>. |
meta_description_present |
boolean | Page has a non-empty <meta name="description">. |
has_og_tags |
boolean | At least an og:title was found. |
has_h1 |
boolean | Page has one or more <h1> tags. |
has_viewport |
boolean | Page has a <meta name="viewport">. |
image_count |
integer | Total number of <img> tags found. |
images_without_alt |
integer | Images missing or with empty alt text. |
error |
string | null | null on success. On failure, a short message (e.g. timeout / DNS / non-2xx). |
A failed row is not an HTTP error — it shows up as a normal entry with error populated and zeroed-out fields. You always get exactly total rows back.
Examples¶
curl¶
curl -X POST "https://seo.toolkitapi.io/v1/seo/bulk-audit" \
-H "x-api-key: $TOOLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"https://example.com",
"https://example.com/pricing",
"https://example.com/blog"
]
}'
Python¶
import requests
URLS = [
"https://example.com",
"https://example.com/pricing",
"https://example.com/blog",
]
resp = requests.post(
"https://seo.toolkitapi.io/v1/seo/bulk-audit",
json={"urls": URLS},
headers={"x-api-key": API_KEY},
timeout=60,
)
data = resp.json()
# Surface anything below threshold or that errored
for row in data["results"]:
if row["error"]:
print(f"⚠️ {row['url']}: {row['error']}")
elif row["score"] < 70:
print(
f" {row['url']}: score={row['score']}, "
f"images_without_alt={row['images_without_alt']}, "
f"og={row['has_og_tags']}"
)
JavaScript¶
const URLS = [
"https://example.com",
"https://example.com/pricing",
"https://example.com/blog",
];
const resp = await fetch("https://seo.toolkitapi.io/v1/seo/bulk-audit", {
method: "POST",
headers: {
"x-api-key": process.env.TOOLKIT_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ urls: URLS }),
});
const data = await resp.json();
const failing = data.results.filter((r) => r.error || r.score < 70);
console.table(failing);
Example Response¶
{
"total": 2,
"results": [
{
"url": "https://example.com/",
"score": 82,
"meta_title_present": true,
"meta_description_present": true,
"has_og_tags": true,
"has_h1": true,
"has_viewport": true,
"image_count": 5,
"images_without_alt": 0,
"error": null
},
{
"url": "https://example.com/missing",
"score": 0,
"meta_title_present": false,
"meta_description_present": false,
"has_og_tags": false,
"has_h1": false,
"has_viewport": false,
"image_count": 0,
"images_without_alt": 0,
"error": "Error fetching URL: 404"
}
]
}
Notes¶
- URLs are audited sequentially server-side. A 10-URL batch can take ~10–30 s depending on target latency — set client timeouts accordingly.
- Each URL goes through the same SSRF guard as
/v1/seo/audit; private/loopback/link-local hosts are rejected (the row will land with anerror). - For partial-success batches, look at the per-row
errorfield; the HTTP status of the call itself stays200even if every URL fails. - Need richer per-page detail (heading hierarchy, missing-alt
srcs, full OG/Twitter)? Re-issue offending URLs against/v1/seo/audit.