Documentazione API — SEO APIv1

SEO API — Documentazione API REST

Prodotto: SEO API · Versione API: v1 Base URL: https://seo.example.com/api/seo (alias versionato: https://seo.example.com/api/v1/seo) Autenticazione: Authorization: Bearer <API_KEY> Formato: JSON (UTF-8) · Aggiornato: 2026-06-30


Questa è la documentazione completa e ufficiale dell'API REST della suite SEO. Espone via API tutte le funzioni del prodotto: SEO Spider (crawl tecnico), GEO / AI Search (GEO audit, AI Visibility, AI Engine, AI Competitor, AI Prompt Research), Rank Tracker (progetti e keyword), Keyword Research, Google Search Console, Google Analytics e Alert.

Tutte le richieste richiedono un'API key valida (vedi Authentication) e sono soggette a rate-limit. Le risposte sono incapsulate in un oggetto {"data": ...}; gli elenchi includono un blocco meta di paginazione.


Indice

  1. Introduzione e convenzioni
  2. Base URL
  3. Autenticazione
  4. Rate limiting
  5. Convenzioni (paginazione, CSV, wrapping)
  6. Codici di errore standard
  7. GET /status
  8. GET / — Discovery
  9. Account & Lookups (API key, locations, languages)
  10. Rank Tracker — Progetti, Keyword, Keyword Research
  11. SEO Spider
  12. GEO & AI Search
  13. Search Console, Analytics & Alerts


Introduction

The SEO API exposes the SEO suite over HTTP/JSON. A single controller sub-routes on the path segments after seo, giving programmatic access to:

The discovery index (GET /) returns "name": "SEO API", "version": "v1".


Base URL

https://seo.example.com/api/seo

A versioned alias is also registered and served by the same controller:

https://seo.example.com/api/v1/seo

Both routes (seo and v1) are mapped to the same controller. When the request comes in under /api/v1/seo/..., the controller drops the leading seo version-prefix segment, so the two forms are equivalent (the literal seo is never treated as a resource name).

Resource names that contain multiple words use dashes in the URL (e.g. search-console, ai-visibility, keyword-research). The router normalizes dashes to underscores internally and the controller restores them, so dashed paths are the canonical form.


Authentication

All requests authenticate with a Bearer API key in the Authorization header. No cookies are used (CORS is enabled with Access-Control-Allow-Origin: *).

Authorization: Bearer <api_key>

The api_key comes from the user account. Two conditions must hold:

  1. The API must be enabled globally by the administrator.
  2. The user's plan must have the API enabled (plan_settings.api_is_enabled).

The API key is matched against an active user account (users.status = 1).

Auth example

curl -s https://seo.example.com/api/seo/status \
  -H "Authorization: Bearer YOUR_API_KEY"

Missing or invalid token

