API REST.
Gestisci siti, audit, parole chiave, posizioni, backlink, Score e report dal tuo codice. Un'unica chiave, JSON ovunque, errori sempre nello stesso formato e il prezzo di ogni chiamata prima di spenderlo.
Indirizzo e chiave.
L'indirizzo di base è https://seoprimo.it/api/v1. Ogni richiesta porta la chiave nell'intestazione Authorization: Bearer sp_live_…. I cookie del pannello non funzionano qui, e la chiave non funziona nel pannello.
curl -sS https://seoprimo.it/api/v1/sites \
-H "Authorization: Bearer sp_live_..."
Crea le chiavi dal pannello, in Integrazioni, Chiavi API: il segreto si vede una sola volta (formato sp_live_ + 43 caratteri, noi teniamo solo l'hash) e si può revocare in qualunque momento. Gli scope sono due:
| Scope | Permette |
|---|---|
read | Tutti i GET: elenchi, dettagli, Score, report, crediti, esito delle run. |
write | POST e DELETE: aggiungere o eliminare un sito, avviare audit e analisi a crediti, creare monitoraggi. |
Le due cose sono indipendenti: una chiave solo write non può leggere. Per l'uso normale scegli entrambi.
Richieste al minuto.
Ogni chiave può fare 60 richieste al minuto (finestra di 60 secondi che parte dalla prima richiesta). Il limite è lo stesso per l'API REST e per il server MCP: i due canali si dividono lo stesso conteggio.
Ogni risposta riporta X-RateLimit-Limit, il limite della chiave. Oltre il limite la risposta è 429 (rate_limited) e l'intestazione Retry-After dice fra quanti secondi riprovare: non serve altro che aspettare.
Per aspettare il risultato di un lavoro lungo non interrogare di continuo: una richiesta ogni 5-10 secondi basta, oppure usa i webhook.
Il prezzo prima di spenderlo.
Le chiamate a pagamento usano i crediti dell'attività, gli stessi del pannello (prima quelli del piano, poi i pacchetti, ciascuno dalla scadenza più vicina). Sono gratis: elenco e dettaglio dei siti, aggiunta di un sito, lettura di audit, Score, report, crediti e run, creazione di un monitoraggio. L'audit rapido costa 10 crediti fissi; l'audit completo costa la scansione, 2 crediti fino a 200 pagine e 1 credito ogni 100 pagine oltre, più i controlli Lighthouse se li chiedi; le analisi del motore (parole chiave, posizioni, backlink, concorrenti, risultati di ricerca, SEO locale, visibilità AI) costano in base a quanti dati chiedi.
dry_run: la stima senza esecuzione
Aggiungi ?dry_run=true a qualunque POST: non parte niente, non si addebita niente e la risposta è la stima.
curl -sS -X POST "https://seoprimo.it/api/v1/sites/<site_id>/keywords/research?dry_run=true" \
-H "Authorization: Bearer $SEOPRIMO_KEY" -H "Content-Type: application/json" \
-d '{"seeds": ["scarpe running"], "result_limit": 150}'
# {"estimated_credits": 12}
- La stima è sempre il caso peggiore: è la cifra che viene prenotata prima della chiamata.
- A chiamata finita il consumo si ricalcola sui dati davvero ricevuti e non supera mai la stima: la differenza torna disponibile.
- Se la chiamata fallisce, i crediti tornano per intero.
- L'intestazione
X-Credits-Chargeddice quanti crediti ha addebitato la chiamata (per un lavoro lungo in corso, quelli prenotati). - Senza crediti sufficienti la risposta è
402e non parte niente:
{
"error": "insufficient_credits",
"needed": 25,
"available": 4
}
Avvia, poi controlla.
Alcune operazioni non finiscono subito: l'audit completo e il controllo delle posizioni rispondono 202 con una run in corso. Leggi l'esito da GET /runs/{id}: lo stato è queued, running, done o failed, e il risultato (result) c'è solo a done. Se la run fallisce, i crediti tornano indietro.
# a long job (full audit, rank check) answers 202 with a run
RUN_ID=$(curl -sS -X POST "$API/sites/$SITE_ID/full-audits" -H "$AUTH" -H "Content-Type: application/json" \
-d '{"max_pages": 100, "run_lighthouse": false}' | jq -r .id)
# poll the run: status is queued, running, done or failed; "result" is filled only when done
curl -sS "$API/runs/$RUN_ID" -H "$AUTH" | jq '{status, credits_reserved, credits_charged, result}'
L'audit rapido è diverso: non è una run. Risponde 202 con l'id dell'audit e si segue con GET /sites/{id}/audits, cercando quell'id. È il percorso dell'esempio qui sotto.
Sito, audit rapido, Score.
Il percorso completo: aggiungi un sito, chiedi il prezzo dell'audit rapido, avvialo, aspetta che finisca e leggi lo SeoPrimo Score. Servono i crediti per l'audit (10) e una chiave con entrambi gli scope. Lo Score dell'audit rapido è preliminare (preliminary: true) finché il pilastro Autorità non è misurato.
export SEOPRIMO_KEY="sp_live_..."
API=https://seoprimo.it/api/v1
AUTH="Authorization: Bearer $SEOPRIMO_KEY"
# 1. add the site (free). If it already exists the answer is 409: take its id from GET /sites
SITE_ID=$(curl -sS -X POST "$API/sites" -H "$AUTH" -H "Content-Type: application/json" \
-d '{"url": "https://www.acmescarpe.it/", "name": "Acme Scarpe"}' | jq -r .id)
# 2. the price of the quick audit, without running it
curl -sS -X POST "$API/sites/$SITE_ID/audits?dry_run=true" -H "$AUTH"
# {"estimated_credits":10}
# 3. start the quick audit (answer 202: {"id": "...", "status": "queued", "cost": 10})
AUDIT_ID=$(curl -sS -X POST "$API/sites/$SITE_ID/audits" -H "$AUTH" | jq -r .id)
# 4. poll until the audit is done or failed
while true; do
STATUS=$(curl -sS "$API/sites/$SITE_ID/audits" -H "$AUTH" \
| jq -r --arg id "$AUDIT_ID" '.audits[] | select(.id == $id) | .status')
echo "status: $STATUS"
[ "$STATUS" = "done" ] || [ "$STATUS" = "failed" ] && break
sleep 10
done
# 5. the SeoPrimo Score
curl -sS "$API/sites/$SITE_ID/score" -H "$AUTH" | jq '{score, preliminary, pillars}'
import os
import time
import httpx
api = httpx.Client(
base_url="https://seoprimo.it/api/v1",
headers={"Authorization": f"Bearer {os.environ['SEOPRIMO_KEY']}"},
timeout=30,
)
def ok(response: httpx.Response) -> dict:
if response.is_error:
body = response.json() # always {"error": code, "detail": ...}
raise RuntimeError(f"{response.status_code} {body['error']}: {body['detail']}")
return response.json()
# 1. add the site (free). If it already exists the answer is 409: take its id from GET /sites
site = ok(api.post("/sites", json={"url": "https://www.acmescarpe.it/", "name": "Acme Scarpe"}))
site_id = site["id"]
# 2. the price of the quick audit, without running it
print(ok(api.post(f"/sites/{site_id}/audits", params={"dry_run": "true"}))) # {'estimated_credits': 10}
# 3. start the quick audit (answer 202)
audit_id = ok(api.post(f"/sites/{site_id}/audits"))["id"]
# 4. poll until the audit is done or failed
while True:
audits = ok(api.get(f"/sites/{site_id}/audits"))["audits"]
audit = next(a for a in audits if a["id"] == audit_id)
print("status:", audit["status"])
if audit["status"] in ("done", "failed"):
break
time.sleep(10)
# 5. the SeoPrimo Score
score = ok(api.get(f"/sites/{site_id}/score"))
print(score["score"], "preliminary" if score["preliminary"] else "full", score["pillars"])
// Node 18+ (global fetch)
const API = "https://seoprimo.it/api/v1";
const headers = { Authorization: `Bearer ${process.env.SEOPRIMO_KEY}`, "Content-Type": "application/json" };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function call(method, path, body) {
const response = await fetch(API + path, { method, headers, body: body && JSON.stringify(body) });
const data = await response.json();
if (!response.ok) throw new Error(`${response.status} ${data.error}: ${JSON.stringify(data.detail)}`);
return data;
}
// 1. add the site (free). If it already exists the answer is 409: take its id from GET /sites
const site = await call("POST", "/sites", { url: "https://www.acmescarpe.it/", name: "Acme Scarpe" });
// 2. the price of the quick audit, without running it
console.log(await call("POST", `/sites/${site.id}/audits?dry_run=true`)); // { estimated_credits: 10 }
// 3. start the quick audit (answer 202)
const started = await call("POST", `/sites/${site.id}/audits`);
// 4. poll until the audit is done or failed
let audit;
do {
await sleep(10_000);
const { audits } = await call("GET", `/sites/${site.id}/audits`);
audit = audits.find((a) => a.id === started.id);
console.log("status:", audit.status);
} while (!["done", "failed"].includes(audit.status));
// 5. the SeoPrimo Score
const score = await call("GET", `/sites/${site.id}/score`);
console.log(score.score, score.preliminary ? "preliminary" : "full", score.pillars);
La risposta di GET /sites/{id}/score ha la forma {"score": 72, "preliminary": true, "pillars": {…}, …}: score è lo SeoPrimo Score (0-100), pillars i cinque pilastri, checks i controlli superati o no, history le ultime 12 misure.
Sempre lo stesso formato.
Ogni errore è un JSON {"error": codice, "detail": testo}: error è un codice stabile su cui puoi basare il codice, detail un testo per le persone (in italiano; per validation_error è l'elenco dei campi sbagliati). L'unica variante è il 402, che al posto di detail porta needed e available.
| Stato | Codice | Significato |
|---|---|---|
401 | invalid_api_key | Chiave mancante, non valida o revocata. |
402 | insufficient_credits | Crediti insufficienti. Il corpo aggiunge needed e available. |
403 | insufficient_scope | La chiave non ha lo scope richiesto (read per i GET, write per POST e DELETE). |
404 | not_found | La risorsa non esiste o è di un'altra attività. |
409 | conflict | Il sito è già stato aggiunto. |
422 | validation_error | Richiesta non valida: detail è l'elenco dei campi sbagliati (loc, msg, type). |
429 | rate_limited | Più di 60 richieste al minuto con questa chiave. Aspetta i secondi indicati da Retry-After. |
500 | internal_error | Errore interno: riprova più tardi. |
502 | engine_error | Il motore di analisi ha dato errore. I crediti sono restituiti. |
503 | engine_not_configured | Il motore di analisi non è configurato o non risponde (engine_not_configured, engine_unavailable, service_unavailable). I crediti sono restituiti. |
Tutti gli endpoint.
La tabella è generata dalla specifica OpenAPI al momento della pubblicazione di questa pagina: non può restare indietro. Le * sono i campi obbligatori; i percorsi con {id} portano un id nell'indirizzo. Schemi, esempi di risposta e «Try it out» sono nella documentazione interattiva.
| Chiamata | Cosa fa | Parametri | Scope | Costo |
|---|---|---|---|---|
GET /sites | Elenco dei siti | - | read | Gratis |
POST /sites | Aggiungi un sito | url*, name, dry_run | write | Gratis |
GET /sites/{site_id} | Dettaglio sito | - | read | Gratis |
DELETE /sites/{site_id} | Elimina un sito | - | write | Gratis |
POST /sites/{site_id}/audits | Avvia l'audit rapido | dry_run | write | 10 crediti fissi |
GET /sites/{site_id}/audits | Audit rapidi del sito | - | read | Gratis |
POST /sites/{site_id}/full-audits | Avvia l'audit completo | max_pages, run_lighthouse, dry_run | write | A crediti · lavoro lungo |
POST /sites/{site_id}/keywords/research | Ricerca di parole chiave | seeds*, result_limit, dry_run | write | A crediti |
POST /sites/{site_id}/keywords/metrics | Metriche di parole chiave | keywords*, dry_run | write | A crediti |
GET /sites/{site_id}/rankings/trackers | Monitoraggi posizioni | - | read | Gratis |
POST /sites/{site_id}/rankings/trackers | Crea il monitoraggio | keywords*, schedule, dry_run | write | Gratis |
POST /sites/{site_id}/rankings/trackers/{tracker_id}/run | Controlla le posizioni ora | dry_run | write | A crediti · lavoro lungo |
POST /sites/{site_id}/backlinks/overview | Panoramica backlink | target, dry_run | write | A crediti |
POST /sites/{site_id}/backlinks/profile | Profilo backlink | target, page, page_size, dry_run | write | A crediti |
POST /sites/{site_id}/competitors | Concorrenti nelle ricerche | keywords*, limit, dry_run | write | A crediti |
POST /sites/{site_id}/serp | Risultati di ricerca | keywords*, depth, dry_run | write | A crediti |
POST /sites/{site_id}/local/profile | Scheda Google Business | business_name, dry_run | write | A crediti |
POST /sites/{site_id}/local/reviews | Recensioni Google | business_name, dry_run | write | A crediti |
POST /sites/{site_id}/ai-visibility | Visibilità nelle risposte AI | brand, prompts, keywords, platforms, limit, dry_run | write | A crediti |
GET /sites/{site_id}/score | SeoPrimo Score del sito | - | read | Gratis |
GET /sites/{site_id}/reports | Report del sito | - | read | Gratis |
GET /sites/{site_id}/reports/{report_id} | Un report con il suo HTML | - | read | Gratis |
POST /sites/{site_id}/agents | Avvia un agente | agent*, budget_credits, dry_run | write | A crediti · lavoro lungo |
GET /sites/{site_id}/agents/{run_id} | Stato di un agente | - | read | Gratis |
GET /credits | Crediti disponibili | - | read | Gratis |
GET /runs/{run_id} | Stato e risultato di una run | - | read | Gratis |
Una chiave,
venti endpoint.
Crea la chiave dal pannello e fai la prima chiamata in un minuto.