{
  "openapi": "3.1.0",
  "info": {
    "title": "System Agency — API dei servizi del sito",
    "version": "1.1.0",
    "description": "Gli endpoint che il sito espone. Non è un prodotto API con una sua versione: è la superficie che le pagine del sito usano per sé, descritta per come è. Un 503 non è un guasto: è un prerequisito di configurazione non attivo, e il corpo dice quale. La descrizione completa, in italiano, è in /api/. L’elenco in forma RFC 9727 è in /.well-known/api-catalog. Nota di versione: il numero di questa specifica è la versione del sito, non una promessa di compatibilità tra release.",
    "contact": {
      "name": "SYSTEM",
      "url": "https://systemagency.dev/api/",
      "email": "ciao@systemagency.dev"
    },
    "termsOfService": "https://systemagency.dev/terms/",
    "license": {
      "name": "Descrizione disponibile",
      "url": "https://systemagency.dev/api/"
    }
  },
  "servers": [
    {
      "url": "https://systemagency.dev",
      "description": "System Agency"
    }
  ],
  "externalDocs": {
    "description": "Documentazione degli endpoint",
    "url": "https://systemagency.dev/api/"
  },
  "paths": {
    "/api/leads": {
      "post": {
        "summary": "Invio di una richiesta (brief)",
        "description": "Registra una richiesta di progetto. È l’unico endpoint che scrive un dato personale. Limite applicato: 8 richieste per client ogni 10 minuti, 200 complessive per ora..",
        "security": [],
        "x-prerequisites": [
          "LEAD_INTAKE_ENABLED=true",
          "DATABASE_URL, oppure SUPABASE_URL con SUPABASE_SERVICE_ROLE_KEY",
          "RATE_LIMIT_SECRET di almeno 32 caratteri",
          "TURNSTILE_SECRET quando CAPTCHA_ENABLED=true"
        ],
        "x-rate-limit": "8 richieste per client ogni 10 minuti, 200 complessive per ora.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "idempotencyKey": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "contact": {
                    "type": "object",
                    "required": [
                      "name",
                      "email"
                    ],
                    "properties": {
                      "name": {
                        "type": "string",
                        "maxLength": 150
                      },
                      "email": {
                        "type": "string",
                        "format": "email",
                        "maxLength": 254
                      },
                      "phone": {
                        "type": "string",
                        "maxLength": 40
                      }
                    }
                  },
                  "company": {
                    "type": "object",
                    "required": [
                      "name",
                      "industry",
                      "website"
                    ],
                    "properties": {
                      "name": {
                        "type": "string",
                        "maxLength": 150
                      },
                      "industry": {
                        "type": "string",
                        "enum": [
                          "Hospitality",
                          "Real estate",
                          "Servizi professionali",
                          "Ecommerce",
                          "B2B",
                          "Startup",
                          "Automotive",
                          "Altro"
                        ]
                      },
                      "website": {
                        "type": "string",
                        "maxLength": 500,
                        "description": "URL assoluto http/https, o stringa vuota."
                      }
                    }
                  },
                  "services": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "Sito web",
                        "Ecommerce",
                        "SEO",
                        "GEO",
                        "AI",
                        "Automazioni",
                        "Sistema digitale",
                        "Da definire"
                      ]
                    },
                    "minItems": 1,
                    "maxItems": 8
                  },
                  "problems": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "Più richieste qualificate",
                        "Più vendite",
                        "Sito poco efficace",
                        "Scarsa visibilità",
                        "Processi manuali",
                        "Follow-up lento",
                        "Carico di assistenza",
                        "Strumenti scollegati"
                      ]
                    },
                    "minItems": 1,
                    "maxItems": 8
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 4000
                  },
                  "timeline": {
                    "type": "string",
                    "enum": [
                      "Appena possibile",
                      "Entro 1–3 mesi",
                      "Entro 3–6 mesi",
                      "Sto esplorando"
                    ]
                  },
                  "budget": {
                    "type": "string",
                    "enum": [
                      "Meno di €3.000",
                      "€3.000–€7.000",
                      "€7.000–€15.000",
                      "Oltre €15.000",
                      "Da definire insieme"
                    ]
                  },
                  "privacyAcknowledged": {
                    "type": "boolean",
                    "const": true
                  },
                  "websiteTrap": {
                    "type": "string",
                    "maxLength": 0
                  },
                  "acquisition": {
                    "type": "object",
                    "properties": {
                      "landingPage": {
                        "type": "string",
                        "maxLength": 500
                      },
                      "utmSource": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "maxLength": 200
                      },
                      "utmMedium": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "maxLength": 200
                      },
                      "utmCampaign": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "maxLength": 200
                      }
                    }
                  },
                  "captchaToken": {
                    "type": "string",
                    "maxLength": 2048
                  }
                },
                "required": [
                  "idempotencyKey",
                  "contact",
                  "company",
                  "services",
                  "problems",
                  "timeline",
                  "budget",
                  "privacyAcknowledged",
                  "websiteTrap",
                  "acquisition"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{saved:true, reference}: la referenza è la chiave idempotente della registrazione.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saved": {
                      "type": "boolean"
                    },
                    "reference": {
                      "type": "string",
                      "format": "uuid"
                    }
                  },
                  "required": [
                    "saved",
                    "reference"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Corpo non valido o verifica umana mancante.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Origine della richiesta diversa da questo sito, o verifica umana non accettata.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "409": {
            "description": "La stessa chiave idempotente con un corpo diverso.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "413": {
            "description": "Corpo oltre 16 kB.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "415": {
            "description": "Content-Type diverso da application/json.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Limite di richieste raggiunto.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Prerequisito non attivo: invio non abilitato, storage non configurato o verifica umana non configurata su questo server.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/audit": {
      "post": {
        "summary": "Controllo strutturale di una pagina pubblica",
        "description": "Scarica l’HTML di un URL pubblico, lo legge senza eseguire gli script e aggiunge i segnali di robots.txt e sitemap del dominio. Limite applicato: 5 richieste per client per ora, 150 complessive per ora. Il limite è nel database: senza storage il controllo non parte..",
        "security": [],
        "x-prerequisites": [
          "URL_AUDIT_ENABLED=true",
          "DATABASE_URL, oppure SUPABASE_URL con SUPABASE_SERVICE_ROLE_KEY",
          "RATE_LIMIT_SECRET di almeno 32 caratteri"
        ],
        "x-rate-limit": "5 richieste per client per ora, 150 complessive per ora. Il limite è nel database: senza storage il controllo non parte.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2000
                  }
                },
                "required": [
                  "url"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Segnali osservati, cosa non è misurato e cosa resterebbe da verificare."
          },
          "400": {
            "description": "URL non valido, non pubblico o con indirizzo non consentito.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Origine della richiesta diversa da questo sito.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "La pagina non è leggibile: non HTML, oltre 1 MB, troppi redirect.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Limite di richieste raggiunto.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "502": {
            "description": "Impossibile leggere la pagina.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Controllo non attivo su questo server o limite di richieste senza storage.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "504": {
            "description": "Il sito ha impiegato troppo tempo a rispondere o a risolversi.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/audit-vitals": {
      "post": {
        "summary": "Dati di campo Core Web Vitals (CrUX via PageSpeed Insights)",
        "description": "Chiede al dataset pubblico di Google i valori P75 osservati su un URL. Strategia mobile. Limite applicato: 5 richieste per client per ora, 150 complessive per ora. Il limite è nel database: senza storage il controllo non parte..",
        "security": [],
        "x-prerequisites": [
          "URL_AUDIT_ENABLED=true",
          "DATABASE_URL, oppure SUPABASE_URL con SUPABASE_SERVICE_ROLE_KEY",
          "RATE_LIMIT_SECRET di almeno 32 caratteri"
        ],
        "x-rate-limit": "5 richieste per client per ora, 150 complessive per ora. Il limite è nel database: senza storage il controllo non parte.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2000
                  }
                },
                "required": [
                  "url"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "status: measured | no_field_data | unavailable, con la fonte e il perimetro. `measured` può convivere con `no_field_data` per un solo URL."
          },
          "400": {
            "description": "URL non valido o non pubblico.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Origine della richiesta diversa da questo sito.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Limite di richieste raggiunto.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Controllo non attivo su questo server.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/audit-browser": {
      "post": {
        "summary": "Audit nel browser con axe",
        "description": "Apre la pagina in un browser headless sul server, blocca le risorse esterne e misura LCP/CLS di laboratorio e violazioni axe. Limite applicato: 5 richieste per client per ora, 150 complessive per ora. Il limite è nel database: senza storage il controllo non parte..",
        "security": [],
        "x-prerequisites": [
          "URL_AUDIT_ENABLED=true",
          "DATABASE_URL, oppure SUPABASE_URL con SUPABASE_SERVICE_ROLE_KEY",
          "RATE_LIMIT_SECRET di almeno 32 caratteri",
          "un browser che il server può eseguire (AUDIT_BROWSER_EXECUTABLE)",
          "TURNSTILE_SECRET quando CAPTCHA_ENABLED=true"
        ],
        "x-rate-limit": "5 richieste per client per ora, 150 complessive per ora. Il limite è nel database: senza storage il controllo non parte.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2000
                  },
                  "captchaToken": {
                    "type": "string",
                    "maxLength": 2048
                  }
                },
                "required": [
                  "url"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Misure di laboratorio, violazioni axe con quelle non valutabili, risorse bloccate, perimetro dichiarato."
          },
          "400": {
            "description": "URL non valido o verifica umana mancante.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Origine non autorizzata o verifica umana non accettata.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Limite di richieste raggiunto.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Controllo non attivo, nessun browser disponibile sul server (nomina AUDIT_BROWSER_EXECUTABLE), o due audit già in corso.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/prune-leads": {
      "post": {
        "summary": "Scansione delle richieste oltre la finestra di conservazione (operatore)",
        "description": "Applica subito la finestra di conservazione dei record e risponde di quanti ne ha rimossi. Limite contato nel database: nessuno.",
        "security": [
          {
            "operatorBearer": []
          }
        ],
        "x-prerequisites": [
          "API_ADMIN_SECRET di almeno 32 caratteri",
          "DATABASE_URL, oppure SUPABASE_URL con SUPABASE_SERVICE_ROLE_KEY"
        ],
        "x-rate-limit": "nessuno contato nel database",
        "responses": {
          "200": {
            "description": "{windowDays, removed}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "windowDays": {
                      "type": "integer"
                    },
                    "removed": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "windowDays",
                    "removed"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Secret dell’operatore mancante, corto o diverso.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Storage non configurato.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/retry-notifications": {
      "post": {
        "summary": "Recupero delle notificazioni non partite (operatore)",
        "description": "Riprova l’invio delle notifiche di richieste salvate che non sono partite. Limite contato nel database: nessuno.",
        "security": [
          {
            "operatorBearer": []
          }
        ],
        "x-prerequisites": [
          "API_ADMIN_SECRET di almeno 32 caratteri",
          "DATABASE_URL, oppure SUPABASE_URL con SUPABASE_SERVICE_ROLE_KEY",
          "un canale di notificazione: RESEND_API_KEY con LEAD_NOTIFY_EMAIL e EMAIL_FROM, oppure CRM_WEBHOOK_URL con CRM_WEBHOOK_SECRET"
        ],
        "x-rate-limit": "nessuno contato nel database",
        "responses": {
          "200": {
            "description": "{processed}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "processed": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "processed"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Secret dell’operatore mancante, corto o diverso.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Storage non configurato.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Unico campo di ogni errore: una frase che dice cosa manca."
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "summary": "Stato del servizio",
        "description": "Risponde dello stato del servizio e dei prerequisiti che lo abilitano. Nessuna autenticazione, nessuna scrittura. Limite contato nel database: nessuno.",
        "security": [],
        "x-prerequisites": [],
        "x-rate-limit": "nessuno contato nel database",
        "responses": {
          "200": {
            "description": "Il servizio ha risposto, con l’elenco dei prerequisiti e il loro stato di configurazione.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "available",
                        "available_with_limits"
                      ]
                    },
                    "measured": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "notMeasured": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "capabilities": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "path": {
                            "type": "string"
                          },
                          "method": {
                            "type": "string"
                          },
                          "ready": {
                            "type": "boolean"
                          },
                          "gates": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "ready": {
                                  "type": "boolean"
                                }
                              },
                              "required": [
                                "name",
                                "ready"
                              ]
                            }
                          },
                          "required": [
                            "path",
                            "method",
                            "ready",
                            "gates"
                          ]
                        }
                      }
                    },
                    "required": [
                      "status",
                      "measured",
                      "notMeasured",
                      "capabilities"
                    ]
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "operatorBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Secret dell’operatore (API_ADMIN_SECRET), inviato come Authorization: Bearer."
      }
    }
  }
}