{"openapi": "3.1.0", "info": {"title": "Miraqo API", "version": "1", "description": "Stessi strumenti del connettore MCP, su REST. Auth: Bearer token (Impostazioni Account → Connettore AI)."}, "servers": [{"url": "https://app.miraqo.io"}], "security": [{"bearerAuth": []}], "components": {"securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer"}}}, "paths": {"/api/v1/list_projects": {"post": {"operationId": "list_projects", "summary": "list_projects", "description": "Elenca i progetti SEO visibili al token, con l'ultimo audit (id, stato, data, punteggio). Punto di partenza per ottenere i project_id usati dagli altri strumenti.", "requestBody": {"required": false, "content": {"application/json": {"schema": {"type": "object", "properties": {}, "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_audit_overview": {"post": {"operationId": "get_audit_overview", "summary": "get_audit_overview", "description": "Riepilogo dell'ultimo SEO audit completato di un progetto: punteggio, pagine, errori e disponibilità della topical architecture (strutturale e rifinita AI).", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_topical_architecture": {"post": {"operationId": "get_topical_architecture", "summary": "get_topical_architecture", "description": "Topical architecture dell'ultimo audit: summary, cluster/silo con pillar (url, in/out link, word_count), sorgente dei silo (menu o link), diagnostico silo_assign e conteggi anomalie. view: auto (default, AI se disponibile) | structural | ai.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer"}, "view": {"type": "string", "enum": ["auto", "structural", "ai"]}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_cluster_pages": {"post": {"operationId": "get_cluster_pages", "summary": "get_cluster_pages", "description": "Pagine di un cluster/silo (o di tutto il sito) ordinate per in-link, con i campi diagnostici: word_count, silo_signal, is_taxonomy, is_product, depth, scci. Paginato (max 100).", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer"}, "cluster": {"type": "string", "description": "ID cluster (da get_topical_architecture); omesso = tutte le pagine"}, "view": {"type": "string", "enum": ["auto", "structural", "ai"]}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}, "offset": {"type": "integer", "minimum": 0}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/find_pages": {"post": {"operationId": "find_pages", "summary": "find_pages", "description": "Cerca pagine per sottostringa in URL o titolo e ritorna i campi diagnostici del nodo (cluster, silo_signal, word_count, is_taxonomy, is_product…). Max 30 risultati.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer"}, "query": {"type": "string"}, "view": {"type": "string", "enum": ["auto", "structural", "ai"]}}, "required": ["project_id", "query"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_keywords": {"post": {"operationId": "get_keywords", "summary": "get_keywords", "description": "Keyword tracciate di un progetto con ultima posizione, posizione del check precedente e variazione (`change`: positivo = salita, negativo = scesa; fuori Top 100 conta 100), URL e SERP AI Overview, più summary (media posizioni, top 3/10). order_by=change mette per prime le più scese. Paginato (max 100).", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}, "query": {"type": "string", "description": "filtro sottostringa sulla keyword"}, "device": {"type": "string", "enum": ["desktop", "mobile"]}, "order_by": {"type": "string", "enum": ["change", "difficulty", "keyword", "position", "volume"]}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}, "offset": {"type": "integer", "minimum": 0}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_keyword_history": {"post": {"operationId": "get_keyword_history", "summary": "get_keyword_history", "description": "Storico posizioni di una keyword tracciata (fino a 365 giorni) con feature SERP: AI Overview, featured snippet, citazioni AIO.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}, "keyword": {"type": "string", "description": "testo esatto della keyword"}, "device": {"type": "string", "enum": ["desktop", "mobile"]}, "days": {"type": "integer", "minimum": 1, "maximum": 365}}, "required": ["project_id", "keyword"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_youtube_rankings": {"post": {"operationId": "get_youtube_rankings", "summary": "get_youtube_rankings", "description": "Posizioni del canale YouTube del progetto nella ricerca di YouTube per le keyword monitorate «anche su YouTube» (primi 20 video). Senza keyword: ultimo check di ogni keyword con la top 20 (canale, views, titolo). Con keyword: storico fino a 365 giorni. position null = nessun video del canale fra i primi 20.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}, "keyword": {"type": "string", "description": "testo esatto della keyword (opzionale: storico)"}, "days": {"type": "integer", "minimum": 1, "maximum": 365}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_competitors": {"post": {"operationId": "get_competitors", "summary": "get_competitors", "description": "Competitor monitorati del progetto: posizione media sulle keyword tracciate e conteggio del keyword gap (totale e non coperto).", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_keyword_gap": {"post": {"operationId": "get_keyword_gap", "summary": "get_keyword_gap", "description": "Keyword gap dai competitor: keyword per cui un competitor si posiziona, con volume, posizione, difficulty, cpc e URL. only_uncovered=true (default) mostra solo quelle che il progetto NON copre. Paginato (max 100).", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}, "competitor_id": {"type": "integer", "description": "ID da get_competitors; omesso = tutti"}, "only_uncovered": {"type": "boolean"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}, "offset": {"type": "integer", "minimum": 0}, "location_code": {"type": "integer", "description": "mercato dello snapshot gap; omesso = mercato del progetto. Altri mercati esistono solo se già analizzati dalla pagina Competitor."}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_backlinks": {"post": {"operationId": "get_backlinks", "summary": "get_backlinks", "description": "Backlink monitorati del progetto: totali dell'ultimo snapshot (nuovi/persi, referring domains, domain rank) e righe con anchor, dofollow, stato, spam score. Paginato (max 100).", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}, "status": {"type": "string", "enum": ["active", "lost", "broken", "noindex", "unknown"]}, "order_by": {"type": "string", "enum": ["domain_rank", "first_seen", "spam"]}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}, "offset": {"type": "integer", "minimum": 0}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_ai_visibility": {"post": {"operationId": "get_ai_visibility", "summary": "get_ai_visibility", "description": "Presenza AI del progetto. Senza prompt_id: prompt monitorati con ultimo risultato per fonte (menzione brand, citazione URL, Share of Voice) e peso crediti. Con prompt_id: storico dei risultati con citazioni e brand menzionati.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}, "prompt_id": {"type": "integer"}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_geo_audit": {"post": {"operationId": "get_geo_audit", "summary": "get_geo_audit", "description": "Ultimo GEO Audit completato del progetto: come ChatGPT, Gemini e Perplexity descrivono il brand (identità, associazioni, punti di forza e debolezze, distorsioni, sentiment per fonte, alternative, raccomandazioni). Sola lettura, zero crediti: l'audit si lancia dall'app. include_responses=true aggiunge le risposte grezze delle AI (lunghe). audit null = nessun audit completato.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}, "include_responses": {"type": "boolean"}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_audit_issues": {"post": {"operationId": "get_audit_issues", "summary": "get_audit_issues", "description": "Problemi dell'ultimo SEO audit completato: punteggio, pagine scansionate e contatori issue non a zero (404, title, duplicati, sicurezza, sitemap…).", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_audit_pages": {"post": {"operationId": "get_audit_pages", "summary": "get_audit_pages", "description": "Pagine dell'ultimo SEO audit con status, title, word count, tempi (load, LCP, TTI) e flag dei problemi. Filtrabile per issue e ordinabile. Paginato (max 100). `issue=\"errors\"` sono le pagine 4xx/5xx: i redirect NON compaiono qui (il crawler li segue e salva lo status finale) — per quelli usa get_audit_redirects.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}, "issue": {"type": "string", "enum": ["canonical", "errors", "images_without_alt", "missing_description", "missing_h1", "missing_title", "multiple_h1", "noindex", "slow"]}, "order_by": {"type": "string", "enum": ["lcp", "load_time", "status_code", "url", "word_count"]}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}, "offset": {"type": "integer", "minimum": 0}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_audit_redirects": {"post": {"operationId": "get_audit_redirects", "summary": "get_audit_redirects", "description": "Catene di redirect dell'ultimo audit (URL iniziale, hop, destinazione finale, status, loop) con `linked_from`: le pagine del sito che contengono il link al redirect, cioè dove si applica il fix. È il dettaglio dietro al contatore `errors_redirect` di get_audit_issues.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}, "offset": {"type": "integer", "minimum": 0}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_cwv_report": {"post": {"operationId": "get_cwv_report", "summary": "get_cwv_report", "description": "Report Core Web Vitals dell'ultimo audit: pagine campionate PSI (perf score, LCP, CLS, TBT, TTFB, dati di campo CrUX), ticket di sviluppo AI e le 10 pagine peggiori per LCP.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_gsc_performance": {"post": {"operationId": "get_gsc_performance", "summary": "get_gsc_performance", "description": "Performance Search Console del progetto aggregata per query o pagina (click, impression, CTR, posizione media), con totali del periodo. I dati sono pre-aggregati in finestre di 7, 28 o 90 giorni: un valore diverso viene arrotondato per eccesso e la risposta riporta in `days` la finestra EFFETTIVA (`days_requested` = quella chiesta). Qui non c'è storico giorno per giorno: per datare un calo usa get_gsc_daily.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}, "dimension": {"type": "string", "enum": ["query", "page"]}, "days": {"type": "integer", "minimum": 1, "maximum": 90}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_gsc_daily": {"post": {"operationId": "get_gsc_daily", "summary": "get_gsc_daily", "description": "Serie GIORNO PER GIORNO dei totali di sito da Search Console (click, impression, CTR, posizione media), fino a 90 giorni. È l'unico dato GSC con date reali: usalo per datare un calo o un picco, mentre get_gsc_performance dice QUALI query e pagine ma solo aggregate su finestre fisse. Totali di sito, non per pagina. `last_synced` dice fin dove arriva la serie: una coda a zero è quasi sempre un sync fermo, non un crollo.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer", "description": "ID progetto (da list_projects)"}, "days": {"type": "integer", "minimum": 1, "maximum": 90}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_domain_analysis": {"post": {"operationId": "get_domain_analysis", "summary": "get_domain_analysis", "description": "Snapshot dell'analisi della concorrenza di un dominio (authority, backlinks, keyword organiche, top keyword e pagine). Solo lettura della cache: per aggiornare usa run_domain_analysis. Senza location_code torna l'analisi più recente, qualunque mercato: il mercato effettivo è sempre nei campi location_code/language_code/market.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"domain": {"type": "string", "description": "dominio nudo, es. \"esempio.it\""}, "organization_id": {"type": "integer"}, "location_code": {"type": "integer", "description": "filtra sul mercato (es. 2756 = Svizzera)"}, "language_code": {"type": "string", "description": "filtra sulla lingua SERP (es. \"de\")"}}, "required": ["domain"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/get_usage": {"post": {"operationId": "get_usage", "summary": "get_usage", "description": "Consumi del mese vs limiti del piano: ricerche keyword, crediti audit, crediti Presenza AI, backlink, analisi concorrenza, monitoraggi keyword e tetto azioni MCP.", "requestBody": {"required": false, "content": {"application/json": {"schema": {"type": "object", "properties": {"organization_id": {"type": "integer"}}, "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/run_keyword_research": {"post": {"operationId": "run_keyword_research", "summary": "run_keyword_research", "description": "Avvia una ricerca keyword e ritorna un job_id: richiama lo stesso tool passando job_id per avere i risultati (volume, CPC, intent, difficulty = indice di competizione Google Ads 0-100, NON difficoltà SEO), di solito pronti entro un paio di minuti. mode: related (da una seed), list (volumi di una lista), domain (keyword di un dominio), gap (keyword del competitor non coperte dal dominio). Con project_id marca le keyword già tracciate. Il ritiro con job_id non consuma quota. AZIONE A PAGAMENTO: consuma 1 unità della quota mensile del piano e 1 azione MCP (tetto dedicato). Usa get_usage per i crediti residui.", "requestBody": {"required": false, "content": {"application/json": {"schema": {"type": "object", "properties": {"mode": {"type": "string", "enum": ["related", "list", "domain", "gap"]}, "job_id": {"type": "integer", "description": "ritira una ricerca già avviata"}, "seed": {"type": "string", "description": "keyword di partenza (mode=related)"}, "keywords": {"type": "array", "items": {"type": "string"}, "maxItems": 200, "description": "lista keyword (mode=list)"}, "domain": {"type": "string", "description": "dominio (mode=domain/gap)"}, "competitor": {"type": "string", "description": "dominio competitor (mode=gap)"}, "location_code": {"type": "integer", "description": "default 2380 (Italia)"}, "language_code": {"type": "string", "description": "default 'it'"}, "project_id": {"type": "integer"}, "organization_id": {"type": "integer"}}, "required": [], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/run_ai_visibility_check": {"post": {"operationId": "run_ai_visibility_check", "summary": "run_ai_visibility_check", "description": "Avvia in background il check di Presenza AI dei prompt del progetto (tutti gli attivi, o solo prompt_id). I risultati compaiono in get_ai_visibility dopo qualche minuto. Il tetto MCP conta 1 azione per prompt avviato. AZIONE A PAGAMENTO: consuma 1 unità della quota mensile del piano e 1 azione MCP (tetto dedicato). Usa get_usage per i crediti residui.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer"}, "prompt_id": {"type": "integer"}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/add_ai_prompt": {"post": {"operationId": "add_ai_prompt", "summary": "add_ai_prompt", "description": "Crea un prompt di Presenza AI e avvia il primo check. I crediti consumati dipendono dalle sources (pesi correnti nel campo credit_weights di get_ai_visibility) sulla quota crediti Presenza AI del piano. AZIONE A PAGAMENTO: consuma 1 unità della quota mensile del piano e 1 azione MCP (tetto dedicato). Usa get_usage per i crediti residui.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer"}, "query": {"type": "string", "maxLength": 500}, "sources": {"type": "array", "minItems": 1, "items": {"type": "string", "enum": ["google_aio", "ai_mode", "chatgpt", "gemini", "perplexity"]}}, "intent_category": {"type": "string", "enum": ["informational", "comparative", "commercial", "problem_solving"]}, "location_code": {"type": "integer", "description": "override di mercato del prompt (codice paese, es. 2724 = Spagna); default: il mercato del progetto. La stessa query può essere monitorata su più mercati."}, "language_code": {"type": "string", "description": "lingua del mercato override (es. \"es\"); default: la principale del paese"}}, "required": ["project_id", "query", "sources"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/run_domain_analysis": {"post": {"operationId": "run_domain_analysis", "summary": "run_domain_analysis", "description": "Analisi della concorrenza di un dominio: keyword organiche, traffico stimato, top keyword/pagine. I dati sono PER MERCATO (default: Italia/it) — lo stesso dominio su google.ch e su google.it dà numeri diversi. Se esiste uno snapshot più recente di 7 giorni per quel mercato lo riusa GRATIS (cached=true); altrimenti refresh (~30-60s). AZIONE A PAGAMENTO: consuma 1 unità della quota mensile del piano e 1 azione MCP (tetto dedicato). Usa get_usage per i crediti residui.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"domain": {"type": "string", "description": "dominio nudo, es. \"esempio.it\""}, "organization_id": {"type": "integer"}, "location_code": {"type": "integer", "description": "codice paese (es. 2756 = Svizzera); default 2380 (Italia)"}, "language_code": {"type": "string", "description": "lingua SERP del paese (es. \"de\"); default: la principale del paese"}}, "required": ["domain"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/run_audit": {"post": {"operationId": "run_audit", "summary": "run_audit", "description": "Avvia un nuovo SEO Audit del progetto (crawler interno, in background: dura minuti). AZIONE A PAGAMENTO: consuma 1 azione MCP subito e, a fine crawl, crediti audit del piano pari alle pagine reali analizzate. Stato in list_projects; risultati in get_audit_overview.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer"}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/refine_topical_architecture": {"post": {"operationId": "refine_topical_architecture", "summary": "refine_topical_architecture", "description": "Rifinisce con l'AI i cluster della Topical Architecture dell'ultimo audit (equivale a 'Rifinisci con AI' in app). UNA sola volta per audit: si riattiva con un nuovo audit SEO. Consuma 1 rifinitura AI del budget mensile del piano. AZIONE A PAGAMENTO: consuma 1 unità della quota mensile del piano e 1 azione MCP (tetto dedicato). Usa get_usage per i crediti residui.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer"}}, "required": ["project_id"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}, "/api/v1/generate_topical_bridge": {"post": {"operationId": "generate_topical_bridge", "summary": "generate_topical_bridge", "description": "Genera il \"ponte semantico\" (paragrafo con anchor, pronto da incollare) per linkare la pagina target dalla pagina source — la coppia pillar→orfana dell'Action Plan. source/target = URL (o id-nodo) di pagine dell'ultimo audit. La coppia già generata torna dalla cache GRATIS; altrimenti consuma 1 ponte AI del budget mensile (tetto 20 per audit). AZIONE A PAGAMENTO: consuma 1 unità della quota mensile del piano e 1 azione MCP (tetto dedicato). Usa get_usage per i crediti residui.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"project_id": {"type": "integer"}, "source": {"type": "string", "description": "URL della pagina che ospiterà il link"}, "target": {"type": "string", "description": "URL della pagina da linkare"}}, "required": ["project_id", "source", "target"], "additionalProperties": false}}}}, "responses": {"200": {"description": "ok=true, risultato in data"}, "400": {"description": "ok=false, messaggio in error"}}}}}}