When the Authorization: Bearer header is missing, the API responds 401 with the "no bearer" error. When the token is present but does not match an active account (or the account's plan does not have the API enabled), the API responds 401 with the "no access" error. The error body follows the standard error envelope (see Conventions):

{
  "errors": [
    { "title": "...", "status": "401" }
  ]
}

If the API is globally disabled by the administrator, the request resolves to a 404 (the route is hidden) rather than a 401.


Rate limiting

Non-admin accounts are limited to 60 requests per 60 seconds per API key (a fixed-window counter keyed on the API key). Each response carries rate-limit headers:

Header Meaning
X-RateLimit-Limit The window limit (60).
X-RateLimit-Remaining Requests remaining in the current window (omitted once negative).
X-RateLimit-Reset Seconds until the window resets (sent when the limit is exceeded).

When the limit is exceeded the API responds 429 (rate-limit error). Admin accounts (type = 1) are not rate-limited.

Some write/analysis endpoints enforce additional per-hour quotas (e.g. 30/hour for GEO, AI Engine, AI Visibility, AI Competitor and prompt research; 60/hour for keyword research). Hitting those quotas also returns 429.


Conventions

Response envelope

Successful responses are JSON wrapped in a top-level data key:

{ "data": ... }

List endpoints add a meta object:

{
  "data": [ ... ],
  "meta": {
    "page": 1,
    "per_page": 25,
    "total_results": 137,
    "total_pages": 6
  }
}

Error responses use a top-level errors array, each entry with title and status:

{
  "errors": [
    { "title": "Risorsa non trovata.", "status": "404" }
  ]
}

Pagination

List endpoints accept these query parameters:

Param Default Min Max Notes
page 1 1 1-based page number.
per_page 25 1 100 Clamped into the 1..100 range.

meta.total_pages is computed as ceil(total_results / per_page).

CSV export

Any list endpoint can be exported as CSV by adding the query parameter:

?format=csv

When format=csv is set, the endpoint bypasses pagination and returns the full result set (capped at 5000 rows) as a file download rather than JSON. The response uses:

The first row is the header (column names taken from the first record's keys). Array/object values are JSON-encoded into the cell; booleans become 1/0. Cell values are formula-injection-safe.

curl -s "https://seo.example.com/api/seo/projects?format=csv" \
  -H "Authorization: Bearer YOUR_API_KEY" -o projects.csv

CORS / preflight

OPTIONS preflight requests are answered (HTTP 204) before authentication. Allowed methods: GET, POST, PATCH, DELETE, OPTIONS. Allowed headers: Authorization, Content-Type.

Request body

Write endpoints accept either form-encoded bodies (application/x-www-form-urlencoded) or a raw JSON body.


Standard error codes

HTTP When it occurs
400 Generic bad request (default error status).
401 Missing Authorization: Bearer header, or the token is invalid / the account lacks API access.
404 Unknown resource or sub-path; resource not owned by the caller; or the API is globally disabled.
405 Known resource but the HTTP method is not allowed for it (the Allow header lists permitted methods).
422 Validation error — a required field is missing or no updatable field was provided (e.g. name/domain, keyword, url, start_url, brand, competitors, prompt).
429 Rate limit exceeded (60/60s) or a per-endpoint hourly quota / refresh cooldown reached.

Additional statuses used by integration endpoints: 409 (Google/DataForSEO not configured or account not connected) and 502 (an upstream analysis or Google call failed).


GET /status

Returns the configuration/connection state for the calling account. No pagination.

Response fields

Field Type Meaning
dataforseo_configured boolean DataForSEO data source is configured (admin level).
google_configured boolean Google integration is configured (admin level).
google_connected boolean This account has a connected Google OAuth account.
google_email string | null The connected Google account email, or null if not connected.
api_enabled boolean The caller's plan has the API enabled.

Example

curl -s https://seo.example.com/api/seo/status \
  -H "Authorization: Bearer YOUR_API_KEY"
{
  "data": {
    "dataforseo_configured": true,
    "google_configured": true,
    "google_connected": true,
    "google_email": "owner@example.com",
    "api_enabled": true
  }
}

GET / — Discovery index

GET /api/seo (the resource root, via GET) returns a discovery document describing the API.

Response fields

Field Type Value
name string "SEO API".
version string "v1".
base_url string The API base URL, <SITE_URL>/api/seo.
auth string "Authorization: Bearer <api_key>".
docs string URL of the API docs, <SITE_URL>/api-docs.
resources array List of available resource names (see below).
notes string Free-text usage notes (pagination, CSV export, rate limit, CORS).

resources is:

status, projects, keywords, spider, geo, ai-visibility, ai-engine,
ai-competitor, prompt-research, keyword-research, search-console, analytics, alerts

Example

curl -s https://seo.example.com/api/seo \
  -H "Authorization: Bearer YOUR_API_KEY"
{
  "data": {
    "name": "SEO API",
    "version": "v1",
    "base_url": "https://seo.example.com/api/seo",
    "auth": "Authorization: Bearer <api_key>",
    "docs": "https://seo.example.com/api-docs",
    "resources": [
      "status", "projects", "keywords", "spider", "geo",
      "ai-visibility", "ai-engine", "ai-competitor", "prompt-research",
      "keyword-research", "search-console", "analytics", "alerts"
    ],
    "notes": "Liste paginate (?page=&per_page=, max 100); export CSV con ?format=csv; rate-limit 60/min; CORS abilitato."
  }
}

Account & Lookups

Account-level utilities and reference data. These are not listed in the discovery resources array but are fully supported resources.

GET /api-key — show the masked API key

Returns the caller's own API key, masked (first 4 + last 4 characters kept, the middle replaced by bullets). Useful for confirming which key is in use without exposing it.

Path: GET /api/seo/api-key

Response data: { "api_key_masked": "062d••…••ab" } (null if no key is set).

curl -s "https://seo.example.com/api/seo/api-key" \
  -H "Authorization: Bearer <api_key>"

POST /api-key/rotate — rotate the API key

Generates a new API key, stores it, and returns it in clear text (the only time it is shown in full). The old key stops working immediately — its cache entry is invalidated, so the next request with the old key returns 401.

Path: POST /api/seo/api-key/rotate

Response data: { "api_key": "<new_key>" }.

curl -s -X POST "https://seo.example.com/api/seo/api-key/rotate" \
  -H "Authorization: Bearer <current_api_key>"

After rotating, update your stored credential before making further calls — the rest of your session must use the new key.

GET /locations — location lookup

Looks up Google/DataForSEO geo targets (used for location_code on projects and keyword research). Requires the DataForSEO data source to be configured (409 otherwise).

Path: GET /api/seo/locations

Query parameters

Param Type Default Notes
q string Free-text filter (country/city name).
country string ISO-2 country code. Without country → a list of countries; with country=IT → that country's cities/regions.
limit int 100 Max results, clamped to 1–500.

Response data: array of location objects (as returned by the data source, e.g. location_code, location_name, country_iso_code). 502 if the upstream lookup fails.

curl -s "https://seo.example.com/api/seo/locations?country=IT&q=milan" \
  -H "Authorization: Bearer <api_key>"

GET /languages — language lookup

Returns supported language codes for projects / keyword research. Prefers the live data-source catalogue (cached ~24h) and falls back to a built-in list when the data source is unavailable, so it never errors.

Path: GET /api/seo/languages

Query parameters

Param Type Notes
q string Filter by language name (substring, case-insensitive) or exact code.

Response data: array of { "code": "it", "name": "Italiano" } objects.

curl -s "https://seo.example.com/api/seo/languages?q=ital" \
  -H "Authorization: Bearer <api_key>"

Rank Tracker (Projects, Keywords, Keyword Research)

REST endpoints for managing rank-tracking projects, their tracked keywords, and ad-hoc keyword research.

Common response envelope

Success responses use a JSON:API-style envelope:

{ "data": { ... }, "meta": { ... } }

meta is only present on paginated list responses. Error responses use:

{ "errors": [ { "title": "message", "status": "404" } ] }

Common errors

Status When
401 Missing/invalid Bearer key, inactive user, or API access not enabled for the plan.
404 Unknown resource/sub-path, or a project/keyword that does not exist or is not owned by the authenticated user. Title: not_found message.
405 Known resource but unsupported HTTP method (an Allow header lists the permitted verbs).
422 Validation error (missing required fields, nothing to update, empty keyword).
429 Rate limit reached (global API limit, refresh cooldown, or keyword-research hourly cap).
502 Upstream DataForSEO error during a refresh or research call.
409 DataForSEO data source not configured.

All collections are scoped to the authenticated user (user_id). Operations on a project/keyword owned by another user return 404.


GET /projects — list projects

Returns the authenticated user's rank-tracker projects, newest first (ordered by project_id DESC).

Path: GET /api/seo/projects

Query parameters

Param Type Default Notes
page int 1 Page number (min 1).
per_page int 25 Items per page, clamped to 1–100.
format string If csv, returns the full set (capped at 5000 rows) as a CSV file download instead of a JSON page.

Response data — array of project objects:

Field Type Description
id int Project ID.
name string Project display name.
domain string Normalized domain being tracked.
location_code int DataForSEO location code (geo target).
language_code string Language code (e.g. it, en).
keywords int Count of keywords tracked in the project.
gsc_property string | null Linked Google Search Console property.
ga4_property string | null Linked GA4 property ID (digits only).
last_datetime string | null Last refresh timestamp (Y-m-d H:i:s) or null.
datetime string Project creation timestamp.

Response meta: page, per_page, total_results, total_pages.

Example

curl -s "https://seo.example.com/api/seo/projects?page=1&per_page=25" \
  -H "Authorization: Bearer <api_key>"
{
  "data": [
    {
      "id": 12,
      "name": "My Website",
      "domain": "example.com",
      "location_code": 2380,
      "language_code": "it",
      "keywords": 34,
      "gsc_property": "sc-domain:example.com",
      "ga4_property": "123456789",
      "last_datetime": "2026-06-12 03:15:02",
      "datetime": "2026-05-01 10:22:41"
    }
  ],
  "meta": { "page": 1, "per_page": 25, "total_results": 1, "total_pages": 1 }
}

GET /projects/{id} — get one project (with stats + keywords)

Returns a single project including its keywords and a 90-day stats summary computed locally.

Path: GET /api/seo/projects/{id}

Path params: id (int) — project ID.

Query parameters

Param Type Notes
format string If csv, returns a deep export of the project's keywords (one row per keyword: project, domain, keyword, last_position, last_url, search_volume, last_datetime) as a CSV file download instead of the JSON object below.

Response data

Field Type Description
id int Project ID.
name string Project name.
domain string Tracked domain.
location_code int DataForSEO location code.
language_code string Language code.
gsc_property string | null Linked GSC property.
ga4_property string | null Linked GA4 property ID.
last_datetime string | null Last refresh timestamp.
datetime string Creation timestamp.
stats object | null 90-day stats summary (see below); null if the stats helper is unavailable.
keywords array Tracked keywords (see below), ordered by keyword_id ASC.

keywords[] items

Field Type Description
id int Keyword ID.
keyword string The keyword phrase.
last_position int | null Most recent ranking position (null if never ranked/checked).
last_url string | null URL that ranked at the last check.
search_volume int | null Monthly search volume (null if unknown).
last_datetime string | null Timestamp of the last position check.

stats object (computed over rankings of the last 90 days)

Field Type Description
tracked int Number of keywords in the project.
ranking int Number of keywords with a known latest position.
avg_position float | null Average latest position across ranking keywords (1 decimal), or null.
buckets object Position distribution: top3, top10, top20, top50, top100, none (int counts).
gained array Up to 5 biggest improvers; each { keyword, delta, from, to } (delta/from null for new entries).
lost array Up to 5 biggest decliners; each { keyword, delta, from, to } (to null for dropped-out keywords).
timeline object Map of YYYY-MM-DD → average position that day (1 decimal).

Example

curl -s "https://seo.example.com/api/seo/projects/12" \
  -H "Authorization: Bearer <api_key>"
{
  "data": {
    "id": 12,
    "name": "My Website",
    "domain": "example.com",
    "location_code": 2380,
    "language_code": "it",
    "gsc_property": "sc-domain:example.com",
    "ga4_property": "123456789",
    "last_datetime": "2026-06-12 03:15:02",
    "datetime": "2026-05-01 10:22:41",
    "stats": {
      "tracked": 2,
      "ranking": 2,
      "avg_position": 7.5,
      "buckets": { "top3": 1, "top10": 1, "top20": 0, "top50": 0, "top100": 0, "none": 0 },
      "gained": [ { "keyword": "agenzia seo trento", "delta": 4, "from": 9, "to": 5 } ],
      "lost": [],
      "timeline": { "2026-06-10": 8.0, "2026-06-12": 7.5 }
    },
    "keywords": [
      {
        "id": 101,
        "keyword": "agenzia seo trento",
        "last_position": 5,
        "last_url": "https://example.com/seo",
        "search_volume": 320,
        "last_datetime": "2026-06-12 03:15:02"
      }
    ]
  }
}

Errors: 404 if the project does not exist or is not owned by the caller.


POST /projects — create a project

Creates a new rank-tracker project for the authenticated user.

Path: POST /api/seo/projects

Request body

Field Type Required Notes
name string yes Trimmed, max 255 chars. Must be non-empty.
domain string yes Normalized (scheme/path/www stripped). Must be non-empty.
location_code int no Defaults to the configured default_location_code.
language_code string no Lowercased, stripped to [a-z-], max 8 chars. Defaults to the configured default_language_code, falling back to en.

Response: 201 Created with data: { "id": <new_project_id> }.

Example

curl -s -X POST "https://seo.example.com/api/seo/projects" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{ "name": "My Website", "domain": "example.com", "location_code": 2380, "language_code": "it" }'
{ "data": { "id": 13 } }

Errors: 422 if name or domain is empty (I campi name e domain sono obbligatori.).


PATCH /projects/{id} — update a project

Partially updates a project. Only the fields present in the body are changed.

Path: PATCH /api/seo/projects/{id}

Path params: id (int).

Request body (all optional; at least one must be present)

Field Type Notes
name string Trimmed, max 255 chars.
domain string Normalized (scheme/path/www stripped).
location_code int Cast to int.
language_code string Lowercased, stripped to [a-z-], max 8 chars.
gsc_property string | null Max 255 chars. Send null or "" to clear it.
ga4_property string | null Digits only. Send null or "" to clear it.

Response: data: { "id": <project_id> }.

Example

curl -s -X PATCH "https://seo.example.com/api/seo/projects/12" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{ "name": "My Website", "gsc_property": "sc-domain:example.com" }'
{ "data": { "id": 12 } }

Errors: 404 if not found/owned; 422 if no recognized field is supplied (Nessun campo da aggiornare.).


POST /projects/{id}/keywords — add keywords

Adds keywords to a project, de-duplicating case-insensitively against existing keywords and within the submitted batch.

Path: POST /api/seo/projects/{id}/keywords

Path params: id (int).

Request body — provide one of:

Field Type Notes
keywords array of strings Preferred. Each entry trimmed, max 255 chars; empty entries skipped.
keyword string Alternative. A newline-separated list (\r\n, \r, or \n) split into individual keywords.
mode string Optional. replacewipe the project's current keywords first (their rankings cascade), then insert the supplied list. Any other value / omitted → append (default).

If keywords is a non-empty array it is used; otherwise keyword is split. At least one must yield a keyword. Matching is case-insensitive both against existing keywords and within the batch.

Response: 201 Created with counts.

Field Type Description
added int Number of new keywords inserted.
skipped int Number skipped as duplicates (existing or repeated in batch).
removed int Number of keywords deleted by mode=replace (always 0 in append mode).

Example (array form)

curl -s -X POST "https://seo.example.com/api/seo/projects/12/keywords" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{ "keywords": ["agenzia seo trento", "consulente seo", "agenzia seo trento"] }'
{ "data": { "added": 2, "skipped": 1, "removed": 0 } }

Example (replace the whole set)

curl -s -X POST "https://seo.example.com/api/seo/projects/12/keywords" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{ "mode": "replace", "keywords": ["agenzia seo trento", "consulente seo"] }'

Example (newline string form)

curl -s -X POST "https://seo.example.com/api/seo/projects/12/keywords" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{ "keyword": "agenzia seo trento\nconsulente seo" }'

Errors: 404 if project not found/owned; 422 if neither field yields a keyword (Fornire keyword (stringa) o keywords (array).).


POST /projects/{id}/refresh — refresh rankings

Triggers a synchronous refresh of all the project's keyword rankings via DataForSEO. Uses an internal claim + cooldown (~10 min) to avoid concurrent/duplicate refreshes.

Path: POST /api/seo/projects/{id}/refresh

Path params: id (int).

Request body: none.

Response: data: { "refreshed": <count> } — number of keywords successfully refreshed.

Example

curl -s -X POST "https://seo.example.com/api/seo/projects/12/refresh" \
  -H "Authorization: Bearer <api_key>"
{ "data": { "refreshed": 34 } }

Errors:

Status Cause
409 DataForSEO data source not configured (not_configured).
404 Project not found/owned (not_found).
429 A refresh ran recently; in cooldown (~10 min).
502 Upstream API failed mid-run; message includes how many succeeded before the failure.

GET /projects/{id} keyword sub-resources

Project keywords are returned inline by GET /projects/{id}. Individual keyword operations live under the top-level /keywords resource (the keyword's own ID, not the project ID).

GET /keywords/{id}/history — keyword ranking history

Returns the position history for one keyword over a window of days.

Path: GET /api/seo/keywords/{id}/history

Path params: id (int) — keyword ID (must belong to the caller).

Query parameters

Param Type Default Notes
days int 90 Look-back window, clamped to 1–365.

Response data

Field Type Description
keyword string The keyword phrase.
history array Points ordered by datetime ascending.

Each history[] item: { "position": int|null, "url": string|null, "datetime": "Y-m-d H:i:s" }.

Example

curl -s "https://seo.example.com/api/seo/keywords/101/history?days=30" \
  -H "Authorization: Bearer <api_key>"
{
  "data": {
    "keyword": "agenzia seo trento",
    "history": [
      { "position": 9, "url": "https://example.com/seo", "datetime": "2026-06-10 03:10:00" },
      { "position": 5, "url": "https://example.com/seo", "datetime": "2026-06-12 03:15:02" }
    ]
  }
}

DELETE /keywords/{id} — delete a keyword

Removes a single tracked keyword (and its rankings cascade).

Path: DELETE /api/seo/keywords/{id}

Path params: id (int) — keyword ID owned by the caller.

Response: data: { "id": <keyword_id> }.

curl -s -X DELETE "https://seo.example.com/api/seo/keywords/101" \
  -H "Authorization: Bearer <api_key>"
{ "data": { "id": 101 } }

Errors: 404 if the keyword does not exist or is not owned by the caller.

GET /keywords — list all tracked keywords

Lists every keyword tracked across the caller's projects (paginated), newest project first. Unlike the inline keyword list in GET /projects/{id}, this collection spans all projects and supports filtering and CSV export.

Path: GET /api/seo/keywords

Query parameters

Param Type Default Notes
page int 1 Page number (min 1).
per_page int 25 Items per page, clamped to 1–100.
project_id int Restrict to keywords of a single project.
tag string Only keywords carrying this tag (exact match against the keyword's tag set).
format string csv → full set (capped at 5000) as a CSV download; the tags array is flattened with |.

Response data — array of keyword objects (same shape as GET /keywords/{id}):

Field Type Description
id int Keyword ID.
project_id int Owning project.
keyword string The keyword phrase.
search_volume int | null Monthly search volume.
last_position int | null Most recent tracked position.
last_url string | null Ranking URL at the last check.
target_url string | null Organisational target URL.
tags array Tag strings (empty array when none).
notes string | null Free-text note.
last_datetime string | null Last check timestamp.
datetime string When the keyword was added.
curl -s "https://seo.example.com/api/seo/keywords?tag=priority&per_page=50" \
  -H "Authorization: Bearer <api_key>"

GET /keywords/{id} — get one keyword

Returns a single tracked keyword (owned by the caller), in the object shape above.

Path: GET /api/seo/keywords/{id} · Errors: 404 if not found/owned.

PATCH /keywords/{id} — update organisational fields

Updates only the organisational metadata of a keyword. The keyword text itself is not editable (it is unique per project and tied to billing/history).

Path: PATCH /api/seo/keywords/{id}

Body params (JSON or form-encoded; send at least one, else 422):

Field Type Notes
target_url string | null Up to 2048 chars; null/empty clears it.
tags array | string Array of tags, or a comma-separated string. Trimmed, de-duplicated, max 20 tags (64 chars each).
notes string | null Up to 1024 chars; null/empty clears it.

Response data: the updated keyword object (same shape as GET /keywords/{id}).

curl -s -X PATCH "https://seo.example.com/api/seo/keywords/101" \
  -H "Authorization: Bearer <api_key>" -H "Content-Type: application/json" \
  -d '{"tags":["priority","home"],"target_url":"https://example.com/seo","notes":"Landing page in review"}'

Errors: 404 (not found/owned); 422 if no updatable field (target_url, tags, notes) was supplied.

DELETE /keywords — bulk delete keywords

Deletes multiple keywords in a single call (scoped to the caller).

Path: DELETE /api/seo/keywords

Body params: ids — array of keyword IDs (also accepted as a comma-separated string, or an ?ids= query param). Foreign keywords are silently ignored (the WHERE is scoped to user_id).

Response: data: { "deleted": <count> }.

curl -s -X DELETE "https://seo.example.com/api/seo/keywords" \
  -H "Authorization: Bearer <api_key>" -H "Content-Type: application/json" \
  -d '{"ids":[101,102,103]}'

Errors: 422 if ids is missing/empty or contains no valid integer.

DELETE /projects/{id} — delete a project

Deletes the project; keywords and rankings cascade.

Path: DELETE /api/seo/projects/{id}

Response: data: { "id": <project_id> }.

curl -s -X DELETE "https://seo.example.com/api/seo/projects/12" \
  -H "Authorization: Bearer <api_key>"

Errors: 404 if not found/owned.


POST /keyword-research — keyword research

Runs an on-demand keyword research lookup against DataForSEO Labs: a seed-keyword overview plus related keywords and questions. Results are server-side cached per keyword + location_code + language_code.

Path: POST /api/seo/keyword-research

Request body

Field Type Required Notes
keyword string yes Trimmed; must be non-empty. Lowercased internally.
location_code int no DataForSEO location code. Falls back to the configured default_location_code.
language_code string no Lowercased, stripped to [a-z-], max 8 chars. Falls back to the configured default_language_code.

The ideas limit is fixed server-side (100); it is not a request parameter.

Rate limit: max 60 research calls per user per rolling hour (cache-based counter). Exceeding it returns 429.

Response: 201 Created.

Field Type Description
seed object Metrics for the seed keyword (a "keyword metric" object, see below).
related array Related (non-question) keyword ideas, sorted by opportunity descending.
questions array Question-style keyword ideas, sorted by search_volume descending.
location_code int Resolved location code used.
language_code string Resolved language code used.
generated_at string Y-m-d H:i:s generation timestamp.

Keyword metric object (used for seed and each item in related/questions)

Field Type Description
keyword string The keyword phrase.
search_volume int | null Monthly search volume.
cpc float | null Cost-per-click.
competition float | null Competition index (0–1).
competition_level string | null e.g. LOW, MEDIUM, HIGH.
difficulty int | null Keyword difficulty score.
intent string | null Main search intent (e.g. informational, commercial).
monthly_searches array Per-month search-volume history from DataForSEO (each { year, month, search_volume }).
serp_features array SERP feature types present (e.g. featured_snippet, people_also_ask, video).
opportunity number | null Computed opportunity score (volume vs. difficulty).

Example

curl -s -X POST "https://seo.example.com/api/seo/keyword-research" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{ "keyword": "agenzia seo", "location_code": 2380, "language_code": "it" }'
{
  "data": {
    "seed": {
      "keyword": "agenzia seo",
      "search_volume": 2400,
      "cpc": 6.12,
      "competition": 0.71,
      "competition_level": "HIGH",
      "difficulty": 58,
      "intent": "commercial",
      "monthly_searches": [ { "year": 2026, "month": 5, "search_volume": 2400 } ],
      "serp_features": [ "people_also_ask", "local_pack" ],
      "opportunity": 41.3
    },
    "related": [
      {
        "keyword": "agenzia seo trento",
        "search_volume": 320,
        "cpc": 4.05,
        "competition": 0.44,
        "competition_level": "MEDIUM",
        "difficulty": 27,
        "intent": "commercial",
        "monthly_searches": [],
        "serp_features": [ "local_pack" ],
        "opportunity": 63.0
      }
    ],
    "questions": [
      {
        "keyword": "cosa fa un'agenzia seo",
        "search_volume": 90,
        "cpc": null,
        "competition": null,
        "competition_level": null,
        "difficulty": 12,
        "intent": "informational",
        "monthly_searches": [],
        "serp_features": [ "people_also_ask" ],
        "opportunity": 70.0
      }
    ],
    "location_code": 2380,
    "language_code": "it",
    "generated_at": "2026-06-13 11:04:22"
  }
}

Errors:

Status Cause
422 keyword empty (Il campo keyword è obbligatorio.).
409 DataForSEO not configured (Fonte dati DataForSEO non configurata.).
429 Hourly research limit reached (Limite orario raggiunto.).
502 Upstream research failed (service error message or Ricerca non riuscita.).
405 Any method other than POST on /keyword-research.

SEO Spider

Endpoints for crawling a website and retrieving the resulting SEO audit (scans, crawled pages and issue breakdowns).

Base URL: https://seo.example.com/api/seo Authentication: all requests require a Bearer token.

Authorization: Bearer <api_key>

Conventions


GET /spider

List the SEO Spider scans belonging to the authenticated user, newest first (ordered by scan_id DESC).

Query parameters

Name Type Default Description
page int 1 Page number.
per_page int 25 Results per page (max 100).
format string Set to csv to download the list as CSV (up to 5000 rows).

Per-scan fields (summary shape)

Field Type Description
id int Scan id.
name string Scan name (may be empty).
host string Crawled host.
status string crawling, completed, or failed.
score int | null Overall SEO score (0–100), null until computed.
pages_found int URLs discovered/enqueued.
pages_processed int Pages actually crawled.
errors_count int Number of error-level issues.
warnings_count int Number of warning-level issues.
started_datetime string | null When the crawl started.
finished_datetime string | null When the crawl finished.
datetime string Record creation timestamp.

curl

curl -s "https://seo.example.com/api/seo/spider?page=1&per_page=25" \
  -H "Authorization: Bearer <api_key>"

Example response

{
  "data": [
    {
      "id": 42,
      "name": "Crawl example.com",
      "host": "example.com",
      "status": "completed",
      "score": 78,
      "pages_found": 312,
      "pages_processed": 312,
      "errors_count": 9,
      "warnings_count": 41,
      "started_datetime": "2026-06-13 09:14:02",
      "finished_datetime": "2026-06-13 09:31:55",
      "datetime": "2026-06-13 09:14:01"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 25,
    "total_results": 1,
    "total_pages": 1
  }
}

GET /spider/{id}

Retrieve a single scan (detail shape). Returns the same fields as the list shape, plus — only while status is crawling — a pending field with the number of URLs still queued.

Path parameters

Name Type Description
id int Scan id. Must belong to the authenticated user.

Additional field (detail, crawling only)

Field Type Description
pending int URLs still in the queue (present only when status = crawling).

curl

curl -s "https://seo.example.com/api/seo/spider/42" \
  -H "Authorization: Bearer <api_key>"

Example response (in progress)

{
  "data": {
    "id": 42,
    "name": "Crawl example.com",
    "host": "example.com",
    "status": "crawling",
    "score": null,
    "pages_found": 120,
    "pages_processed": 75,
    "errors_count": 2,
    "warnings_count": 18,
    "started_datetime": "2026-06-13 09:14:02",
    "finished_datetime": null,
    "datetime": "2026-06-13 09:14:01",
    "pending": 45
  }
}

Errors

Status Condition
404 Scan not found or not owned by the authenticated user.

GET /spider/{id}/issues

Aggregated issue breakdown across all crawled pages of a scan. Counts each issue code's occurrences and rolls them up by severity.

Path parameters

Name Type Description
id int Scan id. Must belong to the authenticated user.

Response fields

Field Type Description
by_severity object Totals per severity: { "major": int, "moderate": int, "minor": int }. Counts total occurrences across pages.
issues array One entry per distinct issue code, sorted by pages descending.

Each issues[] entry:

Field Type Description
code string Issue code (e.g. title_missing, meta_desc_missing, images_no_alt).
severity string major, moderate, or minor (codes not in the known map default to minor).
pages int Number of pages affected by this issue code.

Issue codes by severity (from SpiderService::SEVERITY)

Note: the API response does not include a per-issue fix field — only code, severity, and pages.

curl

curl -s "https://seo.example.com/api/seo/spider/42/issues" \
  -H "Authorization: Bearer <api_key>"

Example response

{
  "data": {
    "by_severity": {
      "major": 11,
      "moderate": 53,
      "minor": 128
    },
    "issues": [
      { "code": "images_no_alt",     "severity": "minor",    "pages": 64 },
      { "code": "meta_desc_missing", "severity": "moderate", "pages": 30 },
      { "code": "title_missing",     "severity": "major",    "pages": 6 }
    ]
  }
}

Errors

Status Condition
404 Scan not found or not owned by the authenticated user.

GET /spider/{id}/pages

List the crawled pages of a scan, paginated, ordered by page_id ascending.

Path parameters

Name Type Description
id int Scan id. Must belong to the authenticated user.

Query parameters

Name Type Default Description
page int 1 Page number.
per_page int 25 Results per page (max 100).
status int Filter by exact HTTP status code (e.g. 404).
issues any (truthy) If present/non-empty, return only pages that have at least one issue.
format string Set to csv to download the list as CSV (up to 5000 rows).

Per-page fields

Field Type Description
id int Page id.
url string Page URL.
status_code int HTTP status code (0 if the fetch failed).
score int | null Page SEO score, null if not computed.
title string | null <title> text.
meta_description string | null Meta description.
h1 string | null First H1 text.
canonical string | null Canonical URL.
is_indexable bool Whether the page is indexable.
is_noindex bool Whether the page has a noindex directive.
words_count int Word count of the page body.
images_no_alt int Number of images missing alt.
internal_links int Internal links count.
external_links int External links count.
schema_types string | null Detected schema.org types (raw stored value, e.g. comma-separated).
issues array Issue codes for this page (decoded from stored JSON; [] if none).

Note the JSON key names differ from the underlying columns: internal_links_count is returned as internal_links and external_links_count as external_links.

curl

# All pages
curl -s "https://seo.example.com/api/seo/spider/42/pages?page=1&per_page=50" \
  -H "Authorization: Bearer <api_key>"

# Only pages with issues
curl -s "https://seo.example.com/api/seo/spider/42/pages?issues=1" \
  -H "Authorization: Bearer <api_key>"

# Only 404 pages
curl -s "https://seo.example.com/api/seo/spider/42/pages?status=404" \
  -H "Authorization: Bearer <api_key>"

# CSV export
curl -s "https://seo.example.com/api/seo/spider/42/pages?format=csv" \
  -H "Authorization: Bearer <api_key>" -o spider_pages.csv

Example response

{
  "data": [
    {
      "id": 1501,
      "url": "https://example.com/services/",
      "status_code": 200,
      "score": 82,
      "title": "Services - My Website",
      "meta_description": "I nostri servizi di sviluppo e SEO.",
      "h1": "I nostri servizi",
      "canonical": "https://example.com/services/",
      "is_indexable": true,
      "is_noindex": false,
      "words_count": 642,
      "images_no_alt": 2,
      "internal_links": 38,
      "external_links": 4,
      "schema_types": "Organization,WebPage",
      "issues": ["images_no_alt", "meta_desc_long"]
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 50,
    "total_results": 1,
    "total_pages": 1
  }
}

Errors

Status Condition
404 Scan not found or not owned by the authenticated user.

POST /spider

Start a new crawl. Returns immediately with the new scan id; crawling proceeds asynchronously (driven by cron + live polling), so poll GET /spider/{id} to track progress.

Request body (JSON)

Field Type Required Default Description
start_url string yes* URL to start crawling from. *Either start_url or url must be provided and non-empty.
url string yes* Alias accepted if start_url is absent.
name string no "" Friendly name for the scan.
max_pages int no plan limit (fallback 200) Max pages to crawl. Capped to the plan limit when set, then hard-capped server-side to 110000.
max_depth int no 10 Max crawl depth. Clamped server-side to 050.
speed string no fast One of safe, fast, turbo (courtesy delay between pages: 4 / 2 / 1 s respectively). Any other value falls back to fast at the API layer.
include_subdomains bool no false Crawl subdomains of the host as well.
respect_robots bool no false Honor robots.txt rules.
use_sitemap bool no false Seed the queue from the site's sitemap.
exclude_paths string no "" Path-exclusion rules (newline/glob style). Truncated to 2000 chars at the API layer (2048 in storage).

Success response — HTTP 201:

Field Type Description
id int New scan id.
status string Always crawling on creation.

curl

curl -s -X POST "https://seo.example.com/api/seo/spider" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Crawl example.com",
    "start_url": "https://example.com/",
    "max_pages": 500,
    "max_depth": 10,
    "speed": "fast",
    "include_subdomains": false,
    "respect_robots": true,
    "use_sitemap": true,
    "exclude_paths": "/wp-admin/\n/cart/"
  }'

Example response

{
  "data": {
    "id": 43,
    "status": "crawling"
  }
}

Errors

Status Condition
422 start_url (and url) missing/empty — "Il campo start_url è obbligatorio."
422 The scan could not be created/started (service error, or "Impossibile avviare la scansione.").

POST /spider/{id}/rescan

Re-crawls the same scan in place: the scan_id is preserved and the previous crawl is kept as a history snapshot (so you can compare before/after). Sets the scan back to crawling.

Path: POST /api/seo/spider/{id}/rescan

Success response: data: { "id": <scan_id>, "status": "crawling" }.

curl -s -X POST "https://seo.example.com/api/seo/spider/43/rescan" \
  -H "Authorization: Bearer <api_key>"

Errors: 404 (not found/owned); 422 if the re-crawl could not be started.

PATCH /spider/{id} — rename a scan

Renames a scan. Only the name field is updatable.

Path: PATCH /api/seo/spider/{id}

Body params: name (required, non-empty, trimmed to 255 chars).

Success response: data: { "id": <scan_id>, "name": "<new name>" }.

curl -s -X PATCH "https://seo.example.com/api/seo/spider/43" \
  -H "Authorization: Bearer <api_key>" -H "Content-Type: application/json" \
  -d '{"name":"Homepage crawl — June"}'

Errors: 404 (not found/owned); 422 if name is missing or empty.

GET /spider/{id}/history — per-crawl snapshots

Returns the immutable history of every completed crawl of this scan (one snapshot per crawl), oldest first. Useful for trend charts and diffing.

Path: GET /api/seo/spider/{id}/history

Response data — array of snapshot objects:

Field Type Description
score int | null Overall score at that crawl.
pages_found / pages_processed int Page counts.
errors_count / warnings_count int Issue counts.
status_2xx / status_3xx / status_4xx / status_5xx int HTTP status-class tallies.
duration_seconds int Crawl duration.
started_datetime / finished_datetime / datetime string Timestamps.
curl -s "https://seo.example.com/api/seo/spider/43/history" \
  -H "Authorization: Bearer <api_key>"

Errors: 404 if the scan is not found/owned.

GET /spider/{id}/compare — diff two scans

Compares this scan against another of the caller's scans (matched page-by-page via URL hash) and reports what was added, removed, and changed.

Path: GET /api/seo/spider/{id}/compare?to={id2}

Query parameters

Param Type Notes
to int Required — the scan id to compare against (must be owned by the caller).

Response data

Field Type Description
from / to object Scan summaries for each side.
summary object pages_from, pages_to, added, removed, changed, score_delta.
added_pages array Pages present in to but not from (url, status_code, score). Capped at 500.
removed_pages array Pages present in from but not to (url, status_code). Capped at 500.
changed_pages array Pages whose status_code, score, issues_count, or is_indexable changed — each as { "from": …, "to": … }. Capped at 500.
curl -s "https://seo.example.com/api/seo/spider/43/compare?to=41" \
  -H "Authorization: Bearer <api_key>"

Errors: 422 if to is missing; 404 if either scan is not found/owned.

DELETE /spider/{id}

Delete a scan. Cascades to its crawled pages, queue, and link records.

Path parameters

Name Type Description
id int Scan id. Must belong to the authenticated user.

Success response

Field Type Description
id int The deleted scan id.

curl

curl -s -X DELETE "https://seo.example.com/api/seo/spider/43" \
  -H "Authorization: Bearer <api_key>"

Example response

{
  "data": {
    "id": 43
  }
}

Errors

Status Condition
404 Scan not found or not owned by the authenticated user.

REST endpoints for the Generative Engine Optimization (GEO) and AI-search modules of the suite. These five resources share a common implementation (resource_config() + generic_resource() + run_action()), so their request/response shapes follow the same pattern; only the table, fields and POST runner differ.

Resources at a glance

Resource DB table id column
geo GEO audits audit_id
ai-visibility AI visibility runs run_id
ai-engine AI engine tests test_id
ai-competitor AI competitor reports report_id
prompt-research Prompt research prompt_id

Common behaviour (all five resources)

These rules are enforced by the shared generic_resource() / cast_row() logic and apply identically to every resource below.

Scoping. All rows are scoped to the authenticated user (WHERE user_id = <uid>). The user_id column is stripped from single-item responses.

ID normalization (cast_row). The native id column (audit_id, run_id, test_id, report_id, prompt_id) is renamed to id and cast to integer in every response. The following columns, when present, are cast to integer: geo_score, score, prompts_total, cited_count, first_count, pages_count. has_llms_txt is cast to boolean. avg_position is cast to float.

GET /{resource} (list). Returns the list columns defined per resource (see each section), ordered by id column DESC, paginated.

GET /{resource}/{id} (single item). Returns the full DB row (all columns), with id normalization applied and user_id removed. Any of these JSON payload columns, when present and stored as a string, are decoded into objects before being returned: result, checks, recommendations, ai_crawlers, competitors. Returns 404 if no matching row for the user.

POST /{resource} (create / run). There is no plain insert — a POST to the collection triggers the resource's analysis runner (run_action), which is the costly operation (OpenAI / DataForSEO). On success it returns the freshly produced result with HTTP 201. Each runner is rate-limited to 30 runs/hour per user (429 when exceeded). Validation failures return 422; analysis failures return 502.

DELETE /{resource}/{id}. Not supported for these five resources — generic_resource() only handles GET (list / single) and POST (run). A DELETE will not match and is not routed here.


GEO — Generative Engine Optimization audit

geo audits a URL for how well it is optimized for generative/AI engines (LLM-friendliness): page count, presence of an llms.txt, and a computed GEO score.

Resource id: audit_id

GET /geo — list

List columns: audit_id (→ id), host, url, geo_score, has_llms_txt, pages_count, datetime.

curl -H "Authorization: Bearer <api_key>" \
  "https://seo.example.com/api/seo/geo?page=1&per_page=25"
{
  "data": [
    {
      "host": "example.com",
      "url": "https://example.com/",
      "geo_score": 72,
      "has_llms_txt": true,
      "pages_count": 18,
      "datetime": "2026-06-13 10:42:11",
      "id": 14
    }
  ]
}

GET /geo/{id} — single audit

Returns the full row; checks, recommendations and ai_crawlers JSON columns (when present) are decoded.

curl -H "Authorization: Bearer <api_key>" \
  "https://seo.example.com/api/seo/geo/14"

POST /geo — run an audit

Body params read by the runner:

Field Required Notes
url yes* The URL/domain to audit
domain Used as a fallback if url is absent

* At least one of url / domain must be a non-empty string, otherwise 422 (Il campo url è obbligatorio.).

Runs GeoAuditService::audit($url, $uid). Returns 201 with the audit result, or 502 (Audit non riuscito.) if the service returns null.

curl -X POST -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/"}' \
  "https://seo.example.com/api/seo/geo"

AI Visibility — brand presence across AI engines

ai-visibility measures how often and how prominently a brand is cited by AI engines across a set of prompts: citation counts, first-mention counts, average position and an overall score, per engine.

Resource id: run_id

GET /ai-visibility — list

List columns: run_id (→ id), brand, host, engine, score, prompts_total, cited_count, first_count, avg_position, datetime.

curl -H "Authorization: Bearer <api_key>" \
  "https://seo.example.com/api/seo/ai-visibility?per_page=50"
{
  "data": [
    {
      "brand": "Acme",
      "host": "acme.com",
      "engine": "openai",
      "score": 64,
      "prompts_total": 10,
      "cited_count": 6,
      "first_count": 2,
      "avg_position": 1.83,
      "datetime": "2026-06-13 09:15:00",
      "id": 7
    }
  ]
}

GET /ai-visibility/{id} — single run

Returns the full row; result JSON column (when present) is decoded.

POST /ai-visibility — run an analysis

Body params read by the runner:

Field Required Notes
brand yes Non-empty string, else 422 (Il campo brand è obbligatorio.)
domain no Brand domain (passed to the service)
prompts no Array of prompts; ignored unless it is a non-empty array
topic no Topic string

Runs AiVisibilityService::analyze($brand, $domain, $prompts, $uid, $topic). Returns 201 with the result, or 502 with the service error message ($svc->error or Analisi non riuscita.).

curl -X POST -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
        "brand": "Acme",
        "domain": "acme.com",
        "topic": "industrial pumps",
        "prompts": ["best industrial pump brands", "who makes reliable pumps"]
      }' \
  "https://seo.example.com/api/seo/ai-visibility"

AI Engine — keyword/URL AI-readiness test

ai-engine tests how a specific URL performs for a given keyword from an AI-engine perspective, producing a score.

Resource id: test_id

GET /ai-engine — list

List columns: test_id (→ id), keyword, url, score, datetime.

curl -H "Authorization: Bearer <api_key>" \
  "https://seo.example.com/api/seo/ai-engine"
{
  "data": [
    {
      "keyword": "industrial pumps",
      "url": "https://acme.com/pumps",
      "score": 58,
      "datetime": "2026-06-13 11:02:44",
      "id": 3
    }
  ]
}

GET /ai-engine/{id} — single test

Returns the full row; result JSON column (when present) is decoded.

POST /ai-engine — run a test

Body params read by the runner:

Field Required Notes
keyword yes Both keyword and url must be non-empty, else 422 (I campi keyword e url sono obbligatori.)
url yes The URL to test

Runs AiEngineService::analyze($keyword, $url, $uid). Returns 201 with the result, or 502 with the service error message.

curl -X POST -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"keyword":"industrial pumps","url":"https://acme.com/pumps"}' \
  "https://seo.example.com/api/seo/ai-engine"

AI Competitor — brand vs competitors AI report

ai-competitor generates a comparative report of a brand against one or more competitors as seen by AI engines.

Resource id: report_id

GET /ai-competitor — list

List columns: report_id (→ id), brand, competitors, datetime.

curl -H "Authorization: Bearer <api_key>" \
  "https://seo.example.com/api/seo/ai-competitor"
{
  "data": [
    {
      "brand": "Acme",
      "competitors": "Globex, Initech",
      "datetime": "2026-06-13 08:30:00",
      "id": 2
    }
  ]
}

GET /ai-competitor/{id} — single report

Returns the full row; competitors and result JSON columns (when present) are decoded.

POST /ai-competitor — run a report

Body params read by the runner:

Field Required Notes
brand yes The subject brand/host. host / main_host are accepted as aliases. Both brand and competitors must be non-empty, else 422 (I campi brand e competitors sono obbligatori.)
competitors yes Competitor list — a comma-separated string or a JSON array of names/hosts.
language no Output language (default Italian).
mode no Set to matrix to run the symmetric N-way analysis (see below).
symmetric no Truthy alternative to mode=matrix (also accepted as a ?symmetric=1 query param).

Default (single-subject) mode. Returns 201 with the analysis of brand measured against the given competitors, or 502 with the service error message. Rate limited to 30 runs/hour per user (429 when exceeded).

curl -X POST -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"brand":"Acme","competitors":"Globex, Initech","language":"English"}' \
  "https://seo.example.com/api/seo/ai-competitor"
Matrix mode — symmetric N-way analysis

With {"mode":"matrix"} (or "symmetric":true, or ?symmetric=1) every participant — the subject brand plus each competitor — is analysed in turn as the subject, then the runs are merged into a single symmetric report. This yields a comparable score for each brand rather than a one-directional "us vs. them" view. Rate-limiting and spend caps are enforced internally by the matrix runner (it is not double rate-limited).

curl -X POST -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"mode":"matrix","brand":"Acme","competitors":["Globex","Initech"],"language":"English"}' \
  "https://seo.example.com/api/seo/ai-competitor"

The matrix response is an object keyed on the participating brands. Its exact fields come from AiCompetitorService::analyze_matrix(), but it is shaped around a brands list (each brand with its aggregated score/metrics across all runs) and a pairwise matrix block (how each brand fares against each other brand). Example (abridged):

{
  "data": {
    "mode": "matrix",
    "brands": [
      { "brand": "Acme",   "score": 71, "cited": 12, "first": 4 },
      { "brand": "Globex", "score": 63, "cited": 9,  "first": 2 },
      { "brand": "Initech","score": 58, "cited": 7,  "first": 1 }
    ],
    "matrix": [
      { "subject": "Acme",   "vs": { "Globex": 0.6, "Initech": 0.7 } },
      { "subject": "Globex", "vs": { "Acme": 0.4,   "Initech": 0.55 } },
      { "subject": "Initech","vs": { "Acme": 0.3,   "Globex": 0.45 } }
    ]
  }
}

The concrete keys/values are produced by the service; treat the block above as the shape (a per-brand summary list + a pairwise comparison block), not a fixed contract.

On failure the endpoint returns 502, or 429 when the internal quota/spend limit is hit (the error message contains limite).


AI Prompt Research — prompt expansion / research

prompt-research expands a seed prompt into research output (e.g. related AI-search prompts/variations) for a given language.

Resource id: prompt_id

Note: Prompt Research uses its own cache-based hourly limiter (pr_rate_<uid>, 30/hour) instead of the shared table rate guard, and analyze() is called without a user id.

GET /prompt-research — list

List columns: prompt_id (→ id), prompt, language, datetime.

curl -H "Authorization: Bearer <api_key>" \
  "https://seo.example.com/api/seo/prompt-research"
{
  "data": [
    {
      "prompt": "best CRM for small business",
      "language": "en",
      "datetime": "2026-06-13 12:00:00",
      "id": 9
    }
  ]
}

GET /prompt-research/{id} — single item

Returns the full row; result JSON column (when present) is decoded.

POST /prompt-research — run research

Body params read by the runner:

Field Required Notes
prompt yes Non-empty string, else 422 (Il campo prompt è obbligatorio.)

Runs PromptResearchService::analyze($prompt). Returns 201 with the result, or 502 with the service error message. Rate limited to 30 runs/hour per user (429 when exceeded).

curl -X POST -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"best CRM for small business"}' \
  "https://seo.example.com/api/seo/prompt-research"

