GTG-SEO — Documentazione API REST
Prodotto: Suite SEO GTG (SEO G Tech Group) · Versione API: v1
Base URL: https://seo.gtechgroup.it/api/seo (alias versionato: https://seo.gtechgroup.it/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 GTG-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
- Introduzione e convenzioni
- Base URL
- Autenticazione
- Rate limiting
- Convenzioni (paginazione, CSV, wrapping)
- Codici di errore standard
GET /statusGET /— Discovery- Rank Tracker — Progetti, Keyword, Keyword Research
- SEO Spider
- GEO & AI Search
- Search Console, Analytics & Alerts
Introduction
The GTG-SEO REST API exposes the GTG-SEO suite over HTTP/JSON. A single controller
(GtgSeoApi) sub-routes on the path segments after seo, giving programmatic access to:
- Rank Tracker —
projectsand their trackedkeywords(positions + history). - SEO Spider — site crawls (
spider) and the crawledpages. - GEO / AI —
geo(GEO audits),ai-visibility,ai-engine,ai-competitor, andprompt-research. - Keyword Research —
keyword-research(DataForSEO-backed). - Search Console & Analytics —
search-consoleandanalytics(live Google GSC + GA4 data). - Alerts —
alerts.
The discovery index (GET /) returns "name": "SEO API", "version": "v1".
Base URL
https://seo.gtechgroup.it/api/seo
A versioned alias is also registered and served by the same controller:
https://seo.gtechgroup.it/api/v1/seo
Both routes (seo and v1) are mapped to GtgSeoApi. 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:
- The API must be enabled globally by the administrator.
- 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.gtechgroup.it/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:
Content-Type: text/csv; charset=UTF-8Content-Disposition: attachment; filename="<name>.csv"
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.gtechgroup.it/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.gtechgroup.it/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>/gtg-seo/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.gtechgroup.it/api/seo \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"name": "SEO API",
"version": "v1",
"base_url": "https://seo.gtechgroup.it/api/seo",
"auth": "Authorization: Bearer <api_key>",
"docs": "https://seo.gtechgroup.it/gtg-seo/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."
}
}
Rank Tracker (Projects, Keywords, Keyword Research)
REST endpoints for managing rank-tracking projects, their tracked keywords, and ad-hoc keyword research.
- Base URL:
https://seo.gtechgroup.it/api/seo - Versioned alias: the same controller also answers under
/api/v1/seo/...(the leadingseo/version segment is stripped before routing). - Authentication: every request requires
Authorization: Bearer <api_key>. The key is matched against an active user (users.api_key,status = 1); the user must also have a plan with API access enabled. - Content type for writes: the body reader accepts either form-encoded (
$_POST) data or a raw JSON body. JSON examples are used throughout (Content-Type: application/json).
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.gtechgroup.it/api/seo/projects?page=1&per_page=25" \
-H "Authorization: Bearer <api_key>"
{
"data": [
{
"id": 12,
"name": "G Tech Group",
"domain": "gtechgroup.it",
"location_code": 2380,
"language_code": "it",
"keywords": 34,
"gsc_property": "sc-domain:gtechgroup.it",
"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.
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.gtechgroup.it/api/seo/projects/12" \
-H "Authorization: Bearer <api_key>"
{
"data": {
"id": 12,
"name": "G Tech Group",
"domain": "gtechgroup.it",
"location_code": 2380,
"language_code": "it",
"gsc_property": "sc-domain:gtechgroup.it",
"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://gtechgroup.it/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 (via gtg_seo_normalize_domain). 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.gtechgroup.it/api/seo/projects" \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{ "name": "G Tech Group", "domain": "gtechgroup.it", "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 via gtg_seo_normalize_domain. |
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.gtechgroup.it/api/seo/projects/12" \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{ "name": "G Tech Group SRLS", "gsc_property": "sc-domain:gtechgroup.it" }'
{ "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. |
If keywords is a non-empty array it is used; otherwise keyword is split. At least one must yield a keyword.
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). |
Example (array form)
curl -s -X POST "https://seo.gtechgroup.it/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 } }
Example (newline string form)
curl -s -X POST "https://seo.gtechgroup.it/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.gtechgroup.it/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.gtechgroup.it/api/seo/keywords/101/history?days=30" \
-H "Authorization: Bearer <api_key>"
{
"data": {
"keyword": "agenzia seo trento",
"history": [
{ "position": 9, "url": "https://gtechgroup.it/seo", "datetime": "2026-06-10 03:10:00" },
{ "position": 5, "url": "https://gtechgroup.it/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.gtechgroup.it/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.
Note: the
/keywordsresource only supportsGET(history) andDELETE. There is no separate "list keywords" endpoint — keywords are listed insideGET /projects/{id}.
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.gtechgroup.it/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.gtechgroup.it/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.gtechgroup.it/api/seo
Authentication: all requests require a Bearer token.
Authorization: Bearer <api_key>
Conventions
- List endpoints are paginated with
?page=(default1) and?per_page=(default25, max100). - List endpoints also support CSV export with
?format=csv(returnstext/csvas a file download; up to 5000 rows in a single shot, no pagination). - Rate limit: 60 requests/minute. CORS is enabled.
- Successful single-resource responses use a JSON:API style envelope:
{ "data": { ... } }. - Successful list responses use
{ "data": [ ... ], "meta": { page, per_page, total_results, total_pages } }.
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.gtechgroup.it/api/seo/spider?page=1&per_page=25" \
-H "Authorization: Bearer <api_key>"
Example response
{
"data": [
{
"id": 42,
"name": "Crawl gtechgroup.it",
"host": "gtechgroup.it",
"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.gtechgroup.it/api/seo/spider/42" \
-H "Authorization: Bearer <api_key>"
Example response (in progress)
{
"data": {
"id": 42,
"name": "Crawl gtechgroup.it",
"host": "gtechgroup.it",
"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)
- major:
status_4xx,status_5xx,title_missing,h1_missing,h1_multiple,not_https,fetch_failed - moderate:
meta_desc_missing,page_heavy,response_slow,no_viewport,low_words,mixed_content,compression_off,canonical_missing,status_3xx,redirect_chain,dom_too_large,render_blocking - minor:
title_short,title_long,meta_desc_long,images_no_alt,url_too_long,url_params,url_uppercase,url_http,no_schema,no_og,blocked_robots,nofollow,no_doctype,lang_missing,charset_missing,no_favicon,deprecated_html,no_twitter,noindex,email_exposed,no_hsts,no_security_headers
Note: the API response does not include a per-issue
fixfield — onlycode,severity, andpages.
curl
curl -s "https://seo.gtechgroup.it/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_countis returned asinternal_linksandexternal_links_countasexternal_links.
curl
# All pages
curl -s "https://seo.gtechgroup.it/api/seo/spider/42/pages?page=1&per_page=50" \
-H "Authorization: Bearer <api_key>"
# Only pages with issues
curl -s "https://seo.gtechgroup.it/api/seo/spider/42/pages?issues=1" \
-H "Authorization: Bearer <api_key>"
# Only 404 pages
curl -s "https://seo.gtechgroup.it/api/seo/spider/42/pages?status=404" \
-H "Authorization: Bearer <api_key>"
# CSV export
curl -s "https://seo.gtechgroup.it/api/seo/spider/42/pages?format=csv" \
-H "Authorization: Bearer <api_key>" -o spider_pages.csv
Example response
{
"data": [
{
"id": 1501,
"url": "https://gtechgroup.it/servizi/",
"status_code": 200,
"score": 82,
"title": "Servizi - G Tech Group",
"meta_description": "I nostri servizi di sviluppo e SEO.",
"h1": "I nostri servizi",
"canonical": "https://gtechgroup.it/servizi/",
"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 (gtg_seo_spider_pages_limit, fallback 200) |
Max pages to crawl. Capped to the plan limit when set, then hard-capped server-side to 1–10000. |
max_depth |
int | no | 10 |
Max crawl depth. Clamped server-side to 0–50. |
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.gtechgroup.it/api/seo/spider" \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{
"name": "Crawl gtechgroup.it",
"start_url": "https://gtechgroup.it/",
"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."). |
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.gtechgroup.it/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. |
GEO & AI Search
REST endpoints for the Generative Engine Optimization (GEO) and AI-search modules of the GTG-SEO 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.
- Base URL:
https://seo.gtechgroup.it/api/seo - Auth:
Authorization: Bearer <api_key>(required on every request) - Content-Type for POST bodies:
application/json(form-encoded$_POSTis also accepted)
Resources at a glance
| Resource | DB table | id column |
|---|---|---|
geo |
gtg_seo_geo_audits |
audit_id |
ai-visibility |
gtg_seo_ai_visibility |
run_id |
ai-engine |
gtg_seo_ai_engine_tests |
test_id |
ai-competitor |
gtg_seo_ai_competitor |
report_id |
prompt-research |
gtg_seo_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.
- Query params:
?page(default1),?per_page(default25, clamped to1–100). ?format=csvexports the whole set (capped at 5000 rows) as a CSV download instead of a JSON page.
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.
Table: gtg_seo_geo_audits · 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.gtechgroup.it/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.gtechgroup.it/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.gtechgroup.it/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.
Table: gtg_seo_ai_visibility · 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.gtechgroup.it/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.gtechgroup.it/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.
Table: gtg_seo_ai_engine_tests · id: test_id
GET /ai-engine — list
List columns: test_id (→ id), keyword, url, score, datetime.
curl -H "Authorization: Bearer <api_key>" \
"https://seo.gtechgroup.it/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.gtechgroup.it/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.
Table: gtg_seo_ai_competitor · id: report_id
GET /ai-competitor — list
List columns: report_id (→ id), brand, competitors, datetime.
curl -H "Authorization: Bearer <api_key>" \
"https://seo.gtechgroup.it/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 | Both brand and competitors must be non-empty, else 422 (I campi brand e competitors sono obbligatori.) |
competitors |
yes | Competitor list (string) |
Runs AiCompetitorService::analyze($brand, $competitors, $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 '{"brand":"Acme","competitors":"Globex, Initech"}' \
"https://seo.gtechgroup.it/api/seo/ai-competitor"
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.
Table: gtg_seo_prompt_research · 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, andanalyze()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.gtechgroup.it/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.gtechgroup.it/api/seo/prompt-research"
Search Console, Analytics & Alerts
Base URL: https://seo.gtechgroup.it/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:
409—Integrazione Google non configurata dall'amministratore.— the Google OAuth integration has not been configured at the admin level (gtg_seo_google_is_configured()is false).409—Account Google non collegato. Collegalo dal pannello (Search Console).— the authenticated user has no row ingtg_seo_google_oauth(their account is not linked).502— token could not be obtained / refresh error (the client error message is returned, e.g.Account Google non collegato.).
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:
- Optional query param
range= number of days. Allowed values:7,28,90. Any other / missing value defaults to28. - Google data lags ~2 days, so the window is shifted back by 2 days and is inclusive:
end=today − 2 daysstart=today − (2 + range − 1) days
- The response echoes
range_days,start, andend.
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. |
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:
422—Parametro "site" mancante o non valido. Vedi GET /api/seo/search-console/sites.
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.gtechgroup.it/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 }
]
}
}
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:
422—Parametro "property" mancante o non valido. Vedi GET /api/seo/analytics/properties.
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.gtechgroup.it/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 }
]
}
}
/alerts
Local SEO alerts for the authenticated user. Does not require a Google connection — reads from the gtg_seo_alerts table.
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 1–100. 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.gtechgroup.it/api/seo/alerts?is_read=0&page=1&per_page=25" \
-H "Authorization: Bearer <api_key>"
CSV export:
curl -s "https://seo.gtechgroup.it/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
- Single alert: if
{id}does not belong to the user, returns404. On success returns{ "id": <id>, "is_read": <bool> }. - All alerts (no id): returns
{ "updated": "all", "is_read": <bool> }.
curl
# mark one alert read
curl -s -X PATCH "https://seo.gtechgroup.it/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.gtechgroup.it/api/seo/alerts" \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{"is_read": true}'
Documentazione generata da analisi diretta del codice sorgente (app/controllers/api/GtgSeoApi.php). © G Tech Group S.R.L.S.
Novità — endpoint aggiunti/estesi (aggiornamento 2026-07)
Questa release aggiunge nuovi endpoint e parametri all'API. Autenticazione e convenzioni invariate.
Account — rotazione API key
GET /api/seo/api-key→ chiave mascherata ({"api_key_masked":"062d••…••ab"}).POST /api/seo/api-key/rotate→ genera una nuova API key e la restituisce ({"api_key":"…"}). La vecchia chiave smette immediatamente di funzionare (401).
Lookup location & lingue
GET /api/seo/locations?q=<testo>&country=<ISO2>&limit=100→ location Google. Senzacountry= elenco Paesi; concountry=IT= città/regioni di quel Paese (es. Milano). Richiede DataForSEO (409 altrimenti).GET /api/seo/languages?q=<testo>→ elenco codici lingua supportati (code,name).
Keyword — nuove operazioni e campi
GET /api/seo/keywords→ tutte le keyword tracciate dei progetti dell'utente (paginato; filtri?project_id=,?tag=;?format=csv).GET /api/seo/keywords/{id}→ dettaglio di una singola keyword.PATCH /api/seo/keywords/{id}→ aggiorna solo i campi organizzativi:target_url,tags(array o stringa CSV),notes. (Il testo della keyword NON è modificabile.)DELETE /api/seo/keywordscon body{"ids":[1,2,3]}→ eliminazione massiva (una sola chiamata).- Nuovi campi in tutte le risposte keyword:
target_url,tags(array),notes.
Progetti
GET /api/seo/projects/{id}?format=csv→ export CSV delle keyword del progetto (una riga per keyword).POST /api/seo/projects/{id}/keywordscon{"mode":"replace", "keywords":[…]}→ sostituisce l'intero set di keyword (risposta{added, removed, skipped}). Senzamode, comportamento invariato (append + dedup).
SEO Spider
POST /api/seo/spider/{id}/rescan→ ri-scansiona lo stesso dominio mantenendoscan_ide lo storico (per confronto pre/post).GET /api/seo/spider/{id}/history→ snapshot immutabili di ogni crawl completato (score, pagine, errori/warning, codici stato, durata).PATCH /api/seo/spider/{id}con{"name":"…"}→ rinomina una scansione.GET /api/seo/spider/{id}/compare?to={id2}→ confronto tra due scansioni dell'utente:summary(pagine nuove/perse/cambiate,score_delta) + listeadded_pages,removed_pages,changed_pages(delta status/score/issues/indicizzabilità per URL).
Search Console — filtri avanzati
GET /api/seo/search-console?site=… accetta ora (modalità custom):
dimensions(CSV tra:query,page,country,device,date,searchAppearance)search_type(web,image,video,news,discover,googleNews, defaultweb)query_filter,page_filter(contiene),country(ISO),device(DESKTOP/MOBILE/TABLET)row_limit(max 1000) Risposta:rows[]conkeys[]+clicks/impressions/ctr/position. Senzadimensions, resta il bundle predefinito (totali + top query + top pagine).
Analytics (GA4) — metriche/dimensioni flessibili
GET /api/seo/analytics?property=… accetta ora (modalità custom):
metrics(CSV, whitelist:sessions,activeUsers,newUsers,totalUsers,screenPageViews,engagementRate,averageSessionDuration,bounceRate,eventCount,conversions,…)dimensions(CSV, whitelist:pagePath,pageTitle,landingPage,country,city,region,deviceCategory,browser,operatingSystem,sessionSource,sessionMedium,sessionSourceMedium,date,language)order_by(una delle metrics),limit(max 1000) Senzametrics, resta il bundle predefinito.
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.