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-email

Inizializzazione

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_abc123

from_ 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 mese

Quando 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 percentuali

Registra 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}, 200

Restituisce 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.



Modifica questa pagina su GitHub

Ultimo aggiornamento