Python SDK
SDK ufficiale Python per Tratto Email API
SDK ufficiale per Python: tratto-email. Nessuna dipendenza esterna,
costruito su urllib. Richiede Python 3.10+.
Ogni esempio di questa pagina passa il controllo di mypy contro la 2.3.0 pubblicata. Dove una funzionalità richiede una versione precisa, la pagina dichiara la minima che ce l'ha.
Installazione
pip install tratto-emailInizializzazione
from tratto import Tratto
client = Tratto('tratto_live_...')timeout fissa quanto può aspettare ogni richiesta, in secondi, sia per la
connessione sia per ogni lettura. Richiede la 2.3.0 o successiva e vale
60 se non lo indichi. Una richiesta che ci mette di più solleva
TrattoError con code == 'timeout' e status_code == 0:
client = Tratto('tratto_live_...', timeout=10)Un invio andato in timeout può essere arrivato comunque all'API. Ritentalo con la stessa chiave di idempotenza, così non parte due volte.
Dalla 2.3.0 l'SDK codifica (percent-encoding) ogni id che inserisce nel path
dell'URL, quindi un id che contiene /, ? o # non può cambiare la rotta
chiamata.
Invia Email
from tratto import SendEmailOptions
result = client.emails.send(SendEmailOptions(
from_='hello@tuodominio.it',
to='user@example.com',
subject='Ciao!',
html='<h1>Benvenuto!</h1>',
text='Benvenuto a Tratto',
))
print(result['id']) # email_abc123from_ si può omettere dalla 2.0.0 o successiva: Tratto usa allora il mittente transazionale del workspace, o il mittente generale se manca. Vedi Mittenti.
Dalla 2.0.0, SendEmailOptions e le altre classi di opzioni accettano solo
argomenti per nome: SendEmailOptions('hello@tuodominio.it', ...) solleva
TypeError.
Invia con Template
result = client.emails.send(SendEmailOptions(
from_='hello@tuodominio.it',
to='user@example.com',
subject='Benvenuto su Acme!',
template_id='tmpl_abc123',
variables={
'firstName': 'Alice',
'code': 'SECRET123',
},
))Invia con Markdown
Il campo markdown richiede la 1.1.0 o successiva. Un
pip install tratto-email senza vincoli prende l'ultima release, quindi
un'installazione nuova ce l'ha; un pin sotto la 1.1.0 no.
Invia Markdown e lascia che il server
renderizzi HTML responsive per email — markdown è mutuamente esclusivo con
html:
result = client.emails.send(SendEmailOptions(
from_='hello@tuodominio.it',
to='user@example.com',
subject='Benvenuto!',
markdown='# Benvenuto, {{firstName}}!',
variables={'firstName': 'Alice'},
))La stessa release ha aggiunto markdown a templates.create /
templates.update (creando template format: 'emailmd'), con format,
source e renderWarnings nelle risposte dei template.
Pianifica Invio
result = client.emails.send(SendEmailOptions(
from_='hello@tuodominio.it',
to='user@example.com',
subject='Promemoria',
text='Ci vediamo domani!',
scheduled_at='2025-07-01T09:00:00Z',
))Finché l'email è ancora scheduled puoi spostarla a un'altra data o
annullarla. Entrambi i metodi richiedono la 2.3.0 o successiva:
client.emails.reschedule('email_abc123', '2025-07-02T09:00:00Z')
client.emails.cancel('email_abc123') # la sua quota torna al meseQuando l'email non è più scheduled, entrambi sollevano TrattoError con
codice CONFLICT (409). Spostarla in un altro mese UTC può sollevare
QUOTA_EXCEEDED (429) se quel mese è pieno, e un RATE_LIMITED (429) si può
ritentare senza rischi.
Invia un Batch
emails.send_batch() invia fino a 100 email in una sola richiesta e richiede
la 2.1.0 o successiva. Ogni email è un SendEmailOptions senza
scheduled_at: un'email con scheduled_at riceve un 422 i cui details
indicano quale. La chiave di idempotenza vale per tutto
il batch, quindi va sulla chiamata: un'email con un suo idempotency_key
solleva ValueError.
import uuid
result = client.emails.send_batch(
[
SendEmailOptions(from_='hello@yourdomain.com', to='alice@example.com', subject='La tua ricevuta', text='Grazie, Alice.'),
SendEmailOptions(from_='hello@yourdomain.com', to='bob@example.com', subject='La tua ricevuta', text='Grazie, Bob.'),
],
idempotency_key=str(uuid.uuid4()),
)
print(result['accepted'], result['failed'])
for item in result['emails']: # stesso ordine della richiesta
if item['status'] == 'failed':
print(item['to'], item['error']['code'])Un batch con alcune email non partite restituisce comunque la risposta 202,
non solleva eccezioni: un'email non partita non ha id. Invia di nuovo solo
quelle, in un nuovo batch con una nuova chiave. Dopo un TrattoError 503
o 500 non è partito niente, e ripetere con la stessa chiave è sicuro.
Quota, limiti ed errori sono in Invio in Batch.
Allegati
Richiede la tratto-email 2.2.0 o successiva e un piano a pagamento: il piano Free
risponde 403 PLAN_UPGRADE_REQUIRED. Prima carica il file, poi indicane l'id
in emails.send() o emails.send_batch().
with open('fattura.pdf', 'rb') as f:
file = client.attachments.upload(
f, filename='fattura.pdf', content_type='application/pdf'
)
client.emails.send(SendEmailOptions(
from_='billing@yourdomain.com',
to='alice@example.com',
subject='La tua fattura',
html='<p>In allegato.</p>',
attachments=[{'id': file['id']}],
))
meta = client.attachments.get(file['id']) # solo i metadati, mai i byte
client.attachments.delete(file['id'])Il contenuto può essere bytes o un file aperto in modalità binaria. Limiti, scadenza e spazio sono in Allegati.
Ottieni Status Email
email = client.emails.get('email_abc123')
print(email['data']['status']) # 'delivered'Lista Email
response = client.emails.list(limit=50)
for email in response['data']:
print(f"{email['id']}: {email['status']}")Registra un Dominio
domain = client.domains.add('hello.tuodominio.it')
for record in domain['data']['records']:
print(record['type'], record['host'], record['value'])Verifica Dominio
result = client.domains.verify('dom_abc123')
print(result['data']['status']) # 'pending', 'verified' oppure 'failed'Crea Contatto
from tratto import CreateContactOptions
contact = client.contacts.create(CreateContactOptions(
email='user@example.com',
first_name='Alice',
last_name='Smith',
tags=['vip', 'newsletter'],
custom_fields={'plan': 'pro'},
))Crea Campagna
from tratto import CreateCampaignOptions
campaign = client.campaigns.create(CreateCampaignOptions(
name='Saldi Q3',
template_id='tmpl_abc123',
audience_id='aud_abc123',
from_name='Acme Newsletter',
from_email='newsletter@tuodominio.it',
subject_a='Le nostre migliori offerte del Q3',
))
print(campaign['data']['id'])Invia Campagna
sent = client.campaigns.send('camp_xyz789')
print(sent['data']['status']) # 'sending'Ferma una Campagna
client.campaigns.pause('camp_xyz789')
client.campaigns.unschedule('camp_xyz789')unschedule richiede la 1.2.0 o successiva. Vale solo mentre la campagna è
scheduled e non è uscito niente: su una campagna che sta spedendo, già in
pausa, o che aspetta la sua ondata di prova
solleva TrattoError con code CONFLICT, perché una parte della lista può già
avere l'email. Mettila in pausa invece.
Statistiche Campagna
stats = client.campaigns.get_stats('camp_xyz789')
print(stats['data']['stats']['delivered']) # 4950
print(stats['data']['rates']['openRate']) # 50 → 50%, i tassi sono percentualiRegistra Webhook
from tratto import CreateWebhookOptions
webhook = client.webhooks.create(CreateWebhookOptions(
url='https://tuoapi.com/webhooks/tratto',
events=['sent', 'delivered', 'opened', 'clicked', 'bounced'],
))
print(webhook['data']['secret']) # whsec_xyz789... (restituito una sola volta)Verifica i Webhook
verify_webhook controlla l'header X-Tratto-Signature di una consegna e
richiede la 2.3.0 o successiva. Passagli il body grezzo esattamente come
arriva, prima di qualunque parsing JSON. Con Flask:
import json
import os
from flask import Flask, request
from tratto import verify_webhook
app = Flask(__name__)
@app.post('/webhooks/tratto')
def tratto_webhook():
raw_body = request.get_data() # bytes, prima di qualunque parsing
if not verify_webhook(
raw_body,
request.headers.get('X-Tratto-Signature'),
os.environ['TRATTO_WEBHOOK_SECRET'],
):
return {'error': 'Firma non valida'}, 401
event = json.loads(raw_body)
return {'ok': True}, 200Restituisce False per un header mancante, malformato, scaduto o che non
corrisponde, e solleva ValueError solo con un secret vuoto o una tolleranza
negativa. La tolleranza è di 5 minuti in entrambe le direzioni; la cambi con
tolerance_seconds=600. Come è costruita la firma è spiegato in
Verifica della firma dei webhook.
Elenca i Flow
flows = client.flows.list(limit=20)
active = [f for f in flows['data'] if f['status'] == 'active']L'argomento status di flows.list è deprecato dalla 2.3.0: l'API lo
ignora e restituisce i flow di ogni status, quindi passarlo ora emette un
DeprecationWarning. Filtra invece sullo status di ogni flow (draft,
active o inactive) nella risposta, come sopra.
Gestione Errori
from tratto import TrattoError
try:
result = client.emails.send(SendEmailOptions(
from_='hello@nonverificato.com',
to='user@example.com',
subject='Ciao',
text='Test',
))
except TrattoError as e:
if e.code == 'FORBIDDEN':
# Il messaggio indica quale dominio non è verificato.
print(e)Un errore con code == 'timeout' e status_code == 0 è il
timeout del client, non una risposta dell'API.
Oltre a code e status_code, TrattoError porta due campi che arrivano dalla
risposta dell'API, entrambi dalla 1.2.0 o successiva: suggestion, cioè
cosa fare dell'errore nelle parole dell'API, e docs, il link alla pagina che
spiega il codice. Valgono None quando la risposta non li contiene.
Su un 422 VALIDATION_ERROR, details elenca cosa è sbagliato e dove: per la
quarta email di un batch, e.details[0]['path'] vale '/emails/3/to'.
Richiede la 2.1.0 o successiva e vale None quando la risposta non lo
contiene.
except TrattoError as e:
print(e.code, e.status_code)
if e.suggestion:
print(e.suggestion)suggestion è il campo da leggere dove un solo codice copre più situazioni e a
distinguerle c'è solo il testo. Il caso da tenere a mente è il CONFLICT di
campaigns.send e campaigns.unschedule: vedi
Codici di Errore.
Links
- PyPI: https://pypi.org/project/tratto-email/
- GitHub: https://github.com/tratto-email/tratto-python
- API Docs: API Reference
Modifica questa pagina su GitHub
Ultimo aggiornamento