Riferimento API
Ogni endpoint, ogni campo e ogni risposta dell'API, generati dalla specifica OpenAPI. Per iniziare in cinque minuti parti dalla panoramica per sviluppatori.
La specifica è scritta in italiano e questa pagina la riporta così com'è, in ogni lingua del sito.
Panoramica per sviluppatoriScarica la specifica (openapi.yaml)
Versione della specifica: 1.13.0
Panoramica
- Produzione
- https://api.mailronix.com
- Sviluppo locale
- http://localhost:8080
Documenta la superficie API di mailronix raggiungibile da un sistema esterno tramite API key: invio (ADR-0029, esteso da ADR-0030 con template grafici e variabili) e gestione del ciclo di vita dei contatti (ADR-0029 primo emendamento).
Cosa può fare una chiave: le capacità
Una chiave non può fare tutto ciò che questo documento descrive. Porta un insieme di capacità, scelto quando la crei in console e non più modificabile: per aggiungerne una crei una chiave nuova e ritiri la vecchia. Non c'è un default e non c'è un insieme completo — una chiave fa esattamente ciò per cui l'hai creata.
| Capacità | Endpoint |
|---|---|
email:send |
POST /email/send |
contacts:write |
POST /contacts |
contacts:unsubscribe |
POST /contacts/revoke |
contacts:delete |
POST /contacts/delete |
revocations:read |
GET /contacts/revocations |
email:read |
GET /email/{id} |
Una richiesta a un endpoint per cui la chiave non ha la capacità
riceve 403 capability_not_granted. È un rifiuto definitivo: crea
una chiave con le capacità che ti servono, non ritentare.
revocations:read non è implicita in nessun'altra capacità. In
particolare non la ottieni con contacts:unsubscribe: scrivere una
revoca è un'operazione di cui conosci già l'esito e il cui oggetto hai
fornito tu, leggere l'elenco è l'unica operazione di questa API che ti
restituisce indirizzi che non ci hai dato. Sono due autorizzazioni
diverse, e le scegli tu alla creazione della chiave. Nessuna chiave
creata prima dell'introduzione di questa capacità l'ha acquistata.
email:read non è implicita in email:send, per lo stesso
motivo: inviare e leggere l'esito di un invio sono due autorizzazioni
distinte. Una chiave che sa solo inviare non può interrogare lo stato
di nulla.
contacts:delete non è concedibile senza contacts:unsubscribe.
Una chiave che può cancellare ma non revocare porta a implementare
«l'utente ha tolto la spunta» come una cancellazione: la disiscrizione
non viene registrata, la sincronizzazione successiva ricrea il
contatto, e parte una nuova richiesta di conferma verso chi si era
appena disiscritto.
Il consenso lo raccogliamo noi
L'API non consente in alcun modo di creare un Contatto già confermato né di dichiarare un consenso raccolto altrove. Un contatto creato via API nasce non confermato, e la richiesta di conferma parte da noi con la stessa prova di sempre: è il click del destinatario a valere come consenso, non la tua dichiarazione.
Ogni altro endpoint del backend (gestione API key, template, campagne, billing, area riservata) richiede una sessione browser autenticata via cookie ed è un'azione di prodotto, non una superficie di integrazione server-to-server — deliberatamente fuori perimetro di questo documento (ADR-0032). Non esiste alcun modo di far partire una campagna via API.
Webhook in uscita: noi chiamiamo te
Per non fare polling puoi registrare, dalla console (non via API),
uno o più indirizzi HTTPS a cui mailronix invia gli eventi del tuo
account. Endpoint attivi: Free 1, Starter 5, Growth 10. Ogni evento è
un POST con corpo JSON firmato: la forma, la verifica della firma e i
tentativi sono nella sezione webhooks di questo documento. Non
esistono eventi di apertura: non le rileviamo. L'evento email.clicked
esiste solo per i brand che hanno acceso, dalla console, il
tracciamento dei click (spento per impostazione iniziale), e descrive un
click rilevato da un destinatario che aveva acconsentito: scanner
e anteprime possono generarlo senza un gesto umano.
Autenticazione
apiKeyTipo: http bearer
API key generata dalla console (POST /api-keys, fuori perimetro di questo documento), formato mrx_live_<segreto>. Mostrata in chiaro una sola volta al momento della creazione. Autentica esclusivamente gli endpoint descritti in questo documento, e solo quelli per cui porta la capacità corrispondente — nessun altro endpoint del backend accetta questo meccanismo.
Una sola lettura, e va concessa esplicitamente. GET /contacts/revocations — la coda delle revoche del tuo brand — è l'unico endpoint di questa API che ti restituisce dati che non gli hai dato tu, e richiede la capacità revocations:read, che non è implicita in nessun'altra.
Tutto il resto una chiave non può interrogarlo: né i tuoi contatti, né i tuoi brand, né sé stessa. Non esiste alcun endpoint che dica a una chiave quale brand ha, quali capacità porta, quando scade o se è mai stata usata. Ciò che la tua integrazione deve sapere di sé lo apprende dall'ESITO delle operazioni che compie — i codici di errore sono parte del contratto e sono stabili.
⚠️ Tratta una chiave con revocations:read come un dato personale in movimento. Senza quella capacità, una chiave finita in un repository pubblico non può annunciare a chi appartiene; con quella capacità, può scaricare gli indirizzi di chi si è disiscritto dal tuo brand. Concedila solo alle integrazioni che devono riconciliare, e ritirala quando smettono.
Ogni chiave è legata a un brand, scelto alla creazione e non più modificabile: è il brand sotto cui tutti i suoi invii partono. Con più brand si creano più chiavi.
sessionCookieTipo: apiKey
Sessione browser autenticata della console — usarla per testare questo endpoint dagli strumenti di sviluppo del browser mentre si è loggati; un'integrazione server-to-server userà normalmente apiKey.
Endpoint
POST/email/send
Invia un'email transazionale
Autenticazione: una tra apiKey, sessionCookie
Accoda un'email transazionale per l'invio. La categoria è sempre
transaction_receipt, determinata esclusivamente lato server —
non esiste alcun modo di selezionarla dalla richiesta.
Il corpo del messaggio si specifica in una delle due modalità seguenti, mai entrambe nella stessa richiesta:
- Contenuto diretto:
subjecte almeno uno trahtml_body/text_body. - Da template:
template_id(creato in console,POST /email-templates, fuori perimetro di questo documento) evariables, una mappanome → valoresostituita nei segnaposto{{nome}}del template.
Il brand del messaggio
Ogni invio parte sotto un brand — il marchio con cui il destinatario ti riconosce. Su questo endpoint il brand non si dichiara: si ricava dalla chiave API che autentica la richiesta, a cui è stato legato quando l'hai creata in console. Il legame è immutabile: una chiave scrive sempre e solo sotto quel brand, qualunque cosa accada alla configurazione del tuo account nel frattempo.
Se hai più brand, crei una chiave per brand. Questo è anche ciò che limita il danno se una chiave ti sfugge: porta un brand solo.
Il campo brand_id è facoltativo e serve a una cosa sola,
proteggerti da uno scambio di chiavi fra due integrazioni: se lo
mandi, deve coincidere con il brand della chiave, altrimenti la
richiesta è rifiutata con 403 brand_mismatch. Non può
scegliere il brand — con una chiave lo scambio di segreti è
altrimenti invisibile, perché produrrebbe messaggi validi sotto il
marchio sbagliato.
Se autentichi con la sessione cookie della console (per una prova dagli strumenti di sviluppo) il brand va invece dichiarato, ed è obbligatorio: lì non c'è una credenziale che lo porti.
La risposta di successo è identica indipendentemente dal fatto che il destinatario sia effettivamente recapitabile o si trovi in una suppression list per bounce/complaint pregressi (ADR-0017): nessun campo della risposta permette di distinguere i due casi, per non offrire a un chiamante un modo di verificare l'esistenza/validità di un indirizzo email altrui.
Il flag marketing
Per impostazione predefinita questo endpoint invia messaggi transazionali: ricevute, conferme di operazione, notifiche di servizio. Sono inviabili a chiunque, anche a chi non è un tuo contatto e anche a chi si è disiscritto — la loro base giuridica non è il consenso, ed è la ragione per cui chi si disiscrive continua a ricevere gli avvisi di sicurezza del tuo servizio.
Con "marketing": true dichiari che il messaggio è una
comunicazione promozionale. La dichiarazione può soltanto
TOGLIERE: aggiunge controlli e non ne rimuove nessuno, e non
esiste alcun valore di questo campo che qualifichi un messaggio
come transazionale — quella qualifica è determinata dal server e
non è dichiarabile.
Un messaggio marketing:
- parte solo verso un indirizzo che è un tuo contatto di quel brand e che ha confermato l'iscrizione;
- non parte verso chi ha revocato il consenso presso quel brand;
- porta automaticamente il link di disiscrizione nel corpo e
l'header
List-Unsubscribe(RFC 8058). Non c'è modo di ometterli; - si ferma se il canale del tuo brand è sospeso, come una campagna.
Se il destinatario non è eleggibile la risposta è
403 recipient_not_eligible_for_marketing. Un codice solo per
due casi diversi — non è un tuo contatto confermato, oppure ha
revocato: distinguerli direbbe a chi possiede la chiave se una
data persona è tua cliente e se ti ha detto di no. Per raccogliere
il consenso usa POST /contacts.
Le due parti del messaggio: HTML e testo
Il destinatario riceve sempre un messaggio a due parti
(multipart/alternative, HTML e testo semplice) con entrambe
popolate: una parte vuota non viene mai spedita. Se ne fornisci
una sola, l'altra viene derivata da quella che hai mandato; se le
fornisci entrambe, partono come le hai scritte, senza modifiche.
Una parte contenente solo spazi vale come assente.
Questo conta perché in un multipart/alternative una parte vuota
non equivale a una parte assente: il client sceglie
l'alternativa più ricca, la trova vuota e mostra un messaggio
bianco. Mandare una sola delle due parti è quindi un'integrazione
legittima e sicura, non un mezzo messaggio.
Vale allo stesso modo per la modalità template_id: un template
produce solo HTML, quindi la parte testo è sempre derivata.
Se mandi solo html_body
La parte testo è il tuo HTML ridotto a testo leggibile:
- i link diventano
etichetta(url): l'indirizzo compare in chiaro, fra parentesi, subito dopo il testo del link. Nessun link va quindi perso per chi legge la versione testuale — un link di conferma, di disiscrizione o di annullamento deve restare raggiungibile in entrambe le parti, ed è un requisito legale, non una scelta estetica. L'URL è omesso solo quando non aggiunge nulla: ancora interna (#...) o etichetta già identica all'indirizzo; - le immagini diventano il loro testo alternativo fra parentesi
quadre (
[Logo]); quelle senzaaltspariscono; - le voci di elenco diventano righe che iniziano con
-; - paragrafi e titoli restano separati da una riga vuota, righe e
celle di tabella da un a capo semplice;
<br>resta un a capo singolo; <script>,<style>e il<title>non finiscono nel testo.
Esempio — questo html_body:
<p>Ordine #4821 confermato.
<a href="https://negozio.example/ordini/4821">Vedi il dettaglio</a></p>
produce questa parte testo:
Ordine #4821 confermato. Vedi il dettaglio (https://negozio.example/ordini/4821)
Se mandi solo text_body
La parte HTML è il tuo testo avvolto in un layout minimo e
neutro: le righe vuote separano i paragrafi, gli a capo singoli
restano tali, gli URL riconoscibili diventano cliccabili. Il
testo è sempre escapato — non viene interpretato come markup —
quindi eventuali < e & arrivano come li hai scritti.
Il layout non aggiunge il marchio mailronix né alcuna riga di piè di pagina: il messaggio parte dal tuo dominio verificato e deve continuare a essere riconoscibile come tuo.
Se vuoi controllo pieno sulle due parti
Mandale entrambe. La derivazione è una rete di sicurezza per chi ne fornisce una sola, non una riscrittura di ciò che fornisci: una parte che hai valorizzato non viene mai toccata.
Ritentare in sicurezza: Idempotency-Key
Un timeout o un errore di rete non ti dice se la richiesta è
stata accodata: ritentare senza precauzioni può duplicare
l'invio. Manda l'header Idempotency-Key (una stringa a tua
scelta, univoca per questo invio — un id d'ordine, per esempio) e
un retry con lo stesso header e lo stesso corpo riceve la
risposta della prima chiamata, email_log_id incluso, senza
accodare una seconda volta.
- Valida per 24 ore dalla prima chiamata; dopo, lo stesso valore può essere riusato per un invio nuovo.
- Legata alla chiave API: solo sull'autenticazione a chiave, mai a sessione. Due chiavi diverse non collidono mai sullo stesso valore, nemmeno dello stesso brand.
- Stesso header, corpo diverso dalla prima chiamata →
422 idempotency_key_body_mismatch. Non è un'ambiguità risolta in silenzio: riusare un nome per una richiesta diversa è un errore da correggere lato tuo, non un'esecuzione alternativa. - Facoltativo: senza l'header il comportamento è quello di sempre, un retry è un secondo invio.
Parametri
Idempotency-Keystring · nell'intestazionefacoltativoVedi "Ritentare in sicurezza" sopra. Ignorato silenziosamente sull'autenticazione a sessione cookie.
Corpo della richiesta · obbligatorio
Variante 1: SendRequestDirect
Almeno uno tra html_body/text_body è obbligatorio insieme a subject. Non deve comparire template_id. Fornirne uno solo è legittimo: la parte mancante viene derivata e il destinatario riceve comunque entrambe, mai una vuota.
fromstring (email)obbligatorioIndirizzo mittente. Il dominio deve essere verificato per il tenant.
from_namestringfacoltativoNome che il destinatario legge al posto dell'indirizzo, per esempio
Acme Assistenza. Facoltativo: se manca si usa il nome impostato sul brand, e se nemmeno il brand ne ha uno il messaggio parte con il solo indirizzo, come prima. Da 1 a 78 caratteri; non può contenere@,<,>, virgolette, a-capo né indirizzi web (il nome di un mittente non deve poter sembrare un altro indirizzo). I caratteri accentati e non latini sono ammessi. Un valore non valido produce400 invalid_from_name. Non cambia l'indirizzofrom, che resta soggetto agli stessi controlli sul dominio.- Vincoli: lunghezza minima 1; lunghezza massima 78
reply_tostring (email)facoltativoIndirizzo a cui vanno le risposte dei destinatari a questo messaggio. Facoltativo; può essere su qualunque dominio, non deve coincidere con quello del brand, ma non può essere un indirizzo del dominio di mailronix. Un solo indirizzo, senza nome visualizzato. Prevale sul Reply-To impostato sul brand. Richiede il consenso del brand: l'impostazione «Consenti reply_to nella richiesta» si attiva dalla console con la sessione ed è spenta di default; finché è spenta la richiesta riceve
403 reply_to_not_allowed. Non si attiva con una API key, di proposito: una chiave sottratta non deve poter reindirizzare le risposte dei tuoi destinatari. Un indirizzo non valido produce400 invalid_reply_to.languagestringfacoltativoLingua di questo messaggio, a scelta di chi lo invia. Facoltativa; senza, vale la lingua di comunicazione del marchio al momento dell'invio. Determina la lingua del piè di pagina di disiscrizione dei messaggi marketing e, se il marchio ha acceso il tracciamento dei click, del link «Preferenze» e delle pagine a cui porta, compresa la richiesta di consenso: la lingua scelta qui resta legata a questo messaggio e non cambia se in seguito cambia la lingua del marchio. Accetta anche la forma con regione (
it-IT), ridotta alla lingua. Un valore che non è una delle cinque produce400 invalid_language: non c'è un ripiego silenzioso.- Valori ammessi:
it,en,es,de,fr attachmentsarray di valorifacoltativoAllegati di questo messaggio. Facoltativo; fino a 5 per messaggio, 7 MB in totale (dimensione dopo la decodifica base64), con l'email completa sotto 10 MB: in email gli allegati sono codificati in base64 e pesano circa un terzo in più, quindi 7 MB di file sono circa 9,6 MB di messaggio e il resto è lo spazio per il corpo. Solo su messaggi transazionali: con
marketing: truela richiesta riceve400 attachments_not_allowed_for_marketing. Ammessi contemplate_ide su tutti i piani. Ogni allegato parte sempre come allegato, mai incorporato nel corpo. Conservazione: i byte restano nel nostro database solo il tempo dell'invio, e sono cancellati alla consegna a SES o all'esito definitivo, in ogni caso dopo 24 ore al massimo (più pochi minuti di pulizia); il log del messaggio conserva solo nome, tipo, dimensione e impronta SHA-256, mai il contenuto. Non c'è scansione antivirus: ciò che alleghi è responsabilità tua. Un ritentativo dello stesso invio con la stessaIdempotency-Keydeve rimandare gli stessi byte. Il tetto di 10 MB sull'email è scelto perché molti server di destinazione rifiutano messaggi più grandi (Gmail e Outlook arrivano a 20-25 MB); un rifiuto del server del destinatario compare comunque come bounce.- Vincoli: al massimo 5 elementi
attachments[].filenamestringobbligatorioNome del file come lo vedrà il destinatario, con l'estensione coerente con
content_type(per esempiofattura.pdf). Percorsi e caratteri di controllo sono rimossi; oltre 100 caratteri il nome è accorciato conservando l'estensione.- Vincoli: lunghezza minima 1; lunghezza massima 100
attachments[].content_typestringobbligatorioElenco chiuso. Il contenuto deve corrispondere al tipo dichiarato (controllo sui primi byte per PDF e immagini, UTF-8 valido per i tipi di testo); altrimenti
400 attachment_type_not_allowed. Eseguibili, archivi e documenti Office non sono ammessi.- Valori ammessi:
application/pdf,image/png,image/jpeg,image/gif,text/plain,text/csv,text/calendar attachments[].contentstring (byte)obbligatorioContenuto del file in base64 standard (RFC 4648 §4, con padding), senza prefisso
data:.tostring (email)obbligatoriosubjectstringobbligatoriohtml_bodystringfacoltativoCorpo HTML. Obbligatorio se
text_bodyè assente. Se lo mandi da solo, la parte testo del messaggio viene derivata da qui — link resi comeetichetta (url); vedi "Le due parti del messaggio" nella descrizione dell'operazione.text_bodystringfacoltativoCorpo testo semplice. Obbligatorio se
html_bodyè assente. Se lo mandi da solo, la parte HTML viene derivata da qui, in un layout neutro senza marchio mailronix; vedi "Le due parti del messaggio" nella descrizione dell'operazione.brand_idstring (uuid)facoltativoCon una API key: facoltativo, e vale come asserzione — se presente deve coincidere con il brand a cui la chiave è legata, altrimenti
403 brand_mismatch. Non sceglie mai il brand. Con la sessione cookie: obbligatorio. Vedi "Il brand del messaggio" nella descrizione dell'operazione.marketingbooleanfacoltativoDichiara che questo messaggio è marketing. Vedi "Il flag marketing" nella descrizione dell'operazione: aggiunge controlli, non ne toglie nessuno, e non esiste alcun valore che possa qualificare un messaggio come transazionale.
- Predefinito:
false
Variante 2: SendRequestTemplate
Alternativa a SendRequestDirect: non devono comparire subject/ html_body/text_body.
fromstring (email)obbligatoriofrom_namestringfacoltativoNome che il destinatario legge al posto dell'indirizzo, per esempio
Acme Assistenza. Facoltativo: se manca si usa il nome impostato sul brand, e se nemmeno il brand ne ha uno il messaggio parte con il solo indirizzo, come prima. Da 1 a 78 caratteri; non può contenere@,<,>, virgolette, a-capo né indirizzi web (il nome di un mittente non deve poter sembrare un altro indirizzo). I caratteri accentati e non latini sono ammessi. Un valore non valido produce400 invalid_from_name. Non cambia l'indirizzofrom, che resta soggetto agli stessi controlli sul dominio.- Vincoli: lunghezza minima 1; lunghezza massima 78
reply_tostring (email)facoltativoIndirizzo a cui vanno le risposte dei destinatari a questo messaggio. Facoltativo; può essere su qualunque dominio, non deve coincidere con quello del brand, ma non può essere un indirizzo del dominio di mailronix. Un solo indirizzo, senza nome visualizzato. Prevale sul Reply-To impostato sul brand. Richiede il consenso del brand: l'impostazione «Consenti reply_to nella richiesta» si attiva dalla console con la sessione ed è spenta di default; finché è spenta la richiesta riceve
403 reply_to_not_allowed. Non si attiva con una API key, di proposito: una chiave sottratta non deve poter reindirizzare le risposte dei tuoi destinatari. Un indirizzo non valido produce400 invalid_reply_to.languagestringfacoltativoLingua di questo messaggio, a scelta di chi lo invia. Facoltativa; senza, vale la lingua di comunicazione del marchio al momento dell'invio. Determina la lingua del piè di pagina di disiscrizione dei messaggi marketing e, se il marchio ha acceso il tracciamento dei click, del link «Preferenze» e delle pagine a cui porta, compresa la richiesta di consenso: la lingua scelta qui resta legata a questo messaggio e non cambia se in seguito cambia la lingua del marchio. Accetta anche la forma con regione (
it-IT), ridotta alla lingua. Un valore che non è una delle cinque produce400 invalid_language: non c'è un ripiego silenzioso.- Valori ammessi:
it,en,es,de,fr attachmentsarray di valorifacoltativoAllegati di questo messaggio. Facoltativo; fino a 5 per messaggio, 7 MB in totale (dimensione dopo la decodifica base64), con l'email completa sotto 10 MB: in email gli allegati sono codificati in base64 e pesano circa un terzo in più, quindi 7 MB di file sono circa 9,6 MB di messaggio e il resto è lo spazio per il corpo. Solo su messaggi transazionali: con
marketing: truela richiesta riceve400 attachments_not_allowed_for_marketing. Ammessi contemplate_ide su tutti i piani. Ogni allegato parte sempre come allegato, mai incorporato nel corpo. Conservazione: i byte restano nel nostro database solo il tempo dell'invio, e sono cancellati alla consegna a SES o all'esito definitivo, in ogni caso dopo 24 ore al massimo (più pochi minuti di pulizia); il log del messaggio conserva solo nome, tipo, dimensione e impronta SHA-256, mai il contenuto. Non c'è scansione antivirus: ciò che alleghi è responsabilità tua. Un ritentativo dello stesso invio con la stessaIdempotency-Keydeve rimandare gli stessi byte. Il tetto di 10 MB sull'email è scelto perché molti server di destinazione rifiutano messaggi più grandi (Gmail e Outlook arrivano a 20-25 MB); un rifiuto del server del destinatario compare comunque come bounce.- Vincoli: al massimo 5 elementi
attachments[].filenamestringobbligatorioNome del file come lo vedrà il destinatario, con l'estensione coerente con
content_type(per esempiofattura.pdf). Percorsi e caratteri di controllo sono rimossi; oltre 100 caratteri il nome è accorciato conservando l'estensione.- Vincoli: lunghezza minima 1; lunghezza massima 100
attachments[].content_typestringobbligatorioElenco chiuso. Il contenuto deve corrispondere al tipo dichiarato (controllo sui primi byte per PDF e immagini, UTF-8 valido per i tipi di testo); altrimenti
400 attachment_type_not_allowed. Eseguibili, archivi e documenti Office non sono ammessi.- Valori ammessi:
application/pdf,image/png,image/jpeg,image/gif,text/plain,text/csv,text/calendar attachments[].contentstring (byte)obbligatorioContenuto del file in base64 standard (RFC 4648 §4, con padding), senza prefisso
data:.tostring (email)obbligatoriotemplate_idstring (uuid)obbligatorioId di un template creato in console (
POST /email-templates, fuori perimetro di questo documento), di proprietà dello stesso tenant a cui appartiene la chiave/sessione usata. Un template produce solo la parte HTML: la parte testo del messaggio viene sempre derivata da quell'HTML (vedi "Le due parti del messaggio" nella descrizione dell'operazione).variablesobjectfacoltativoMappa
nome variabile → valore, sostituita nei segnaposto{{nome_variabile}}del template. Una variabile referenziata dal template ma assente qui produce400 missing_template_variable.brand_idstring (uuid)facoltativoCome in
SendRequestDirect: asserzione facoltativa con una API key, obbligatorio con la sessione cookie.marketingbooleanfacoltativoDichiara che questo messaggio è marketing. Vedi "Il flag marketing" nella descrizione dell'operazione: aggiunge controlli, non ne toglie nessuno, e non esiste alcun valore che possa qualificare un messaggio come transazionale.
- Predefinito:
false
Esempi
Contenuto diretto (senza template)
{
"from": "notifiche@cliente-esempio.com",
"to": "utente@destinatario.com",
"subject": "Conferma ordine #4821",
"html_body": "<p>Il tuo ordine #4821 è confermato.</p>"
}Con brand_id come asserzione (facoltativo)
{
"from": "notifiche@cliente-esempio.com",
"to": "utente@destinatario.com",
"subject": "Conferma ordine #4821",
"html_body": "<p>Il tuo ordine #4821 è confermato.</p>",
"brand_id": "3f2b8c1d-9a4e-4c7b-8e5f-1a2b3c4d5e6f"
}Solo testo (la parte HTML viene derivata)
{
"from": "notifiche@cliente-esempio.com",
"to": "utente@destinatario.com",
"subject": "Conferma ordine #4821",
"text_body": "Il tuo ordine #4821 è confermato.\n\nDettaglio: https://negozio.example/ordini/4821"
}Da template con variabili
{
"from": "notifiche@cliente-esempio.com",
"to": "utente@destinatario.com",
"template_id": "5f1d6c2e-3b4a-4f0e-9c1a-2d8e7f6a1b3c",
"variables": {
"nome_cliente": "Maria",
"numero_ordine": "4821"
}
}Risposte
202
Accodata per l'invio, con entrambe le parti del messaggio popolate (quella non fornita è derivata al momento dell'invio). Identica sia che il destinatario venga effettivamente recapitato sia che sia silenziosamente scartato per suppression list (ADR-0017) — vedi descrizione sopra.
statusstringobbligatorio- Valori ammessi:
queued email_log_idstring (uuid)obbligatorio
{
"status": "queued",
"email_log_id": "9c4e2f1a-7d3b-4a5e-8f6c-1b2a3d4e5f6a"
}400
Richiesta non valida.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
Payload malformato o campi incompatibili/mancanti
{
"error": {
"code": "invalid_request",
"message": "from e to sono obbligatori."
}
}Autenticazione via sessione senza brand_id
{
"error": {
"code": "invalid_request",
"message": "brand_id è obbligatorio: indica il brand da cui parte questo invio."
}
}from non è un indirizzo email
{
"error": {
"code": "invalid_request",
"message": "from non è un indirizzo email valido."
}
}from_name non valido
{
"error": {
"code": "invalid_from_name",
"message": "from_name non è valido: da 1 a 78 caratteri, senza «@», «<», «>», virgolette, a-capo o indirizzi web."
}
}reply_to non valido
{
"error": {
"code": "invalid_reply_to",
"message": "reply_to non è un indirizzo email valido: serve un solo indirizzo, senza nome e senza spazi."
}
}language non valida
{
"error": {
"code": "invalid_language",
"message": "language deve essere una tra it, en, es, de, fr."
}
}Più di 5 allegati
{
"error": {
"code": "too_many_attachments",
"message": "Troppi allegati: al massimo 5 per messaggio."
}
}Allegato non valido
{
"error": {
"code": "invalid_attachment",
"message": "Allegato non valido: servono filename, content_type e content in base64 standard, non vuoti."
}
}Tipo di allegato non ammesso
{
"error": {
"code": "attachment_type_not_allowed",
"message": "Tipo di allegato non ammesso. Sono ammessi application/pdf, image/png, image/jpeg, image/gif, text/plain, text/csv e text/calendar; il contenuto e l'estensione del nome devono corrispondere al tipo dichiarato."
}
}Allegati su un messaggio marketing
{
"error": {
"code": "attachments_not_allowed_for_marketing",
"message": "Un messaggio marketing non può avere allegati: gli allegati sono ammessi solo sui messaggi transazionali."
}
}Variabile del template non fornita
{
"error": {
"code": "missing_template_variable",
"message": "Il template referenzia una variabile non fornita."
}
}401
Nessuna autenticazione valida — né sessione cookie né API key (mancante, malformata, sconosciuta o revocata).
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
{
"error": {
"code": "unauthenticated",
"message": "Autenticazione richiesta."
}
}403
Richiesta rifiutata da un gate applicativo.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
Il mittente non è sul dominio del brand dell'invio
{
"error": {
"code": "from_address_not_of_brand",
"message": "Il dominio del mittente non è quello del brand da cui parte questo invio: un brand invia dal proprio dominio (o da un suo sottodominio verificato). Vedi il dominio del brand su GET /brands."
}
}Dominio del mittente non verificato
{
"error": {
"code": "domain_not_verified",
"message": "Il dominio del mittente non è verificato per questo tenant."
}
}Tenant sospeso
{
"error": {
"code": "tenant_suspended",
"message": "Il tenant è sospeso."
}
}Limite giornaliero di destinatari unici del piano Free raggiunto
{
"error": {
"code": "free_daily_recipients_exceeded",
"message": "Limite giornaliero di destinatari unici del piano Free raggiunto."
}
}Limite mensile del piano superato
{
"error": {
"code": "plan_limit_exceeded",
"message": "Limite mensile del piano superato."
}
}reply_to non consentito dal brand
{
"error": {
"code": "reply_to_not_allowed",
"message": "Questo brand non consente di indicare un reply_to nella richiesta: attivalo nelle impostazioni del brand in console."
}
}brand_id diverso da quello della chiave
{
"error": {
"code": "brand_mismatch",
"message": "Il brand dichiarato non è quello a cui questa chiave è legata: la chiave scrive sotto un brand solo, e il campo brand_id può solo confermarlo."
}
}404
template_id o brand_id inesistente, oppure appartenente a un altro tenant — stessa risposta in entrambi i casi, per non rivelare l'esistenza di template o brand altrui.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
{
"error": {
"code": "template_not_found",
"message": "Template non trovato."
}
}409
Uno stato del brand che il tenant può sciogliere da sé. brand_archived: il brand è archiviato e in scrittura è rifiutato — riattivalo dalla console, oppure, se era la chiave a esserne legata, creane una nuova sul brand giusto. brand_sender_not_configured: il brand non ha ancora un indirizzo mittente, quindi non è ancora un mittente e non può inviare in proprio nome.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
Brand archiviato
{
"error": {
"code": "brand_archived",
"message": "Il brand è archiviato: riattivalo prima di usarlo."
}
}Il brand non ha ancora un indirizzo mittente
{
"error": {
"code": "brand_sender_not_configured",
"message": "Questo brand non ha ancora un indirizzo mittente: impostane uno, su un dominio verificato, prima di inviare in suo nome."
}
}413
Corpo o allegati troppo grandi. attachments_too_large: gli allegati superano 7 MB in totale, misurati dopo la decodifica base64, oppure l'email completa (allegati codificati in base64, che pesano circa un terzo in più del file, più il corpo) supererebbe 10 MB. payload_too_large: il corpo supera il limite di lettura (12 MiB con allegati; 64 KiB senza).
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
Allegati oltre 7 MB in totale, o email oltre 10 MB
{
"error": {
"code": "attachments_too_large",
"message": "Gli allegati superano 7 MB in totale (dimensione dopo la decodifica base64)."
}
}Corpo della richiesta troppo grande
{
"error": {
"code": "payload_too_large",
"message": "Corpo della richiesta troppo grande."
}
}422
idempotency_key_body_mismatch — l'header Idempotency-Key presentato è già associato a una richiesta precedente con un corpo diverso. Usa un valore nuovo per una richiesta diversa.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
{
"error": {
"code": "idempotency_key_body_mismatch",
"message": "Questa Idempotency-Key è già stata usata con un corpo diverso."
}
}429
rate_limited: rate limit per chiave API superato. Applicabile solo quando l'autenticazione avviene via Authorization: Bearer — il limite è per chiave, non per IP, e non si applica all'autenticazione via sessione cookie. attachments_backlog: il tuo account ha già in attesa di invio più di 70 MB di allegati; la richiesta è corretta e nulla è stato scritto. Riprova dopo qualche secondo (header Retry-After), con la stessa Idempotency-Key e gli stessi byte. server_busy: il server sta già trattando il massimo di richieste con corpo grande (oltre 64 KiB, quindi di norma quelle con allegati); la richiesta è corretta, riprova dopo qualche secondo. Non riguarda le richieste normali.
Intestazioni della risposta
Retry-After(integer)Secondi da attendere prima di riprovare (solo con
attachments_backlogeserver_busy).
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
Rate limit per chiave
{
"error": {
"code": "rate_limited",
"message": "Troppe richieste, riprova più tardi."
}
}Troppe richieste con corpo grande in corso
{
"error": {
"code": "server_busy",
"message": "Troppe richieste con corpo grande in corso: riprova fra qualche secondo."
}
}Troppi allegati in attesa di invio
{
"error": {
"code": "attachments_backlog",
"message": "Hai troppi allegati in attesa di invio: riprova fra qualche secondo, quando quelli già accodati saranno partiti."
}
}500
Errore interno.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
{
"error": {
"code": "internal_error",
"message": "Errore interno."
}
}503
Verifica della chiave API o dello stato del tenant temporaneamente non disponibile. Applicabile solo all'autenticazione via API key.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
{
"error": {
"code": "auth_unavailable",
"message": "Verifica della chiave non disponibile."
}
}GET/email/{id}
Leggi lo stato del tuo invio
Autenticazione: una tra apiKey, sessionCookie
Restituisce lo stato dell'invio identificato da email_log_id —
il valore che POST /email/send ti ha restituito. Non esiste un
modo di scoprire l'id di un invio che non è il tuo: un id
inesistente o appartenente a un altro brand riceve lo stesso
404, senza distinzione.
Gli stati, e perché sono meno di quelli interni
status |
significato |
|---|---|
queued |
accodato, non ancora concluso |
sent |
consegnato con successo |
complained |
consegnato, il destinatario ha segnalato come spam |
undelivered |
non arrivato |
undelivered copre sia un bounce reale sia un indirizzo già
soppresso per bounce/complaint pregressi (ADR-0017), e la
distinzione è deliberata, non un'approssimazione provvisoria.
Un indirizzo soppresso non genera alcun invio reale: se questo
endpoint lo segnalasse come caso distinto, chiunque potrebbe
mandare POST /email/send a un indirizzo qualsiasi e usare
questo endpoint per scoprire gratis se è già soppresso per il
tuo brand — lo stesso oracolo che POST /email/send chiude
restituendo sempre lo stesso 202, spostato di una chiamata.
Con undelivered unico sai comunque che il messaggio non è
arrivato — la domanda a cui questo endpoint risponde.
Il valore da solo non basta: anche il momento in cui questo
endpoint smette di rispondere queued è parte del presidio.
Un indirizzo soppresso si risolve nell'istante stesso dell'invio;
un bounce vero richiede il giro reale (coda, invio, notifica del
provider) e non può concludersi altrettanto in fretta. Per questo
una richiesta fatta a meno di qualche secondo dall'invio risponde
sempre queued, anche se l'esito è già determinato: senza
questo, la rapidità della risposta rivelerebbe la stessa
informazione che il valore unico undelivered nasconde.
Parametri
idstring (uuid) · nel percorsoobbligatorioIl valore
email_log_idricevuto daPOST /email/send.
Risposte
200
Stato dell'invio.
email_log_idstring (uuid)obbligatoriostatusstringobbligatorio- Valori ammessi:
queued,sent,complained,undelivered
{
"email_log_id": "9c4e2f1a-7d3b-4a5e-8f6c-1b2a3d4e5f6a",
"status": "sent"
}401
Credenziale assente, non valida o revocata.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
403
capability_not_granted — serve email:read; api_key_disabled.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
404
L'id non esiste, oppure appartiene a un brand diverso da quello della chiave — stessa risposta in entrambi i casi.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
429
Rate limit per chiave API superato.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
POST/contacts
Crea un contatto e avvia la conferma double opt-in
Autenticazione: una tra apiKey, sessionCookie
Crea un contatto non confermato sotto il brand della chiave e gli invia la richiesta di conferma dell'iscrizione.
Il consenso non si dichiara
Questo endpoint non consente in alcun modo di creare un contatto già confermato né di dichiarare un consenso raccolto altrove. Il contatto nasce in attesa, la richiesta di conferma parte da noi, e ciò che vale come consenso è il click del destinatario. È lo stesso identico flusso della console: cambia come autentichi la chiamata, non la natura dell'atto.
Finché il contatto non conferma non riceve nessun messaggio
marketing — e un POST /email/send con "marketing": true verso
quell'indirizzo è rifiutato.
La risposta non dice cosa c'era prima
La risposta è la stessa — 204, corpo vuoto — che l'indirizzo
sia nuovo, che fosse già un tuo contatto, o che avesse revocato il
consenso presso questo brand. Non è una semplificazione: tre
risposte distinte permetterebbero a chi possiede la chiave di
verificare, un indirizzo per volta, se una data persona è tua
cliente e se ti ha detto di no.
Ne segue che questa risposta non restituisce alcun
identificativo, e che gli altri endpoint su questa superficie
indirizzano un contatto per (brand, email).
Se l'indirizzo aveva revocato presso questo brand, il contatto viene creato inerte: nessuna richiesta di conferma parte, e nessuna operazione tua può riattivarlo — solo una nuova iscrizione della persona stessa.
Corpo della richiesta · obbligatorio
emailstring (email)obbligatorionamestringfacoltativoNome del contatto, facoltativo.
brand_idstring (uuid)facoltativoCon una API key: facoltativo, e vale come asserzione — se presente deve coincidere con il brand della chiave, altrimenti
403 brand_mismatch. Non sceglie mai il brand. Con la sessione cookie: obbligatorio.languagestringfacoltativoLingua dell'iscrizione, a scelta di chi chiama l'API: è la lingua della pagina di conferma e, se il marchio ha acceso il tracciamento dei click, della richiesta di consenso che la persona vede e di cui conserviamo il testo. Facoltativa; senza, vale la lingua di comunicazione del marchio. Un valore che non è una delle cinque produce
400 invalid_language.- Valori ammessi:
it,en,es,de,fr
Risposte
201
Solo sul percorso a sessione cookie, che continua a distinguere i tre casi perché un operatore che aggiunge un indirizzo ha bisogno di sapere che c'era già.
204
Presa in carico. Stesso esito per indirizzo nuovo, contatto già esistente e contatto che aveva revocato.
400
Corpo non valido, indirizzo non valido, language non valida (invalid_language), o brand mancante sul percorso a sessione.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
401
Credenziale assente, non valida o revocata.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
403
capability_not_granted — la chiave non ha contacts:write; brand_mismatch — brand_id dichiarato diverso da quello della chiave; brand_marketing_suspended — il canale del brand è sospeso, e non si crea un contatto la cui conferma non partirebbe; api_key_disabled; tenant_suspended.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
409
brand_archived — riattiva il brand prima di usarlo; brand_sender_not_configured — il brand non ha ancora un indirizzo mittente: dichiara dominio e indirizzo mittente sul brand, perché la richiesta di conferma parte da lì; no_verified_domain — verifica il dominio DEL BRAND: la richiesta di conferma parte dall'indirizzo mittente di quel brand, non da un dominio qualunque del tuo account.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
429
Limite di richieste superato per questa chiave.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
POST/contacts/revoke
Registra la disiscrizione di un contatto
Autenticazione: una tra apiKey, sessionCookie
Revoca il consenso di un contatto del brand della chiave. È l'endpoint da chiamare quando una persona toglie il consenso nella tua applicazione.
Chiamalo tu, e non usare la cancellazione al suo posto. Una cancellazione non registra alcuna revoca: la sincronizzazione successiva ricrea il contatto, e gli parte una nuova richiesta di conferma — cioè scriviamo a una persona che si era appena disiscritta.
La revoca non si annulla
Non esiste alcun modo, per te o per noi, di annullare una revoca: il contatto potrà tornare a ricevere comunicazioni solo con una nuova iscrizione fatta da lui. Per questa ragione il limite di richieste su questa operazione non ti rallenta e basta: superarlo disabilita la chiave, e riattivarla richiede una persona che entri in console.
La risposta è la stessa che l'indirizzo fosse un contatto o no.
Puoi chiamarlo prima di averci mandato il contatto
Se l'indirizzo non è ancora un tuo contatto, la revoca viene
registrata lo stesso. Da quel momento quell'indirizzo non riceve
nulla per quel brand: se in seguito lo crei con POST /contacts,
il contatto nasce inerte, nessuna richiesta di conferma parte,
e nessuna campagna lo raggiunge.
È il caso della persona che toglie il consenso nella tua applicazione prima che tu ci abbia mai sincronizzato quel contatto — l'ordine è il tuo, non il nostro.
⚠️ Non usarlo su indirizzi che non hai motivo di negare. La riga scritta è definitiva anche per te: non si cancella, non si corregge, e non esiste un'operazione inversa. Un indirizzo revocato per errore torna a ricevere comunicazioni solo con una nuova iscrizione fatta dalla persona.
Ogni revoca porta il nome della chiave che l'ha scritta
Le revoche registrate da questo endpoint sono elencate in console, con la data, l'indirizzo e il prefisso della chiave che le ha registrate — anche quelle su indirizzi che non erano contatti.
Non è un dettaglio di interfaccia ed è bene che tu lo sappia scrivendo l'integrazione: se una chiave compromessa comincia a negare indirizzi, chi amministra l'account lo vede e sa quale chiave disabilitare. Le revoche registrate prima dell'introduzione di questa tracciatura restano senza attribuzione: non è stata indovinata a posteriori.
Corpo della richiesta · obbligatorio
emailstring (email)obbligatorioIl contatto si indirizza per
(brand, email). Non esiste un identificativo da usare qui: nessuna risposta di questa superficie ne restituisce uno.brand_idstring (uuid)facoltativoAsserzione facoltativa, come in
CreateContactRequest.
Risposte
204
Presa in carico. Stesso esito che l'indirizzo fosse un contatto o no.
400
Corpo o indirizzo non valido.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
401
Credenziale assente, non valida o revocata.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
403
capability_not_granted — serve contacts:unsubscribe; brand_mismatch; api_key_disabled — il limite sulle operazioni irreversibili ha spento questa chiave, e ritentare non la riattiva.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
409
brand_archived.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
POST/contacts/delete
Elimina un contatto
Autenticazione: una tra apiKey, sessionCookie
Elimina il contatto del brand della chiave.
Non è il modo di dire «si è disiscritto». Per quello esiste
POST /contacts/revoke, e la differenza è sostanziale: una
cancellazione rimuove il dato ma non registra alcuna revoca,
quindi non impedisce che un caricamento successivo ricrei il
contatto e faccia ripartire la richiesta di conferma.
Una revoca già registrata, invece, sopravvive alla
cancellazione: se hai revocato prima di cancellare, ricreare quel
contatto lo fa nascere inerte e nessun messaggio parte. È la
ragione per cui contacts:delete non è concedibile senza
contacts:unsubscribe.
POST e non DELETE perché il contatto si indirizza per (brand, email) e l'indirizzo viaggia nel corpo: metterlo in query string
lo scriverebbe nei log di ogni intermediario.
Un POST è verbo di metodo, non un'operazione idempotente
diversa: chiamarlo due volte sullo stesso indirizzo produce lo
stesso esito.
Corpo della richiesta · obbligatorio
emailstring (email)obbligatorioIl contatto si indirizza per
(brand, email). Non esiste un identificativo da usare qui: nessuna risposta di questa superficie ne restituisce uno.brand_idstring (uuid)facoltativoAsserzione facoltativa, come in
CreateContactRequest.
Risposte
204
Presa in carico. Stesso esito che l'indirizzo fosse un contatto o no.
400
Corpo o indirizzo non valido.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
401
Credenziale assente, non valida o revocata.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
403
capability_not_granted — serve contacts:delete, che richiede anche contacts:unsubscribe; brand_mismatch; api_key_disabled.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
409
brand_archived.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
GET/contacts/revocations
Scorri le revoche del tuo brand
Autenticazione: apiKey
Restituisce, in ordine cronologico di revoca, le disiscrizioni
registrate per il brand a cui questa chiave è legata — tutte,
non solo quelle che hai registrato tu: il click sul link di
disiscrizione di una nostra email, l'azione di chi amministra
l'account in console, e le tue chiamate a POST /contacts/revoke.
È l'endpoint da usare per riconciliare: sapere delle revoche avvenute fuori dalla tua integrazione, e smettere di scrivere a chi ha detto di no.
Il cursore
Il campo next_cursor è una stringa opaca: non interpretarla,
non costruirla, non derivarne una data. Conservala e rimandala in
since alla richiesta successiva.
Il cursore è stabile: due revoche registrate nello stesso istante non fanno saltare né ripetere una riga. È la ragione per cui non è una data — con una data, due disiscrizioni scritte nella stessa transazione (succede: un'importazione, una cancellazione a cascata) starebbero sullo stesso valore, e la seconda non ti arriverebbe mai.
Il cursore è legato alla chiave che lo ha ricevuto: presentato
da una chiave di un altro brand riceve 400 invalid_cursor. Non è
un token di autorizzazione e non ti dà accesso a nulla — è una
posizione dentro un insieme che sei già autorizzato a leggere per
intero.
Come si sincronizza
- prima volta: chiama senza
sincee scorri finchéhas_moreèfalse; - conserva l'ultimo
next_cursor; - dalla volta dopo: chiama con
since=<cursore conservato>.
La condizione di fine è has_more == false, non
next_cursor == null. Questa non è una paginazione ma una coda
che si segue: while next_cursor != null — l'abitudine che si
porta dalla paginazione classica — qui non termina mai.
A coda finita next_cursor non è nullo: è la tua posizione, e
resta valida. Una chiamata che non trova nulla di nuovo
restituisce items vuoto, has_more a false e lo stesso
cursore che le hai dato — non devi ricominciare da capo.
next_cursor è nullo in un caso solo: la prima chiamata su un
brand che non ha ancora alcuna revoca — cioè pagina vuota e
nessun since in ingresso. Non c'è ancora una posizione da
ricordare, e la chiamata successiva va fatta di nuovo senza
since. Non esiste il caso «avevo un cursore e ricevo null»:
con un since valido in ingresso il cursore torna sempre
valorizzato, anche a pagina vuota.
Il campo revoked_at
È l'istante in cui la persona ha revocato, non quello in cui
tu lo leggi né quello in cui noi lo abbiamo registrato una seconda
volta. Una revoca ripetuta sullo stesso indirizzo non sposta
questa data: la riga conserva l'istante e l'origine della prima
— che è anche il motivo per cui richiamare POST /contacts/revoke
su un indirizzo già revocato non rompe niente.
Il campo origin
Ti dice chi ha agito, non quale credenziale:
| valore | significato |
|---|---|
unsubscribe_link |
la persona ha usato il link di disiscrizione di una nostra email |
console |
chi amministra l'account ha agito dalla console |
api_key |
una tua integrazione ha chiamato POST /contacts/revoke |
undeclared |
l'origine non è stata dichiarata |
unknown |
la revoca è anteriore all'introduzione di questo campo |
api_key non dice quale chiave: quante integrazioni hai e
come si chiamano non è un'informazione che una di esse debba
apprendere. Se ti serve, è in console.
Cosa questo endpoint non fa
Non ti dice quante revoche esistono: non c'è un totale, e non c'è modo di ottenerlo. Scorri, oppure no.
Non ti dice se un indirizzo revocato sia anche un tuo contatto attuale. Le due cose sono indipendenti: la memoria di una revoca sopravvive alla cancellazione del contatto — è ciò che impedisce a un ciclo «cancella e ricrea» di far tornare inviabile chi si era disiscritto — quindi in questa coda puoi trovare indirizzi che nel tuo elenco contatti non esistono più. È corretto che sia così, e vanno trattati come tutti gli altri: a quegli indirizzi non si scrive.
Non attraversa i brand: vedi le revoche del brand di questa chiave, e nient'altro. Con più brand servono più chiavi.
Parametri
sincestring · nella queryfacoltativoIl
next_cursorricevuto dalla richiesta precedente. Omettilo per partire dall'inizio della coda. Stringa opaca: un valore non emesso da noi, o emesso per una chiave di un altro brand, riceve400 invalid_cursor.limitinteger · nella queryfacoltativoQuante revoche per pagina. Un valore superiore a 500 non è un errore: viene semplicemente servito come 500.
Risposte
200
Una pagina della coda.
itemsarray di valoriobbligatorioitems[].emailstring (email)obbligatorioitems[].revoked_atstring (date-time)obbligatorioL'istante della revoca, in UTC. È quello della PRIMA revoca di questo indirizzo per questo brand: una revoca ripetuta non lo sposta.
items[].originstringobbligatorioChi ha agito. Mai quale credenziale.
- Valori ammessi:
unsubscribe_link,console,api_key,undeclared,unknown next_cursorstring | nullobbligatorioLa posizione da rimandare in
since. Stringa opaca. Nullo soltanto se non esiste ancora alcuna posizione — coda vuota e nessunsincein ingresso. A coda finita NON è nullo: è la tua posizione e resta valida.has_morebooleanobbligatoriotruese esiste almeno un'altra pagina. Scorri finché non diventafalse.
400
invalid_cursor — il cursore non è valido per questa chiave; riparti senza since. invalid_request — limit non è un intero positivo.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
401
Credenziale assente, non valida o revocata.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
403
capability_not_granted — serve revocations:read; api_key_required — questa lettura si autentica con una API key, e da console c'è l'elenco delle revoche; api_key_disabled.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
409
brand_archived.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
429
rate_limited — troppe richieste in questa finestra. È un rifiuto TEMPORANEO e non spegne la chiave: attendi e riprendi dallo stesso cursore.
errorobjectobbligatorioerror.codestringobbligatorioCodice stabile da usare per il branching applicativo —
messageè testo descrittivo (in italiano), non pensato per essere parsato.error.messagestringobbligatorio
Eventi webhook
mailronix invia questo evento al tuo indirizzo; non è un endpoint che chiami tu. Il modo di verificare la firma è nella guida ai webhook.
Guida ai webhookreceiveMailronixEvent
Un evento del tuo account, consegnato al tuo endpoint
mailronix invia questa richiesta al tuo server, all'indirizzo HTTPS registrato in console. Il tuo ricevitore la implementa; non è un endpoint di mailronix.
Rispondi in fretta, con un 2xx
Qualunque risposta 2xx conferma la consegna. Tutto il resto è un
fallimento e provoca un nuovo tentativo: 3xx compresi — i
redirect non vengono seguiti. Il tempo massimo di un tentativo è
10 secondi. Del corpo della risposta leggiamo al più 1 KB e non lo
conserviamo oltre un estratto troncato. Accetta l'evento, scrivilo
in una coda tua e rispondi; l'elaborazione vera falla dopo.
Verifica la firma PRIMA di fidarti del corpo
Ogni richiesta porta l'intestazione Mailronix-Signature:
Mailronix-Signature: t=1761141791,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
- Leggi
t(secondi Unix) e tutti i valoriv1. - Calcola
HMAC-SHA256(segreto, t + "." + corpo_grezzo), in esadecimale minuscolo. Usa il corpo grezzo, byte per byte, così come è arrivato: non rileggere e riserializzare il JSON. - Confronta con ciascun
v1a tempo costante. Basta che uno corrisponda. - Rifiuta se
tdista più di 5 minuti dall'ora del tuo server: protegge dai replay.tè l'istante di questo tentativo, non della creazione dell'evento, quindi un nuovo tentativo dopo ore passa comunque la finestra.
Il segreto (whsec_…) ti viene mostrato una sola volta, alla
creazione dell'endpoint o alla rotazione. Dopo una rotazione senza
revoca immediata, per 24 ore ogni richiesta porta due valori
v1, quello del segreto nuovo e quello del precedente: aggiorna il
tuo ricevitore senza interruzioni, verificando contro entrambi i
segreti finché non hai finito.
import hmac, hashlib, time
def verifica(intestazione, corpo: bytes, segreto: bytes, tolleranza=300):
parti = [p.strip().split("=", 1) for p in intestazione.split(",")]
t = next(v for k, v in parti if k == "t")
firme = [v for k, v in parti if k == "v1"]
if abs(time.time() - int(t)) > tolleranza:
return False
atteso = hmac.new(segreto, t.encode() + b"." + corpo, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(atteso, f) for f in firme)
Idempotenza e ordine
Ogni evento ha un id stabile (evt_…) e lo ritrovi
nell'intestazione Mailronix-Event-Id. Lo stesso evento può
arrivarti più volte — dopo un tentativo andato a vuoto, un
rinvio dalla console, un ripristino dopo un'interruzione nostra —
quindi il tuo ricevitore deve essere idempotente su id.
L'ordine non è garantito: usa created_at per ordinare.
Tentativi
Un tentativo fallito viene ripetuto dopo 10 secondi, 1 minuto, 5 minuti, 30 minuti, 2 ore, 6 ore, 12 ore, 24 ore e 48 ore: dieci tentativi in tutto, in circa quattro giorni. Poi la consegna risulta fallita e puoi rinviarla dalla console. Se una consegna esaurisce i tentativi e l'endpoint non ne ha avuta nessuna riuscita nelle ultime 72 ore, l'endpoint viene disattivato e il titolare dell'account riceve un avviso; si riattiva dalla console. Gli eventi si conservano 30 giorni.
Che cosa non facciamo
Non seguiamo redirect, non usiamo proxy, non consegniamo a indirizzi che si risolvono verso reti private, loopback o link-local (verifica rifatta a ogni tentativo, non solo alla registrazione) e non inviamo mai il corpo dell'email.
Parametri
Mailronix-Signaturestring · nell'intestazioneobbligatoriot=<unix>,v1=<hex>[,v1=<hex>]— vedi sopra.Mailronix-Event-Idstring · nell'intestazioneobbligatorioLo stesso valore del campo
iddel corpo.Mailronix-Delivery-Idstring · nell'intestazioneobbligatorioIdentifica questa consegna. Serve a correlare i log, non alla deduplica: per quella usa l'
iddell'evento.Mailronix-Event-Typestring · nell'intestazioneobbligatorioLo stesso valore del campo
typedel corpo.- Valori ammessi:
email.sent, email.delivered, email.bounced, email.complained, email.failed, email.suppressed, email.clicked, contact.confirmed, contact.unsubscribed, webhook.test
Corpo della richiesta · obbligatorio
idstringobbligatorioStabile per evento: lo stesso valore a ogni tentativo e a ogni rinvio. Chiave di idempotenza del tuo ricevitore.
typestringobbligatorioIl catalogo sottoscrivibile. Non esistono eventi di apertura.
email.clickedesiste solo per i brand che hanno acceso il tracciamento dei click, e solo per i destinatari che hanno acconsentito (vediWebhookClickData).webhook.testlo inviamo noi quando provi un endpoint dalla console: stessa busta, nessun dato di destinatari, non è sottoscrivibile.- Valori ammessi:
email.sent,email.delivered,email.bounced,email.complained,email.failed,email.suppressed,email.clicked,contact.confirmed,contact.unsubscribed,webhook.test api_versionstringobbligatorioLa versione del payload. L'aggiunta di campi non la cambia, quindi ignora i campi che non conosci.
created_atstring (date-time)obbligatorioQuando l'evento è stato registrato, in UTC.
brand_idstring | null (uuid)obbligatorioIl brand a cui appartiene l'evento (un endpoint con filtro per brand non riceve gli eventi degli altri).
nullsolo inwebhook.test.dataobbligatorioIl contenuto dipende da
type:email.clickedportaWebhookClickData, gli altriemail.*portanoWebhookEmailData,contact.*portaWebhookContactData,webhook.testportaWebhookTestData.
Esempi
Un rifiuto permanente
{
"id": "evt_3f1a9c2e7b6d4e8f9a0b1c2d3e4f5a6b",
"type": "email.bounced",
"api_version": "2026-10-22",
"created_at": "2026-10-22T14:03:11Z",
"brand_id": "6f1d2c0e-8b1a-4c53-9a77-0d6a7b3a2c11",
"data": {
"email_log_id": "0b7c1e52-3f0a-4d3e-8a8e-5b1f2c9d4e60",
"recipient": "persona@example.com",
"category": "transactional",
"bounce_type": "permanent"
}
}Una disiscrizione di un indirizzo che non è un contatto
{
"id": "evt_9c4e2a1b7d3f4a6e8b0c1d2e3f4a5b6c",
"type": "contact.unsubscribed",
"api_version": "2026-10-22",
"created_at": "2026-10-22T14:07:42Z",
"brand_id": "6f1d2c0e-8b1a-4c53-9a77-0d6a7b3a2c11",
"data": {
"contact_id": null,
"email": "persona@example.com"
}
}Risposte
2XX
Evento ricevuto. Il corpo della risposta viene ignorato.
3XX
Considerato un fallimento: il redirect non viene seguito e la consegna sarà ritentata.
4XX
Fallimento, ritentato secondo la tabella dei tentativi. Non esiste un codice che significhi «non ritentare»: se non vuoi più gli eventi, disattiva l'endpoint dalla console.
5XX
Fallimento, ritentato.