{
  "openapi": "3.1.0",
  "info": {
    "title": "API di SMTP Senpai",
    "version": "1.2.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. Dalla 1.2 lo stesso account accetta anche richieste nel formato di altre API di email transazionali (SendGrid, Mailgun, Postmark, Resend, Brevo, Mailjet, SparkPost, Mandrill, MailerSend, Amazon SES, Elastic Email, SMTP2GO, Postal, Plunk) su https://app.mailsenpai.com/relay/compat/<servizio>/ oppure https://<servizio>-api.mailsenpai.com/: vedi https://www.mailsenpai.com/api-smtp/ .",
    "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": "Webhook"
    },
    {
      "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": 587,
                    "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": {
            "description": "Account sospeso (il messaggio dice perché) oppure in attivazione.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "servizio in attivazione: stiamo preparando il tuo account, riprova tra qualche minuto"
                }
              }
            }
          },
          "405": {
            "description": "Metodo HTTP non ammesso: l'intestazione Allow dice quello giusto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "metodo non ammesso: usa GET"
                }
              }
            }
          }
        }
      }
    },
    "/invio": {
      "post": {
        "operationId": "invio",
        "tags": [
          "Invio"
        ],
        "summary": "Manda una email a uno o più destinatari",
        "description": "Fino a 500 destinatari per chiamata, ognuno con la sua copia: fino a 50 la chiamata spedisce subito (200), da 51 a 500 viene accettata e messa in coda (202, le copie partono entro un minuto, ogni evento `sent` arriva con lo stesso `id_messaggio`). Allegati fino a 10 file e 10 MB. Copia e copia nascosta solo via SMTP. Intestazioni personalizzate: `X-…` più quelle di buona consegna (List-Unsubscribe, List-Unsubscribe-Post, Feedback-ID, Precedence…), vedi `intestazioni`. Il mittente deve stare su un dominio verificato. Chi è nella lista di soppressione viene trattenuto (evento `dropped`). Con l'intestazione `Idempotency-Key` la stessa richiesta ripetuta entro 24 ore restituisce la prima risposta (con `Idempotent-Replayed: true`) senza spedire di nuovo.",
        "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"
                    },
                    {
                      "$ref": "#/components/schemas/InvioMulti"
                    }
                  ]
                },
                "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"
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "schema": {
                  "type": "string",
                  "const": "true"
                },
                "description": "Presente quando la risposta è quella conservata di una richiesta identica."
              }
            }
          },
          "202": {
            "description": "Più di 50 destinatari: richiesta accettata e messa in coda (dalla 1.2).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvioCoda"
                },
                "example": {
                  "ok": true,
                  "inviati": 0,
                  "in_coda": 2,
                  "non_inviati": 1,
                  "risultati": [
                    {
                      "a": "cliente1@esempio.it",
                      "inviato": false,
                      "in_coda": true,
                      "id_messaggio": "9f2c4e1a7b3d5f6e8a9b0c1d@tuodominio.it"
                    },
                    {
                      "a": "cliente2@esempio.it",
                      "inviato": false,
                      "in_coda": true,
                      "id_messaggio": "1a2b3c4d5e6f7a8b9c0d1e2f@tuodominio.it"
                    },
                    {
                      "a": "bloccato@esempio.it",
                      "inviato": false,
                      "motivo": "indirizzo nella lista di soppressione"
                    }
                  ],
                  "residuo": 8757,
                  "oltre_il_piano": false
                }
              }
            }
          },
          "400": {
            "description": "Dati mancanti o non validi, compreso un valore non valido di un'intestazione di buona consegna (per esempio `intestazione List-Unsubscribe-Post: serve anche List-Unsubscribe con un indirizzo https (RFC 8058)`).",
            "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"
                }
              }
            }
          },
          "405": {
            "description": "Metodo HTTP non ammesso: l'intestazione Allow dice quello giusto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "metodo non ammesso: usa GET"
                }
              }
            }
          },
          "409": {
            "description": "Una richiesta con la stessa Idempotency-Key è ancora in corso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "una richiesta con la stessa Idempotency-Key è ancora in corso"
                }
              }
            }
          },
          "413": {
            "description": "Allegati oltre i 10 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "allegati troppo grandi: al massimo 10 MB in tutto"
                }
              }
            }
          },
          "422": {
            "description": "Idempotency-Key già usata con dati diversi.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "questa Idempotency-Key è già stata usata nelle ultime 24 ore con dati diversi"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 100,
              "pattern": "^[\\x21-\\x7E]+$"
            },
            "description": "Chiave tua (per esempio l'id dell'ordine), valida 24 ore."
          }
        ]
      }
    },
    "/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"
          },
          "405": {
            "description": "Metodo HTTP non ammesso: l'intestazione Allow dice quello giusto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "metodo non ammesso: usa GET"
                }
              }
            }
          }
        }
      }
    },
    "/eventi": {
      "get": {
        "operationId": "eventi",
        "tags": [
          "Registro"
        ],
        "summary": "Movimenti: gli ultimi, oppure le novità dopo un cursore",
        "description": "Senza `dopo`: gli ultimi movimenti, dal più recente (al massimo 500). Con `dopo`: i movimenti successivi a quell'identificativo, dal più vecchio, con `prossimo` da passare alla chiamata seguente. Il registro dettagliato copre 7 giorni sul piano gratuito e 90 giorni sui piani a pagamento.",
        "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
            }
          },
          {
            "name": "dopo",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Cursore: `event_id` dell'ultimo evento già letto (0 per partire dall'inizio). Alias: `after`."
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Alias di `dopo`."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            },
            "description": "Alias di `quanti`."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "eventi": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Evento"
                      }
                    },
                    "prossimo": {
                      "type": "string",
                      "description": "Solo con `dopo`: valore da usare come `dopo` alla prossima chiamata."
                    },
                    "altri": {
                      "type": "boolean",
                      "description": "Solo con `dopo`: vero se ci sono altri eventi da leggere subito."
                    }
                  }
                },
                "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."
                }
              }
            }
          },
          "405": {
            "description": "Metodo HTTP non ammesso: l'intestazione Allow dice quello giusto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "metodo non ammesso: usa GET"
                }
              }
            }
          }
        }
      }
    },
    "/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"
          },
          "405": {
            "description": "Metodo HTTP non ammesso: l'intestazione Allow dice quello giusto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "metodo non ammesso: usa GET"
                }
              }
            }
          }
        }
      }
    },
    "/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`. Se la voce c'era già risponde comunque `ok: true` con `gia_presente: true`.",
        "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,
                  "email": "mario@esempio.it",
                  "gia_presente": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ChiaveNonValida"
          },
          "403": {
            "$ref": "#/components/responses/Sospeso"
          },
          "400": {
            "description": "Voce non valida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "serve un indirizzo email o un dominio (@dominio.it)"
                }
              }
            }
          },
          "405": {
            "description": "Metodo HTTP non ammesso: l'intestazione Allow dice quello giusto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "metodo non ammesso: usa GET"
                }
              }
            }
          }
        }
      }
    },
    "/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. Le voci entrate per rimbalzo o segnalazione danno 409; una voce che non c'è dà 404.",
        "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"
          },
          "404": {
            "description": "Voce non presente in elenco.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "indirizzo non presente in elenco"
                }
              }
            }
          },
          "409": {
            "description": "Voce bloccata dai nostri server: va chiesto lo sblocco dall'area cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "questo indirizzo è stato bloccato dai nostri server (bounce): puoi chiederne lo sblocco dall'area cliente"
                }
              }
            }
          },
          "405": {
            "description": "Metodo HTTP non ammesso: l'intestazione Allow dice quello giusto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "metodo non ammesso: usa GET"
                }
              }
            }
          }
        }
      }
    },
    "/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."
          }
        }
      }
    },
    "/webhook": {
      "get": {
        "operationId": "webhookLeggi",
        "tags": [
          "Webhook"
        ],
        "summary": "Impostazioni del webhook",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "webhook": {
                      "$ref": "#/components/schemas/Webhook"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ChiaveNonValida"
          },
          "405": {
            "description": "Metodo HTTP non ammesso: l'intestazione Allow dice quello giusto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "metodo non ammesso: usa GET"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "webhookImposta",
        "tags": [
          "Webhook"
        ],
        "summary": "Imposta o spegne il webhook",
        "description": "Ogni evento scelto arriva con un POST JSON firmato; se il tuo server non risponde 2xx riproviamo per circa 24 ore (1 min, 5 min, 30 min, 2 h, 6 h, 12 h). Solo indirizzi https pubblici.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "eventi": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Alias di `eventi`."
                  },
                  "attivo": {
                    "type": "boolean"
                  },
                  "active": {
                    "type": "boolean"
                  },
                  "nuovo_segreto": {
                    "type": "boolean"
                  },
                  "rotate_secret": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "url": "https://tuosito.it/webhook/mailsenpai",
                "eventi": [
                  "delivered",
                  "bounce",
                  "complaint"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "webhook": {
                      "$ref": "#/components/schemas/Webhook"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ChiaveNonValida"
          },
          "405": {
            "description": "Metodo HTTP non ammesso: l'intestazione Allow dice quello giusto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "metodo non ammesso: usa GET"
                }
              }
            }
          },
          "422": {
            "description": "Indirizzo non https o non pubblico, oppure nessun evento valido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "l'indirizzo deve essere pubblico"
                }
              }
            }
          }
        }
      }
    },
    "/webhook/prova": {
      "post": {
        "operationId": "webhookProva",
        "tags": [
          "Webhook"
        ],
        "summary": "Manda subito un evento di prova",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "example": {
                  "ok": true,
                  "codice_http": 200,
                  "errore": ""
                }
              }
            }
          },
          "502": {
            "description": "Il tuo server non ha risposto 2xx.",
            "content": {
              "application/json": {
                "example": {
                  "ok": false,
                  "codice_http": 500,
                  "errore": "risposta HTTP 500"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ChiaveNonValida"
          },
          "405": {
            "description": "Metodo HTTP non ammesso: l'intestazione Allow dice quello giusto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Errore"
                },
                "example": {
                  "ok": false,
                  "errore": "metodo non ammesso: usa GET"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "chiaveBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "msp_…",
        "description": "`Authorization: Bearer msp_…` (la chiave inizia con `msp_`). In alternativa si accetta il campo `chiave` nel corpo (JSON o form). La chiave nell'indirizzo (`?chiave=`) viene rifiutata con 401: finirebbe 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": 587
              },
              "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": "Uno o più destinatari (fino a 500; oltre 50 vanno in coda): ognuno riceve la sua copia. I nomi italiani hanno la precedenza sugli alias inglesi se li mandi entrambi.",
        "properties": {
          "a": {
            "oneOf": [
              {
                "type": "string",
                "format": "email"
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "email"
                },
                "minItems": 1,
                "maxItems": 500
              }
            ],
            "description": "Destinatario, oppure elenco fino a 500 destinatari (oltre 50 vanno in coda, risposta 202)."
          },
          "to": {
            "oneOf": [
              {
                "type": "string",
                "format": "email"
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "email"
                },
                "minItems": 1,
                "maxItems": 500
              }
            ],
            "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 (dalla 1.2). Accettate: `X-…` (lettere, cifre e trattini) e le intestazioni di buona consegna List-Unsubscribe (fino a 4 indirizzi `<https://…>` / `<mailto:…>`), List-Unsubscribe-Post (solo `List-Unsubscribe=One-Click` e solo con un indirizzo https in List-Unsubscribe, RFC 8058), List-Id, List-Help, List-Subscribe, List-Owner, List-Archive, List-Post, Feedback-ID (formato Gmail a:b:c:mittente), Precedence (bulk, list, junk), Auto-Submitted, In-Reply-To, References, Importance, Priority, X-Priority, Content-Language, Keywords, Thread-Topic, Thread-Index. Mai accettate (ignorate ed elencate in `intestazioni_ignorate`): intestazioni di identità e struttura (From, Sender, To, Cc, Bcc, Date, Message-ID, Subject, Return-Path, Received, DKIM-Signature, ARC-*, Authentication-Results, MIME-Version, Content-*), quelle che comandano il server di invio (X-Virtual-MTA, X-Job, X-EnvID, X-MSYS-API), X-Mailer, X-MailSenpai-* e quelle dei filtri di ricezione (X-MS-Exchange-*, X-Spam-*, X-Google-*…). Un valore non valido di un'intestazione accettata è un errore 400. A capo e caratteri di controllo vengono tolti; al massimo 30 intestazioni e 2000 caratteri per valore (References 4000); i valori non ASCII vengono codificati MIME.",
            "example": {
              "X-Ordine": "A-10293",
              "List-Unsubscribe": "<https://tuodominio.it/disiscrizione?u=123>, <mailto:disiscrizione@tuodominio.it>",
              "List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
              "Feedback-ID": "ordini:negozio:transazionale:tuodominio"
            }
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Alias di `intestazioni`."
          },
          "allegati": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/Allegato"
            },
            "description": "Al massimo 10 file e 10 MB in tutto (dopo la decodifica)."
          },
          "attachments": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/Allegato"
            },
            "description": "Alias di `allegati`."
          }
        },
        "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."
          },
          "intestazioni_ignorate": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Nomi delle intestazioni personalizzate non accettate (presente solo se ce ne sono, dalla 1.2)."
          }
        }
      },
      "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. Tratta gli identificativi come testo; 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."
          }
        }
      },
      "Allegato": {
        "type": "object",
        "required": [
          "nome",
          "contenuto"
        ],
        "properties": {
          "nome": {
            "type": "string",
            "maxLength": 150,
            "description": "Nome del file. Alias: `filename`. I file eseguibili (.exe, .js, .bat…) vengono rifiutati."
          },
          "contenuto": {
            "type": "string",
            "contentEncoding": "base64",
            "description": "Contenuto in base64. Alias: `content`."
          },
          "tipo": {
            "type": "string",
            "description": "Tipo MIME, per esempio application/pdf. Alias: `content_type`. Se manca: application/octet-stream."
          }
        }
      },
      "InvioCoda": {
        "type": "object",
        "required": [
          "ok",
          "in_coda",
          "risultati"
        ],
        "description": "Risposta 202 quando `a` ha più di 50 destinatari (dalla 1.2): le copie vanno in coda e partono entro un minuto. Ogni copia in coda ha il suo `id_messaggio`; l'evento `sent` (o `dropped` se non può partire) arriva su /eventi e sul webhook.",
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "inviati": {
            "type": "integer",
            "const": 0
          },
          "in_coda": {
            "type": "integer"
          },
          "non_inviati": {
            "type": "integer"
          },
          "risultati": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "a": {
                  "type": "string"
                },
                "inviato": {
                  "type": "boolean",
                  "const": false
                },
                "in_coda": {
                  "type": "boolean"
                },
                "id_messaggio": {
                  "type": "string"
                },
                "motivo": {
                  "type": "string"
                },
                "errore": {
                  "type": "string"
                }
              }
            }
          },
          "residuo": {
            "type": "integer"
          },
          "oltre_il_piano": {
            "type": "boolean"
          },
          "intestazioni_ignorate": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Nomi delle intestazioni personalizzate non accettate (presente solo se ce ne sono, dalla 1.2)."
          }
        }
      },
      "InvioMulti": {
        "type": "object",
        "required": [
          "ok",
          "inviati",
          "risultati"
        ],
        "description": "Risposta quando `a` è un elenco.",
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "inviati": {
            "type": "integer"
          },
          "non_inviati": {
            "type": "integer"
          },
          "risultati": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "a": {
                  "type": "string"
                },
                "inviato": {
                  "type": "boolean"
                },
                "id_messaggio": {
                  "type": "string"
                },
                "motivo": {
                  "type": "string",
                  "description": "Perché non è partita (soppressione)."
                },
                "errore": {
                  "type": "string",
                  "description": "Errore del server di invio o volume finito per quel destinatario."
                }
              }
            }
          },
          "residuo": {
            "type": "integer"
          },
          "oltre_il_piano": {
            "type": "boolean"
          },
          "intestazioni_ignorate": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Nomi delle intestazioni personalizzate non accettate (presente solo se ce ne sono, dalla 1.2)."
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Indirizzo https pubblico. Vuoto = spento."
          },
          "eventi": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "delivered",
                "bounce",
                "defer",
                "complaint",
                "open",
                "click",
                "dropped",
                "sent"
              ]
            }
          },
          "attivo": {
            "type": "boolean"
          },
          "segreto": {
            "type": "string",
            "description": "Solo nella risposta al POST: `whsec_…`, per verificare la firma."
          },
          "tipi_possibili": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "ultime_24_ore": {
            "type": "object",
            "properties": {
              "consegnati": {
                "type": "integer"
              },
              "in_attesa": {
                "type": "integer"
              },
              "falliti": {
                "type": "integer"
              }
            }
          }
        }
      },
      "EventoWebhook": {
        "type": "object",
        "description": "Corpo del POST che mandiamo al tuo indirizzo. Intestazioni: X-MailSenpai-Event, X-MailSenpai-Delivery, X-MailSenpai-Signature (t=<unix>,v1=<HMAC-SHA256 di \"<t>.<corpo>\" con il segreto>).",
        "properties": {
          "id": {
            "type": "string",
            "example": "evt_88214"
          },
          "type": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "properties": {
              "email": {
                "type": "string"
              },
              "domain": {
                "type": "string"
              },
              "from_domain": {
                "type": "string"
              },
              "code": {
                "type": "string"
              },
              "reason": {
                "type": "string"
              },
              "message_id": {
                "type": "string"
              },
              "url": {
                "type": "string"
              }
            }
          },
          "account": {
            "type": "string"
          },
          "test": {
            "type": "boolean",
            "description": "Solo negli eventi di prova."
          }
        }
      }
    },
    "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."
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "eventoRelay": {
      "post": {
        "summary": "Evento del relay (al tuo indirizzo)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventoWebhook"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rispondi 2xx entro 8 secondi."
          }
        }
      }
    }
  }
}