Search Console, Analytics & Alerts

Base URL: https://seo.example.com/api/seo Auth (all endpoints): Authorization: Bearer <api_key>

Endpoint Requires Google account connected?
/search-console Yes
/analytics Yes
/alerts No (reads local DB)

Google connection requirement (search-console & analytics)

Both /search-console and /analytics call google_client() before doing anything else. This returns errors (and stops the request) in three cases:

If a downstream Google API call fails after connection, the endpoint also returns 502 with the Google client error message.

Shared date window (range)

Both Google endpoints use the same window logic:


GET /search-console

Live Google Search Console performance for one verified site/property.

Sub-resource: list sites

GET /api/seo/search-console/sites

Returns a flat JSON array of the site URLs available to the connected Google account (the siteUrl values, e.g. sc-domain:example.com or https://example.com/). The site list is cached for 6 hours per user.

Main report

GET /api/seo/search-console
Query parameters
Param Required Description
site Conditional The GSC siteUrl to query. Must be one of the values from /search-console/sites. Optional only if the account has exactly one site, in which case it is auto-selected.
range No Window in days: 7, 28, or 90. Default 28. See window logic above.

Passing dimensions switches the endpoint into custom query mode (see below), which changes the response shape.

422 case

If site is missing (and the account does not have exactly one site to auto-select) or is not a valid site for the account:

Response (data object)
Field Type Description
site string The resolved siteUrl.
range_days int Window size (7 / 28 / 90).
start string Start date YYYY-MM-DD (inclusive).
end string End date YYYY-MM-DD (inclusive).
totals object Aggregate metrics for the window (see below).
queries array Top 25 search queries (row shape below).
pages array Top 25 pages (row shape below).

totals object:

Field Type Description
clicks int Total clicks.
impressions int Total impressions.
ctr float Click-through rate as a percentage, rounded to 2 decimals (raw GSC ratio × 100).
position float Average position, rounded to 1 decimal.

Each row in queries and pages (GSC row shape):

Field Type Description
key string The query text (for queries) or page URL (for pages).
clicks int Clicks for that key.
impressions int Impressions for that key.
ctr float CTR as a percentage, rounded to 2 decimals.
position float Average position, rounded to 1 decimal.

The full report bundle is cached for 3 hours per (user, site, start, end).

curl
curl -s "https://seo.example.com/api/seo/search-console?site=sc-domain:example.com&range=28" \
  -H "Authorization: Bearer <api_key>"
Example response
{
  "data": {
    "site": "sc-domain:example.com",
    "range_days": 28,
    "start": "2026-06-01",
    "end": "2026-06-28",
    "totals": {
      "clicks": 1234,
      "impressions": 56789,
      "ctr": 2.17,
      "position": 14.3
    },
    "queries": [
      { "key": "esempio keyword", "clicks": 210, "impressions": 4300, "ctr": 4.88, "position": 6.2 },
      { "key": "altra query",     "clicks": 95,  "impressions": 2100, "ctr": 4.52, "position": 9.8 }
    ],
    "pages": [
      { "key": "https://example.com/", "clicks": 540, "impressions": 12000, "ctr": 4.5, "position": 7.1 }
    ]
  }
}
Custom query mode (dimensions)

When dimensions is supplied, the endpoint runs an arbitrary Search Analytics query instead of the default bundle and returns a flat rows list.

Param Notes
dimensions Required to enable this mode. CSV of: query, page, country, device, date, searchAppearance. Unknown values are dropped; if none remain → 422.
row_limit Max rows, clamped to 1–1000. Default 100.
search_type One of web, image, video, news, discover, googleNews. Default web.
query_filter Only rows whose query contains this string.
page_filter Only rows whose page contains this string.
country Filter by country (ISO code, equals).
device Filter by device (DESKTOP / MOBILE / TABLET, equals).

Response data (custom mode): site, range_days, dimensions (the applied list), search_type, and rows — each row { "keys": [...], "clicks": int, "impressions": int, "ctr": float, "position": float } where keys aligns with the requested dimensions.

curl -s "https://seo.example.com/api/seo/search-console?site=sc-domain:example.com&dimensions=query,page&query_filter=seo&row_limit=100" \
  -H "Authorization: Bearer <api_key>"

GET /analytics

Live Google Analytics 4 (GA4) report for one property.

Sub-resource: list properties

GET /api/seo/analytics/properties

Returns a JSON array of the raw GA4 property objects available to the connected account (each contains a property field like properties/123456789). Cached for 6 hours per user.

Main report

GET /api/seo/analytics
Query parameters
Param Required Description
property Conditional GA4 property id. Non-digit characters are stripped, so both properties/123456789 and 123456789 are accepted; the numeric id must match one of the account's properties. Optional only if the account has exactly one property, in which case it is auto-selected.
range No Window in days: 7, 28, or 90. Default 28. See window logic above.
422 case

If property is missing (and there isn't exactly one property to auto-select) or does not match a property of the account:

Response (data object)
Field Type Description
property string The resolved numeric property id.
range_days int Window size (7 / 28 / 90).
start string Start date YYYY-MM-DD (inclusive).
end string End date YYYY-MM-DD (inclusive).
totals object Aggregate GA4 metrics (see below).
top_pages array Top 15 pages by page views (see below).

totals object:

Field Type Description
sessions int Sessions (sessions).
active_users int Active users (activeUsers).
new_users int New users (newUsers).
page_views int Page views (screenPageViews).
engagement_rate float Engagement rate as a percentage, rounded to 1 decimal (engagementRate × 100).
avg_session_duration float Average session duration in seconds, rounded to 1 decimal (averageSessionDuration).

Each row in top_pages:

Field Type Description
page string The page path (pagePath dimension).
views int Page views for that path.

The bundle is cached for 3 hours per (user, property, start, end).

curl
curl -s "https://seo.example.com/api/seo/analytics?property=123456789&range=7" \
  -H "Authorization: Bearer <api_key>"
Example response
{
  "data": {
    "property": "123456789",
    "range_days": 7,
    "start": "2026-06-22",
    "end": "2026-06-28",
    "totals": {
      "sessions": 4821,
      "active_users": 3990,
      "new_users": 2700,
      "page_views": 11230,
      "engagement_rate": 62.4,
      "avg_session_duration": 95.7
    },
    "top_pages": [
      { "page": "/", "views": 3120 },
      { "page": "/blog/articolo", "views": 880 }
    ]
  }
}
Custom report mode (metrics)

When metrics is supplied, the endpoint runs an arbitrary GA4 report instead of the default bundle. Metric and dimension names are whitelist-validated.

Param Notes
metrics Required to enable this mode. CSV from: sessions, activeUsers, newUsers, totalUsers, screenPageViews, engagementRate, averageSessionDuration, bounceRate, eventCount, conversions, screenPageViewsPerSession, sessionsPerUser. Unknown values are dropped; if none remain → 422.
dimensions Optional CSV from: pagePath, pageTitle, landingPage, country, city, region, deviceCategory, browser, operatingSystem, sessionSource, sessionMedium, sessionSourceMedium, date, language.
order_by Metric to sort by (must be one of the requested metrics). Defaults to the first metric when dimensions are present.
limit Max rows, clamped to 1–1000. Default 100 (applied only when dimensions are present).

Response data (custom mode): property, range_days, metrics (applied list), dimensions (applied list), and rows — each row is a flat object with one key per requested dimension (string) and one key per requested metric (number).

curl -s "https://seo.example.com/api/seo/analytics?property=123456789&metrics=sessions,activeUsers&dimensions=country&order_by=sessions&limit=20" \
  -H "Authorization: Bearer <api_key>"

/alerts

Local SEO alerts for the authenticated user. Does not require a Google connection.

GET /alerts — list alerts

GET /api/seo/alerts
Query parameters
Param Required Description
is_read No Filter by read state. Cast to boolean → 0 (unread) or 1 (read). Omit to return all.
page No Page number, min 1. Default 1.
per_page No Items per page, clamped to 1100. Default 25.
format No csv to export the full set (capped at 5000 rows) as a CSV download instead of paginated JSON.

Results are ordered by datetime descending.

Response — one object per alert
Field Type Description
id int Alert id (alert_id).
type string Alert type.
severity string Alert severity.
message string Human-readable message.
is_read bool Whether the alert has been marked read.
datetime string Timestamp the alert was created.
Pagination (meta)

List responses include a meta block:

"meta": {
  "page": 1,
  "per_page": 25,
  "total_results": 137,
  "total_pages": 6
}
curl
curl -s "https://seo.example.com/api/seo/alerts?is_read=0&page=1&per_page=25" \
  -H "Authorization: Bearer <api_key>"

CSV export:

curl -s "https://seo.example.com/api/seo/alerts?format=csv" \
  -H "Authorization: Bearer <api_key>" -o alerts.csv
Example response
{
  "data": [
    {
      "id": 482,
      "type": "ranking_drop",
      "severity": "high",
      "message": "La keyword \"esempio\" e scesa di 12 posizioni.",
      "is_read": false,
      "datetime": "2026-06-29 08:14:02"
    },
    {
      "id": 481,
      "type": "new_backlink",
      "severity": "info",
      "message": "Rilevato un nuovo backlink da example.org.",
      "is_read": true,
      "datetime": "2026-06-28 22:01:11"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 25,
    "total_results": 137,
    "total_pages": 6
  }
}

PATCH /alerts — mark read / unread

Mark a single alert, or all of the user's alerts, as read or unread.

PATCH /api/seo/alerts/{id}    # one alert
PATCH /api/seo/alerts         # all alerts for the user
Request body (JSON)
Field Type Description
is_read bool Target read state. Cast to 0/1. Defaults to 1 (read) if omitted.
Responses
curl
# mark one alert read
curl -s -X PATCH "https://seo.example.com/api/seo/alerts/482" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"is_read": true}'

# mark all alerts read
curl -s -X PATCH "https://seo.example.com/api/seo/alerts" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"is_read": true}'

Documentazione dell'API REST della SEO API.


Novità — endpoint aggiunti/estesi (aggiornamento 2026-07)

Questa release aggiunge nuovi endpoint e parametri all'API. Autenticazione e convenzioni invariate.

Account — rotazione API key

Lookup location & lingue

Keyword — nuove operazioni e campi

Progetti

SEO Spider

Search Console — filtri avanzati

GET /api/seo/search-console?site=… accetta ora (modalità custom):

Analytics (GA4) — metriche/dimensioni flessibili

GET /api/seo/analytics?property=… accetta ora (modalità custom):

Alert su canali

Gli alert generati (rank drop, spider 5xx, SSL in scadenza, blocco crawler AI, ecc.) vengono ora inviati anche ai canali configurati dall'utente (webhook, Slack, Discord, Telegram, …) tramite i Notification Handler del sistema, oltre all'email.