API Reference

PerSeo Insights REST API v1.0

HTTPS only JSON v1.0.0

Base URL

https://insights.perseodesign.com/api/v1

The PerSeo Insights REST API lets you start SEO scans, read results, export reports and manage access tokens programmatically. Ideal for CI/CD, automated monitoring and third-party tool integrations.

To create your first API key, go to the Dashboard, section API & Token.

Authentication

All endpoints require a Bearer token in the HTTP header:

Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

API Key (permanent)

Long-lived tokens created from the Dashboard. They start with sk_live_. Use them for scripts, automations and server-side integrations.

JWT (session 24h)

Short-lived tokens (24h) issued by POST /auth/login. Use them for interactive clients requiring explicit login.

Available scopes

ScopeDescription
readRead access: profile, history, token list
scanStart new scans (POST /scans)
exportExport results (GET /scans/{id}/export/{format})

Rate Limits

API limits mirror the website limits. Reset every 24h (sliding window).

TierSingle Scan/daySitemap Scan/day
Guest (unauthenticated)11
Free (registered)55
Pro5010

For more details on plans and features, visit the Pricing page.

Response Format

All responses follow the standard JSON format:

Success

{ "success": true, "data": { ... }, "meta": { "timestamp": "2026-02-25T10:30:00Z", "version": "1.0", "request_id": "550e8400-..." } }

Error

{ "success": false, "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Scan limit reached (5/5).", "retry_after": 14400 }, "meta": { ... } }

Auth

POST /api/v1/auth/login No auth

Authenticate with email/password and receive a JWT access token (24h).

