Integrate in 5 Steps
- Create an API key
Register and log in, then open SEO Analysis Tool API Key in the sidebar: https://www.spindorai.com/seo-analizi-api-key (free account). Key format:
sp_seo_…— full secret shown only once. - POST https://www.spindorai.com/api/public-seo-analizi/analyze
Headers:
Content-Type: application/json,X-Spindora-Key: sp_seo_…Body:
{"url":"https://example.com","keywords":[],"device":"desktop","locale":"tr"} - Save task_id
See response example below (JSON Response Examples).
- GET https://www.spindorai.com/api/public-seo-analizi/result/{task_id}
Poll with the same API key every 2 seconds. 202 = pending, 200 = ready. Recommended max wait: ~2 minutes (60 attempts).
- Show attribution
Required near results: Powered by Spindora → https://www.spindorai.com
Overview
The Spindora SEO Analysis API runs on-page SEO checks for any URL: title, meta, H1 structure, word count, schema, outbound links, server timing, and more. Requests are processed asynchronously — queue first, then poll results with task_id.
API key required
For external calls
30 analyses / day
Per API key
Attribution
Required link
Authentication
A valid API key is required for external integrations. Create keys from your dashboard; the full secret is shown only once at creation.
| Method | Value |
|---|---|
| Header (recommended) | X-Spindora-Key: sp_seo_… |
| Bearer | Authorization: Bearer sp_seo_… |
Create keys at: www.spindorai.com/seo-analizi-api-key (login required)
Quotas & Limits
- 30 analyses per day per API key.
- Up to 3 active keys per user.
- API responses do not include screenshots — available only on the www.spindorai.com web UI.
Request Flow
POST /analyze
Queue analysis, get task_id
GET /result/{id}
Poll every ~2 seconds
200 OK
JSON result ready
202 = pending · 200 = ready
Request Schema
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | Full URL to analyze (https://…) |
| keywords | string[] | No | Max 3 keywords |
| device | "desktop" | "mobile" | No | Default: desktop |
| locale | "tr" | "en" | No | Language of check messages |
Endpoints
/analyzeStarts an analysis.
{
"url": "https://example.com",
"keywords": ["seo", "kelime"],
"device": "desktop",
"locale": "tr"
}Response: { "task_id": "...", "mode": "api", "attribution": { ... } }
/result/{task_id}Poll with the same API key. Returns full JSON when ready (screenshots empty).
/quotaWeb UI IP quota (not for external API).
JSON Response Examples
Expect these structures in production. screenshots is always an empty array.
POST /analyze → 200
{
"task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"message": "Analiziniz sıraya alındı. Lütfen bekleyin…",
"mode": "api",
"attribution": {
"text": "Powered by Spindora",
"url": "https://www.spindorai.com",
"required": true,
"html": "<a href=\"https://www.spindorai.com\" target=\"_blank\" rel=\"noopener\">Powered by Spindora</a>"
}
}GET /result/{task_id} → 202 (bekliyor)
{ "status": "pending" }GET /result/{task_id} → 200 (hazır, kısaltılmış)
{
"success": true,
"url": "https://example.com",
"final_url": "https://example.com/",
"device": "desktop",
"keywords": ["seo"],
"seo_score": 78,
"word_count": 1240,
"title_text": "Example Domain",
"meta_description": "Example meta description",
"h1_count": 1,
"used_playwright": false,
"screenshots": [],
"checks": [
{ "id": "title", "title": "Sayfa başlığı", "status": "ok", "message": "Başlık mevcut." }
],
"headings": [
{ "level": 1, "text": "Example Heading", "is_valid": true }
],
"keyword_checks": [
{ "keyword": "seo", "in_title": true, "in_meta": false, "in_h1": true, "count": 3, "density_pct": 0.24 }
],
"schema_types": ["WebPage"],
"server": { "ttfb_sec": 0.18, "total_sec": 0.42 },
"attribution": {
"text": "Powered by Spindora",
"url": "https://www.spindorai.com",
"required": true
}
}Attribution (Required)
Show visible, clickable attribution near analysis results in your UI:
<a href="https://www.spindorai.com" target="_blank" rel="noopener">Powered by Spindora</a>Code Examples
curl -X POST "https://www.spindorai.com/api/public-seo-analizi/analyze" \
-H "Content-Type: application/json" \
-H "X-Spindora-Key: sp_seo_YOUR_KEY_HERE" \
-d '{
"url": "https://example.com",
"keywords": ["seo", "analiz"],
"device": "desktop",
"locale": "tr"
}'curl "https://www.spindorai.com/api/public-seo-analizi/result/TASK_ID_HERE" \
-H "X-Spindora-Key: sp_seo_YOUR_KEY_HERE"const API = 'https://www.spindorai.com/api/public-seo-analizi';
const KEY = 'sp_seo_YOUR_KEY_HERE';
async function analyzeUrl(url) {
const start = await fetch(`${API}/analyze`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Spindora-Key': KEY,
},
body: JSON.stringify({
url,
keywords: [],
device: 'desktop',
locale: 'tr',
}),
});
const { task_id } = await start.json();
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 2000));
const res = await fetch(`${API}/result/${task_id}`, {
headers: { 'X-Spindora-Key': KEY },
});
if (res.status === 202) continue;
if (!res.ok) throw new Error(await res.text());
return res.json();
}
throw new Error('Timeout');
}// Node.js 18+ (native fetch)
const API = 'https://www.spindorai.com/api/public-seo-analizi';
const KEY = 'sp_seo_YOUR_KEY_HERE';
async function analyzeUrl(url) {
const startRes = await fetch(`${API}/analyze`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Spindora-Key': KEY,
},
body: JSON.stringify({
url,
keywords: [],
device: 'desktop',
locale: 'tr',
}),
});
const { task_id } = await startRes.json();
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 2000));
const pollRes = await fetch(`${API}/result/${task_id}`, {
headers: { 'X-Spindora-Key': KEY },
});
if (pollRes.status === 202) continue;
if (!pollRes.ok) throw new Error(await pollRes.text());
return pollRes.json();
}
throw new Error('Timeout');
}import time
import requests
API = "https://www.spindorai.com/api/public-seo-analizi"
KEY = "sp_seo_YOUR_KEY_HERE"
HEADERS = {"X-Spindora-Key": KEY}
def analyze(url: str) -> dict:
r = requests.post(
f"{API}/analyze",
headers={**HEADERS, "Content-Type": "application/json"},
json={"url": url, "keywords": [], "device": "desktop", "locale": "tr"},
timeout=30,
)
r.raise_for_status()
task_id = r.json()["task_id"]
for _ in range(60):
time.sleep(2)
poll = requests.get(f"{API}/result/{task_id}", headers=HEADERS, timeout=30)
if poll.status_code == 202:
continue
poll.raise_for_status()
return poll.json()
raise TimeoutError("Analysis timed out")<?php
$api = 'https://www.spindorai.com/api/public-seo-analizi';
$key = 'sp_seo_YOUR_KEY_HERE';
function analyze(string $url): array {
global $api, $key;
$ch = curl_init("$api/analyze");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"X-Spindora-Key: $key",
],
CURLOPT_POSTFIELDS => json_encode([
'url' => $url,
'keywords' => [],
'device' => 'desktop',
'locale' => 'tr',
]),
]);
$body = curl_exec($ch);
curl_close($ch);
$taskId = json_decode($body, true)['task_id'];
for ($i = 0; $i < 60; $i++) {
sleep(2);
$ch = curl_init("$api/result/$taskId");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["X-Spindora-Key: $key"],
]);
$poll = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code === 202) continue;
if ($code !== 200) throw new RuntimeException($poll);
return json_decode($poll, true);
}
throw new RuntimeException('Timeout');
}import java.net.URI;
import java.net.http.*;
import java.time.Duration;
public class SpindoraSeoClient {
private static final String API = "https://www.spindorai.com/api/public-seo-analizi";
private static final String KEY = "sp_seo_YOUR_KEY_HERE";
private static final HttpClient CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.build();
public static String analyze(String url) throws Exception {
String payload = "{\"url\":\"" + url + "\",\"keywords\":[],\"device\":\"desktop\",\"locale\":\"tr\"}";
HttpRequest start = HttpRequest.newBuilder()
.uri(URI.create(API + "/analyze"))
.header("Content-Type", "application/json")
.header("X-Spindora-Key", KEY)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
String startJson = CLIENT.send(start, HttpResponse.BodyHandlers.ofString()).body();
int marker = startJson.indexOf("\"task_id\":\"");
String taskId = startJson.substring(marker + 11, startJson.indexOf('\"', marker + 11));
for (int i = 0; i < 60; i++) {
Thread.sleep(2000);
HttpRequest poll = HttpRequest.newBuilder()
.uri(URI.create(API + "/result/" + taskId))
.header("X-Spindora-Key", KEY)
.GET()
.build();
HttpResponse<String> res = CLIENT.send(poll, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() == 202) continue;
if (res.statusCode() != 200) throw new RuntimeException(res.body());
return res.body();
}
throw new RuntimeException("Timeout");
}
}Response Fields
| Field | Description |
|---|---|
| seo_score | 0–100 SEO score |
| checks[] | Check list (id, title, status, message) |
| headings[] | H1–H6 structure |
| keyword_checks[] | Keyword matches |
| word_count | Word count |
| title_text, meta_description | Page title and meta |
| schema_types[] | Found schema types |
| server | TTFB, total time |
| attribution | Required display info |
Error Codes
| HTTP | Meaning | What to do |
|---|---|---|
| 401 | Invalid or missing API key | Check X-Spindora-Key header |
| 429 | Daily quota exceeded | Retry tomorrow or new key |
| 202 | Analysis in progress | Poll again after 2s |
| 422 | URL unreachable / analysis error | Verify URL is reachable |
| 500 | Server / queue error | Retry after a short delay |
Error body is usually { "detail": "..." }
Integration Checklist
- ✓API key created and stored securely
- ✓X-Spindora-Key sent on every request
- ✓Poll loop implemented after POST /analyze task_id
- ✓Wait on 202, parse JSON on 200
- ✓screenshots[] empty via API — do not expect images
- ✓Powered by Spindora link visible near results
- ✓User-friendly message on 429 quota
Brief for AI / LLMs
Paste this text or the OpenAPI JSON link when asking an AI to write integration code:
# Spindora SEO Analysis API — Integration Brief
Base URL: https://www.spindorai.com/api/public-seo-analizi
Documentation: https://www.spindorai.com/developers/seo-analizi-api
OpenAPI JSON: https://www.spindorai.com/developers/seo-analizi-api.openapi.json
Create API key: Register → log in → sidebar "SEO Analysis Tool API Key" → https://www.spindorai.com/seo-analizi-api-key (free account)
## Authentication
Header: X-Spindora-Key: sp_seo_YOUR_KEY
Alternative: Authorization: Bearer sp_seo_YOUR_KEY
## Flow (async)
1. POST /analyze → body: {"url":"https://example.com","keywords":[],"device":"desktop","locale":"en"}
2. Read task_id from response
3. GET /result/{task_id} — poll every 2 seconds
4. HTTP 202 = still processing, HTTP 200 = result ready
## Requirements
- API key required for external integrations
- Daily limit: 30 analyses per API key
- screenshots[] is always empty via API
- Mandatory UI attribution: <a href="https://www.spindorai.com">Powered by Spindora</a>
## Error codes
401 invalid key | 429 quota exceeded | 202 pending | 422 analysis error | 500 server errorOpenAPI: https://www.spindorai.com/developers/seo-analizi-api.openapi.json