REST API · v1

SEO Analysis Tool API

Integrate free on-page SEO analysis into your site or app.

Base URLhttps://www.spindorai.com/api/public-seo-analizi
OpenAPI JSON

Share this page or the OpenAPI file with AI assistants (ChatGPT, Claude, Cursor, etc.) to generate integration code.

Integrate in 5 Steps

  1. 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.

  2. 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"}

  3. Save task_id

    See response example below (JSON Response Examples).

  4. 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).

  5. 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.

MethodValue
Header (recommended)X-Spindora-Key: sp_seo_…
BearerAuthorization: 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

1

POST /analyze

Queue analysis, get task_id

2

GET /result/{id}

Poll every ~2 seconds

3

200 OK

JSON result ready

202 = pending · 200 = ready

Request Schema

FieldTypeRequiredDescription
urlstringYesFull URL to analyze (https://…)
keywordsstring[]NoMax 3 keywords
device"desktop" | "mobile"NoDefault: desktop
locale"tr" | "en"NoLanguage of check messages

Endpoints

POST/analyze

Starts an analysis.

Request body
{
  "url": "https://example.com",
  "keywords": ["seo", "kelime"],
  "device": "desktop",
  "locale": "tr"
}

Response: { "task_id": "...", "mode": "api", "attribution": { ... } }

GET/result/{task_id}

Poll with the same API key. Returns full JSON when ready (screenshots empty).

GET/quota

Web UI IP quota (not for external API).

JSON Response Examples

Expect these structures in production. screenshots is always an empty array.

POST /analyze → 200

JSON
{
  "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)

JSON
{ "status": "pending" }

GET /result/{task_id} → 200 (hazır, kısaltılmış)

JSON
{
  "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:

HTML
<a href="https://www.spindorai.com" target="_blank" rel="noopener">Powered by Spindora</a>
The attribution object in responses confirms this text. Removing or hiding it violates terms of use.

Code Examples

cURL — Start
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 — Poll
curl "https://www.spindorai.com/api/public-seo-analizi/result/TASK_ID_HERE" \
  -H "X-Spindora-Key: sp_seo_YOUR_KEY_HERE"
JavaScript (browser)
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
// 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');
}
Python
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
<?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');
}
Java
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

FieldDescription
seo_score0–100 SEO score
checks[]Check list (id, title, status, message)
headings[]H1–H6 structure
keyword_checks[]Keyword matches
word_countWord count
title_text, meta_descriptionPage title and meta
schema_types[]Found schema types
serverTTFB, total time
attributionRequired display info

Error Codes

HTTPMeaningWhat to do
401Invalid or missing API keyCheck X-Spindora-Key header
429Daily quota exceededRetry tomorrow or new key
202Analysis in progressPoll again after 2s
422URL unreachable / analysis errorVerify URL is reachable
500Server / queue errorRetry 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:

Copy — AI prompt
# 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 error

OpenAPI: https://www.spindorai.com/developers/seo-analizi-api.openapi.json