# Request curl -s -X POST https://insights.perseodesign.com/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"[email protected]","password":"password123"}'
Response 200
{ "success": true, "data": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_in": 86400, "token_type": "Bearer", "user": { "id": 42, "username": "mario.rossi", "email": "[email protected]" } } }
GET /api/v1/auth/verify JWT Bearer

Verifies the validity of a JWT token.

API Tokens

GET /api/v1/tokens scope: read

Lists all API keys for the authenticated user.

curl https://insights.perseodesign.com/api/v1/tokens \ -H "Authorization: Bearer sk_live_xxxx"
POST /api/v1/tokens scope: read

Creates a new API key. The token is shown only once.

FieldTypeReq.Description
namestringSIToken label (max 100 chars)
scopesarrayNODefault ["read"]. Values: read, scan, export
curl -X POST https://insights.perseodesign.com/api/v1/tokens \ -H "Authorization: Bearer sk_live_xxxx" \ -H "Content-Type: application/json" \ -d '{"name":"Script CI","scopes":["read","scan","export"]}'
IMPORTANT: The token field in the response is shown only once. Save it immediately in a secure place.
DELETE /api/v1/tokens/{id} scope: read

Permanently revokes (disables) an API key.

Scans

POST /api/v1/scans scope: scan

Starts an asynchronous SEO scan. Returns 202 Accepted and a scan_id to use for polling.

FieldTypeReq.Description
urlstringSIURL to analyze (http or https)
modestringNOsingle (default) or sitemap
curl -X POST https://insights.perseodesign.com/api/v1/scans \ -H "Authorization: Bearer sk_live_xxxx" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com","mode":"single"}'
Response 202 Accepted
{ "success": true, "data": { "scan_id": "a7f3d2e1-8b4c-4f9a-b6d5-c3e2f1a0b9d8", "status": "queued", "mode": "single", "url": "https://example.com", "poll_url": "/api/v1/scans/a7f3d2e1-..." } }
GET /api/v1/scans/{scan_id} scope: read

Retrieves status and results of a scan. Poll every 2-3 seconds until status != "running".

running

Running

complete

Completed

error

Failed

stopped

Stopped

# Bash polling loop while true; do RESULT=$(curl -s \ -H "Authorization: Bearer $TOKEN" \ "https://insights.perseodesign.com/api/v1/scans/$SCAN_ID") STATUS=$(echo $RESULT | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['status'])") [ "$STATUS" != "running" ] && break sleep 3 done

History

GET /api/v1/history scope: read

User scan history with pagination.

ParameterTypeDefaultDescription
pageint1Page number
per_pageint20Results per page (max 50)
curl "https://insights.perseodesign.com/api/v1/history?page=1&per_page=20" \ -H "Authorization: Bearer sk_live_xxxx"

Export

GET /api/v1/scans/{id}/export/{format} scope: export

Exports scan results. Supported formats: json, csv, pdf.

# Download CSV curl -H "Authorization: Bearer sk_live_xxxx" \ "https://insights.perseodesign.com/api/v1/scans/127/export/csv" \ -o report.csv

User

GET /api/v1/user/profile scope: read

Returns the authenticated user's profile.

GET /api/v1/user/usage scope: read

Usage statistics: scans today, API requests, plan limits.

Error Codes

CodeHTTPDescription
MISSING_AUTH 401 Authorization header missing or malformed
INVALID_TOKEN 401 Token invalid, expired or revoked
INSUFFICIENT_SCOPE 403 Token does not have the required scope
INVALID_CREDENTIALS 401 Wrong email/password
ACCOUNT_DISABLED 403 Account disabled
RATE_LIMIT_EXCEEDED 429 Scan or request limit exceeded
INVALID_REQUEST 400 Missing or malformed parameters
INVALID_URL 400 Invalid URL
INVALID_SCOPES 400 Unrecognized scopes
NOT_FOUND 404 Resource not found
FORBIDDEN 403 Access denied (resource belongs to another user)
INTERNAL_ERROR 500 Internal server error

Examples

Full flow: login + create token + first scan (Bash)

# 1. Login and get JWT JWT=$(curl -s -X POST https://insights.perseodesign.com/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"[email protected]","password":"password123"}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['access_token'])") # 2. Create a permanent API key TOKEN=$(curl -s -X POST https://insights.perseodesign.com/api/v1/tokens \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{"name":"Script CI","scopes":["read","scan","export"]}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])") echo "Save this token: $TOKEN" # 3. Start a scan SCAN_ID=$(curl -s -X POST https://insights.perseodesign.com/api/v1/scans \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com","mode":"single"}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['scan_id'])") # 4. Poll until complete while true; do RESULT=$(curl -s -H "Authorization: Bearer $TOKEN" \ "https://insights.perseodesign.com/api/v1/scans/$SCAN_ID") STATUS=$(echo $RESULT | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['status'])") echo "Status: $STATUS" [ "$STATUS" != "running" ] && break sleep 3 done # 5. Export as CSV curl -s -H "Authorization: Bearer $TOKEN" \ "https://insights.perseodesign.com/api/v1/scans/127/export/csv" \ -o "report_$(date +%Y%m%d).csv"

Python: monitoring a list of websites

import time import requests API_BASE = "https://insights.perseodesign.com/api/v1" TOKEN = "sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"} SITES = ["https://example.com", "https://myblog.com"] def scan_site(url): resp = requests.post(f"{API_BASE}/scans", json={"url": url, "mode": "single"}, headers=HEADERS) resp.raise_for_status() scan_id = resp.json()["data"]["scan_id"] while True: time.sleep(3) data = requests.get(f"{API_BASE}/scans/{scan_id}", headers=HEADERS).json()["data"] if data["status"] == "complete": return data.get("result", {}) elif data["status"] in ("error", "stopped"): raise RuntimeError(data.get("error")) for site in SITES: result = scan_site(site) g = result.get("googlebot", {}) print(f"{site}: SEO={g.get('seo_score')} Errori={len(g.get('errors',[]))}")

JavaScript/Node.js: sitemap scan

const fetch = require('node-fetch'); const API_BASE = 'https://insights.perseodesign.com/api/v1'; const TOKEN = 'sk_live_xxxx'; const headers = { 'Authorization': `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }; async function scanSitemap(url) { const { data } = await (await fetch(`${API_BASE}/scans`, { method: 'POST', headers, body: JSON.stringify({ url, mode: 'sitemap' }) })).json(); let result; while (true) { await new Promise(r => setTimeout(r, 3000)); result = (await (await fetch(`${API_BASE}/scans/${data.scan_id}`, { headers })).json()).data; if (result.status !== 'running') break; } return result; } scanSitemap('https://myblog.com/sitemap.xml').then(r => console.log(`Completed! ${r.result?.length} URLs analyzed`) );