{
  "openapi": "3.1.0",
  "info": {
    "title": "API di SMTP Senpai",
    "version": "1.0.0",
    "summary": "Invio di email transazionali, registro, lista di soppressione e moduli dei siti.",
    "description": "API REST di SMTP Senpai, il relay di MailSenpai. Tutte le risposte sono JSON con il campo `ok`; in caso di errore c'è anche `errore`, con un testo in italiano. La chiave si trova nell'area cliente, pagina SMTP Senpai. Le chiamate vanno fatte dal tuo server: l'API non accetta richieste dai browser (niente CORS), tranne l'indirizzo dei moduli.",
    "contact": {
      "name": "MailSenpai",
      "url": "https://www.mailsenpai.com/api-smtp/"
    }
  },
  "externalDocs": {
    "description": "Documentazione con esempi",
    "url": "https://www.mailsenpai.com/api-smtp/"
  },
  "servers": [
    {
      "url": "https://app.mailsenpai.com/relay/v1"
    }
  ],
  "security": [
    {
      "chiaveBearer": []
    }
  ],
  "tags": [
    {
      "name": "Invio"
    },
    {
      "name": "Account"
    },
    {
      "name": "Registro"
    },
    {
      "name": "Soppressione"
    },
    {
      "name": "Moduli dei siti"
    }
  ],
  "paths": {
    "/stato": {
      "get": {
        "operationId": "stato",
        "tags": [
          "Account"
        ],
        "summary": "Credenziali SMTP, volume del mese e stato dell'account",
        "description": "Non restituisce la password SMTP: quella si legge e si cambia dall'area cliente.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stato"
                },
                "example": {
                  "ok": true,
                  "stato": "active",
                  "smtp": {
                    "server": "relay.mailsenpai.com",
                    "porta": 2525,
                    "porta_alternativa": 2525,
                    "utente": "r1234@tuodominio.it",
                    "sicurezza": "STARTTLS"
                  },
                  "volume": {
                    "mese": "2026-10",
                    "incluso": 10000,
                    "usato": 1240,
                    "residuo": 8760,
                    "per_ora": 400
                  },
                  "tracciamento": false,
                  "soppressi": 17
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ChiaveNonValida"
          },
          "403": {
            "$ref": "#/components/responses/Sospeso"
          }
        }
      }
    },
    "/invio": {
      "post": {
        "operationId": "invio",
        "tags": [
          "Invio"
        ],
        "summary": "Manda una email a un destinatario",
        "description": "Una chiamata = un destinatario. Niente allegati, copia o copia nascosta: per quelli usa SMTP. Il mittente deve stare su un dominio verificato. Se il destinatario è nella lista di soppressione la risposta è 200 con `inviato: false` e l'email non parte. Non c'è una chiave di idempotenza: dopo un 502 o un timeout controlla `/eventi` prima di ripetere.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvioRichiesta"
              },
              "examples": {
                "italiano": {
                  "summary": "Nomi dei campi in italiano",
                  "value": {
                    "a": "cliente@esempio.it",
                    "da": "ordini@tuodominio.it",
                    "nome_mittente": "Il tuo negozio",
                    "oggetto": "Conferma ordine 10293",
                    "html": "<p>Grazie, il tuo ordine è confermato.</p>",
                    "rispondi_a": "assistenza@tuodominio.it",
                    "intestazioni": {
                      "X-Ordine": "10293"
                    }
                  }
                },
                "inglese": {
                  "summary": "Alias in inglese",
                  "value": {
                    "to": "customer@example.com",
                    "from": "orders@yourdomain.com",
                    "from_name": "Your shop",
                    "subject": "Order 10293 confirmed",
                    "text": "Thanks, your order is confirmed."
                  }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/InvioRichiesta"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accettata, oppure trattenuta perché il destinatario è in soppressione.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/InvioOk"
                    },
                    {
                      "$ref": "#/components/schemas/InvioSoppresso"
                    }
                  ]
                },
                "examples": {
                  "inviata": {
                    "value": {
                      "ok": true,
                      "inviato": true,
                      "id_messaggio": "9f2c4e1a7b3d5f6e8a9b0c1d@tuodominio.it",
                      "residuo": 8759,
                      "oltre_il_piano": false
                    }
                  },
                  "soppressa": {
                    "value": {
                      "ok": true,
                      "inviato": false,
                      "motivo": "indirizzo nella lista di soppressione"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Dati mancanti o non validi.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "destinatario non valido"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ChiaveNonValida"
          },
          "403": {
            "description": "Mittente su un dominio non verificato, oppure account sospeso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "il dominio esempio.com non risulta fra quelli verificati per il tuo account"
                }
              }
            }
          },
          "429": {
            "description": "Volume del mese esaurito e continuità di invio spenta (o tetto di spesa raggiunto).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErroreVolume"
                },
                "example": {
                  "ok": false,
                  "errore": "volume del mese esaurito: aumenta il piano o accendi la continuità di invio dalla tua area",
                  "residuo": 0,
                  "aumenta": "https://app.mailsenpai.com/customer/il-mio-piano",
                  "pannello": "https://app.mailsenpai.com/customer/relay-smtp"
                }
              }
            }
          },
          "502": {
            "description": "Il server di invio non ha accettato il messaggio o non era raggiungibile: il testo riporta la risposta ricevuta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "il server di invio ha risposto: 451 4.3.0 errore temporaneo"
                }
              }
            }
          }
        }
      }
    },
    "/statistiche": {
      "get": {
        "operationId": "statistiche",
        "tags": [
          "Registro"
        ],
        "summary": "Riepilogo degli ultimi giorni",
        "parameters": [
          {
            "name": "giorni",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 90,
              "default": 30
            },
            "description": "Valori fuori intervallo vengono riportati fra 1 e 90."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "giorni": {
                      "type": "integer"
                    },
                    "dati": {
                      "$ref": "#/components/schemas/Conteggi"
                    },
                    "per_giorno": {
                      "description": "Conteggi per data e tipo (al massimo gli ultimi 30 giorni). Senza movimenti arriva un array vuoto `[]`.",
                      "oneOf": [
                        {
                          "type": "object",
                          "additionalProperties": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "integer"
                            }
                          }
                        },
                        {
                          "type": "array",
                          "maxItems": 0
                        }
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "giorni": 7,
                  "dati": {
                    "sent": 812,
                    "delivered": 798,
                    "bounce": 9,
                    "defer": 14,
                    "complaint": 0,
                    "tracciamento": "non attivo",
                    "tasso_consegna": 98.3,
                    "tasso_rimbalzo": 1.11,
                    "tasso_segnalazioni": 0.0
                  },
                  "per_giorno": {
                    "2026-09-30": {
                      "delivered": 120,
                      "bounce": 2
                    },
                    "2026-10-01": {
                      "sent": 95,
                      "delivered": 93
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ChiaveNonValida"
          },
          "403": {
            "$ref": "#/components/responses/Sospeso"
          }
        }
      }
    },
    "/eventi": {
      "get": {
        "operationId": "eventi",
        "tags": [
          "Registro"
        ],
        "summary": "Ultimi movimenti, dal più recente",
        "description": "Il registro dettagliato copre gli ultimi 90 giorni. Non c'è paginazione: si leggono al massimo 500 righe.",
        "parameters": [
          {
            "name": "tipo",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/TipoEvento"
            },
            "description": "Filtra per tipo. `open` e `click` richiedono il tracciamento (altrimenti 403)."
          },
          {
            "name": "quanti",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "eventi": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Evento"
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "eventi": [
                    {
                      "event_id": "88213",
                      "relay_id": "12",
                      "type": "bounce",
                      "email": "mario@esempio.it",
                      "domain": "esempio.it",
                      "from_domain": "tuodominio.it",
                      "code": "550",
                      "reason": "5.1.1 user unknown",
                      "message_id": "",
                      "url": "",
                      "happened_at": "2026-10-01 08:42:10"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ChiaveNonValida"
          },
          "403": {
            "description": "Hai chiesto aperture o clic senza il tracciamento attivo, oppure l'account è sospeso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "servizio sospeso: piano scaduto. Controlla il tuo piano nell'area cliente."
                }
              }
            }
          }
        }
      }
    },
    "/soppressi": {
      "get": {
        "operationId": "soppressi",
        "tags": [
          "Soppressione"
        ],
        "summary": "Indirizzi e domini a cui non scriviamo più",
        "parameters": [
          {
            "name": "quanti",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "cerca",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Parte dell'indirizzo da cercare."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "totale": {
                      "type": "integer",
                      "description": "Totale delle voci in elenco (senza il filtro `cerca`)."
                    },
                    "elenco": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Soppresso"
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "totale": 2,
                  "elenco": [
                    {
                      "supp_id": "501",
                      "relay_id": "12",
                      "email": "mario@esempio.it",
                      "reason": "bounce",
                      "note": "5.1.1 user unknown",
                      "created_at": "2026-10-01 08:42:10"
                    },
                    {
                      "supp_id": "488",
                      "relay_id": "12",
                      "email": "@concorrente.it",
                      "reason": "manuale",
                      "note": "",
                      "created_at": "2026-09-20 10:00:00"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ChiaveNonValida"
          },
          "403": {
            "$ref": "#/components/responses/Sospeso"
          }
        }
      }
    },
    "/sopprimi": {
      "post": {
        "operationId": "sopprimi",
        "tags": [
          "Soppressione"
        ],
        "summary": "Aggiunge un indirizzo o un dominio alla lista di soppressione",
        "description": "Per un dominio intero scrivi `@dominio.it` oppure `dominio.it`. Risponde `ok: false` se la voce non è valida o era già presente.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "example": "mario@esempio.it"
                  },
                  "nota": {
                    "type": "string",
                    "maxLength": 255
                  }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "nota": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Esito"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ChiaveNonValida"
          },
          "403": {
            "$ref": "#/components/responses/Sospeso"
          }
        }
      }
    },
    "/riammetti": {
      "post": {
        "operationId": "riammetti",
        "tags": [
          "Soppressione"
        ],
        "summary": "Toglie dalla lista una voce che hai aggiunto tu",
        "description": "Scrivi la voce esattamente come compare in elenco (per i domini con la chiocciola, `@dominio.it`). Gli indirizzi entrati per rimbalzo o segnalazione di spam non vanno riammessi da qui: chiedine la verifica dall'area cliente.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "example": "mario@esempio.it"
                  }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`ok: true` se la voce c'era ed è stata tolta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Esito"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ChiaveNonValida"
          },
          "403": {
            "$ref": "#/components/responses/Sospeso"
          }
        }
      }
    },
    "/relay/f/{codice}": {
      "servers": [
        {
          "url": "https://app.mailsenpai.com",
          "description": "Indirizzo dei moduli"
        }
      ],
      "parameters": [
        {
          "name": "codice",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[A-Za-z0-9]{6,32}$"
          },
          "description": "Il codice del modulo, lo copi dall'area cliente."
        }
      ],
      "get": {
        "operationId": "moduloInfo",
        "tags": [
          "Moduli dei siti"
        ],
        "security": [],
        "summary": "Promemoria su come usare l'indirizzo",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModuloInfo"
                },
                "example": {
                  "ok": true,
                  "modulo": "Contatti sito",
                  "come": "Punta qui l'attributo action del tuo form, con method=\"POST\".",
                  "campi": [
                    "_subject",
                    "_replyto",
                    "_next",
                    "_cc",
                    "_gotcha",
                    "_format"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Modulo inesistente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "modulo non trovato"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "moduloInvia",
        "tags": [
          "Moduli dei siti"
        ],
        "security": [],
        "summary": "Riceve la compilazione del form del tuo sito",
        "description": "Nessuna chiave: l'indirizzo è pubblico per natura e si protegge limitandolo ai tuoi domini (impostazione del modulo). Risponde in JSON se la richiesta ha `Accept: application/json`, `X-Requested-With: XMLHttpRequest` o un corpo JSON; altrimenti, con l'invio classico del browser, rimanda con 303 alla pagina `_next` (o a quella impostata nel modulo) oppure mostra una pagina di conferma. Ogni email recapitata conta nel volume del mese.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/ModuloCampi"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/ModuloCampi"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ModuloCampi"
              },
              "example": {
                "nome": "Mario Rossi",
                "email": "mario@esempio.it",
                "messaggio": "Vorrei un preventivo",
                "_subject": "Richiesta dal sito",
                "_gotcha": ""
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ricevuto (JSON), oppure pagina di conferma HTML per l'invio classico senza pagina di ringraziamento.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "messaggio": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "messaggio": "Grazie, il messaggio è arrivato."
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "303": {
            "description": "Invio classico: rimando alla pagina di ringraziamento.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "400": {
            "description": "Invio classico non riuscito: pagina HTML con il motivo.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Modulo in pausa, servizio non attivo o sito non abilitato.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "questo modulo non è abilitato per www.altrosito.it"
                }
              }
            }
          },
          "404": {
            "description": "Modulo inesistente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "modulo non trovato"
                }
              }
            }
          },
          "409": {
            "description": "Manca ancora un dominio di invio verificato.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "il servizio non ha ancora un dominio di invio verificato"
                }
              }
            }
          },
          "422": {
            "description": "Nessun campo compilato.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "il modulo è arrivato vuoto"
                }
              }
            }
          },
          "429": {
            "description": "Più di 25 invii in un'ora dalla stessa rete, oppure volume del mese esaurito.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "troppi invii ravvicinati: riprova fra qualche minuto"
                }
              }
            }
          },
          "502": {
            "description": "Consegna non riuscita.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "non siamo riusciti a consegnare la risposta: riprova"
                }
              }
            }
          }
        }
      },
      "options": {
        "operationId": "moduloPreflight",
        "tags": [
          "Moduli dei siti"
        ],
        "security": [],
        "summary": "Preflight CORS per gli invii con JavaScript",
        "responses": {
          "204": {
            "description": "Consentito da qualunque origine; metodi POST e OPTIONS; intestazioni Content-Type e Accept."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "chiaveBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "msp_…",
        "description": "`Authorization: Bearer msp_…` (la chiave inizia con `msp_` ed è lunga 52 caratteri). In alternativa si accetta il campo `chiave` nel corpo (JSON o form) e, per le GET, come parametro dell'indirizzo (sconsigliato: finisce nei registri)."
      }
    },
    "schemas": {
      "Errore": {
        "type": "object",
        "required": [
          "ok",
          "errore"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": false
          },
          "errore": {
            "type": "string",
            "description": "Spiegazione leggibile, sempre in italiano."
          }
        }
      },
      "ErroreVolume": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Errore"
          },
          {
            "type": "object",
            "properties": {
              "residuo": {
                "type": "integer",
                "const": 0
              },
              "aumenta": {
                "type": "string",
                "format": "uri",
                "description": "Pagina dell'area cliente in cui aumentare il volume."
              },
              "pannello": {
                "type": "string",
                "format": "uri",
                "description": "Pagina dell'area cliente con consumi e continuità di invio."
              }
            }
          }
        ]
      },
      "Stato": {
        "type": "object",
        "required": [
          "ok",
          "stato",
          "smtp",
          "volume",
          "tracciamento",
          "soppressi"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "stato": {
            "type": "string",
            "enum": [
              "pending",
              "active"
            ],
            "description": "`pending` = account creato, in attivazione; `active` = operativo. Un account sospeso non arriva qui: riceve 403."
          },
          "smtp": {
            "type": "object",
            "properties": {
              "server": {
                "type": "string",
                "example": "relay.mailsenpai.com"
              },
              "porta": {
                "type": "integer",
                "example": 2525
              },
              "porta_alternativa": {
                "type": "integer",
                "example": 2525
              },
              "utente": {
                "type": "string",
                "example": "r1234@tuodominio.it"
              },
              "sicurezza": {
                "type": "string",
                "const": "STARTTLS"
              }
            }
          },
          "volume": {
            "type": "object",
            "properties": {
              "mese": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}$",
                "example": "2026-10"
              },
              "incluso": {
                "type": "integer",
                "description": "Email incluse nel mese."
              },
              "usato": {
                "type": "integer",
                "description": "Email già contate nel mese (consegnate o rimbalzate)."
              },
              "residuo": {
                "type": "integer"
              },
              "per_ora": {
                "type": "integer",
                "description": "Ritmo orario del piano: oltre questo ritmo i messaggi restano in coda, non vengono rifiutati."
              }
            }
          },
          "tracciamento": {
            "type": "boolean",
            "description": "Vero se hai attivato aperture e clic."
          },
          "soppressi": {
            "type": "integer",
            "description": "Voci nella lista di soppressione."
          }
        }
      },
      "InvioRichiesta": {
        "type": "object",
        "description": "Un destinatario per chiamata. I nomi italiani hanno la precedenza sugli alias inglesi se li mandi entrambi.",
        "properties": {
          "a": {
            "type": "string",
            "format": "email",
            "description": "Destinatario (uno solo)."
          },
          "to": {
            "type": "string",
            "format": "email",
            "description": "Alias di `a`."
          },
          "da": {
            "type": "string",
            "format": "email",
            "description": "Mittente: deve stare su un dominio verificato del tuo account (vale anche un suo sottodominio)."
          },
          "from": {
            "type": "string",
            "format": "email",
            "description": "Alias di `da`."
          },
          "nome_mittente": {
            "type": "string",
            "description": "Nome visualizzato del mittente."
          },
          "from_name": {
            "type": "string",
            "description": "Alias di `nome_mittente`."
          },
          "oggetto": {
            "type": "string",
            "minLength": 1
          },
          "subject": {
            "type": "string",
            "description": "Alias di `oggetto`."
          },
          "html": {
            "type": "string",
            "description": "Corpo HTML. Se manca il testo, la versione testuale viene ricavata dall'HTML."
          },
          "testo": {
            "type": "string",
            "description": "Corpo in solo testo."
          },
          "text": {
            "type": "string",
            "description": "Alias di `testo`."
          },
          "rispondi_a": {
            "type": "string",
            "format": "email",
            "description": "Indirizzo Reply-To. Se non è un indirizzo valido viene ignorato."
          },
          "reply_to": {
            "type": "string",
            "format": "email",
            "description": "Alias di `rispondi_a`."
          },
          "intestazioni": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Intestazioni personalizzate. Passano solo i nomi `X-…` (lettere, cifre e trattini, max 40 caratteri dopo `X-`); le altre vengono scartate senza errore.",
            "example": {
              "X-Ordine": "A-10293"
            }
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Alias di `intestazioni`."
          }
        },
        "allOf": [
          {
            "anyOf": [
              {
                "required": [
                  "a"
                ]
              },
              {
                "required": [
                  "to"
                ]
              }
            ]
          },
          {
            "anyOf": [
              {
                "required": [
                  "da"
                ]
              },
              {
                "required": [
                  "from"
                ]
              }
            ]
          },
          {
            "anyOf": [
              {
                "required": [
                  "oggetto"
                ]
              },
              {
                "required": [
                  "subject"
                ]
              }
            ]
          },
          {
            "anyOf": [
              {
                "required": [
                  "html"
                ]
              },
              {
                "required": [
                  "testo"
                ]
              },
              {
                "required": [
                  "text"
                ]
              }
            ]
          }
        ]
      },
      "InvioOk": {
        "type": "object",
        "required": [
          "ok",
          "inviato",
          "id_messaggio"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "inviato": {
            "type": "boolean",
            "const": true
          },
          "id_messaggio": {
            "type": "string",
            "description": "Identificativo assegnato al messaggio; è anche il suo Message-ID (senza parentesi angolari).",
            "example": "9f2c4e1a7b3d5f6e8a9b0c1d@tuodominio.it"
          },
          "residuo": {
            "type": "integer",
            "description": "Stima del volume rimasto nel mese."
          },
          "oltre_il_piano": {
            "type": "boolean",
            "description": "Vero se il volume incluso era finito e l'invio è passato grazie alla continuità di invio."
          }
        }
      },
      "InvioSoppresso": {
        "type": "object",
        "required": [
          "ok",
          "inviato",
          "motivo"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "inviato": {
            "type": "boolean",
            "const": false
          },
          "motivo": {
            "type": "string",
            "example": "indirizzo nella lista di soppressione"
          }
        }
      },
      "Conteggi": {
        "type": "object",
        "properties": {
          "sent": {
            "type": "integer"
          },
          "delivered": {
            "type": "integer"
          },
          "bounce": {
            "type": "integer"
          },
          "defer": {
            "type": "integer"
          },
          "complaint": {
            "type": "integer"
          },
          "open": {
            "type": "integer",
            "description": "Solo con il tracciamento attivo (destinatari unici)."
          },
          "click": {
            "type": "integer",
            "description": "Solo con il tracciamento attivo (destinatari unici)."
          },
          "tracciamento": {
            "type": "string",
            "description": "Presente al posto di open/click quando il tracciamento non è attivo."
          },
          "tasso_consegna": {
            "type": "number",
            "description": "Percentuale, una cifra decimale."
          },
          "tasso_rimbalzo": {
            "type": "number"
          },
          "tasso_segnalazioni": {
            "type": "number"
          }
        }
      },
      "Evento": {
        "type": "object",
        "description": "Riga del registro. I numeri arrivano come stringhe; le date sono in UTC nel formato `AAAA-MM-GG HH:MM:SS`.",
        "properties": {
          "event_id": {
            "type": "string",
            "pattern": "^\\d+$"
          },
          "relay_id": {
            "type": "string",
            "pattern": "^\\d+$"
          },
          "type": {
            "$ref": "#/components/schemas/TipoEvento"
          },
          "email": {
            "type": "string",
            "description": "Destinatario."
          },
          "domain": {
            "type": "string",
            "description": "Dominio del destinatario."
          },
          "from_domain": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Codice restituito dal server del destinatario, se c'è."
          },
          "reason": {
            "type": "string",
            "description": "Motivo leggibile (rimbalzi, rinvii, trattenute)."
          },
          "message_id": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "Link cliccato (solo eventi click)."
          },
          "happened_at": {
            "type": "string",
            "example": "2026-10-01 08:42:10"
          }
        }
      },
      "TipoEvento": {
        "type": "string",
        "enum": [
          "sent",
          "delivered",
          "bounce",
          "defer",
          "complaint",
          "open",
          "click",
          "dropped"
        ],
        "description": "`sent` accettata via API o modulo · `delivered` consegnata · `bounce` rifiuto definitivo · `defer` rinviata, si riprova · `complaint` segnalazione di spam · `open`/`click` con il tracciamento · `dropped` non spedita perché il destinatario è nella lista di soppressione."
      },
      "Soppresso": {
        "type": "object",
        "properties": {
          "supp_id": {
            "type": "string",
            "pattern": "^\\d+$"
          },
          "relay_id": {
            "type": "string",
            "pattern": "^\\d+$"
          },
          "email": {
            "type": "string",
            "description": "Indirizzo, oppure un dominio intero scritto `@dominio.it`."
          },
          "reason": {
            "type": "string",
            "enum": [
              "bounce",
              "complaint",
              "manuale"
            ]
          },
          "note": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "example": "2026-09-30 14:03:51"
          }
        }
      },
      "Esito": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean"
          }
        }
      },
      "ModuloInfo": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "modulo": {
            "type": "string"
          },
          "come": {
            "type": "string"
          },
          "campi": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ModuloCampi": {
        "type": "object",
        "description": "Tutti i campi del tuo form (max 60, max 5.000 caratteri ciascuno). I nomi che iniziano con `_` sono riservati: valgono solo quelli elencati, gli altri vengono ignorati. I file allegati non sono supportati.",
        "additionalProperties": {
          "type": "string"
        },
        "properties": {
          "_subject": {
            "type": "string",
            "description": "Oggetto della email che ricevi."
          },
          "_replyto": {
            "type": "string",
            "format": "email",
            "description": "Indirizzo a cui rispondere. Se manca si usa il primo campo che contiene «mail» nel nome e un indirizzo valido."
          },
          "_next": {
            "type": "string",
            "format": "uri",
            "description": "Pagina di ringraziamento (indirizzo completo http/https) per l'invio classico."
          },
          "_cc": {
            "type": "string",
            "format": "email",
            "description": "Copia a un altro indirizzo, solo se l'hai permesso nelle impostazioni del modulo."
          },
          "_gotcha": {
            "type": "string",
            "description": "Campo esca da lasciare vuoto e nascosto: se è pieno l'invio viene scartato in silenzio."
          },
          "_format": {
            "type": "string",
            "enum": [
              "plain"
            ],
            "description": "`plain` per ricevere la email in solo testo."
          }
        }
      }
    },
    "responses": {
      "ChiaveNonValida": {
        "description": "Chiave mancante o non valida.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Errore"
            },
            "example": {
              "ok": false,
              "errore": "chiave non valida"
            }
          }
        }
      },
      "Sospeso": {
        "description": "Account sospeso (piano scaduto o non pagato, volume finito senza continuità, tetto di spesa raggiunto…). Il messaggio dice il motivo.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Errore"
            },
            "example": {
              "ok": false,
              "errore": "servizio sospeso: volume del mese esaurito. Controlla il tuo piano nell'area cliente."
            }
          }
        }
      }
    }
  }
}