Python SDK
Official Python SDK for the Tratto Email API
The official SDK for Python: tratto-email. No external dependencies,
built on urllib. Requires Python 3.10+.
Every example on this page typechecks with mypy against the published 2.3.0. Where a feature needs a particular version, the page names the minimum one that has it.
Installation
pip install tratto-emailInitialization
from tratto import Tratto
client = Tratto('tratto_live_...')timeout sets how long each request may wait, in seconds, for the
connection and for each read. It requires 2.3.0 or newer and defaults to
60. A request that takes longer raises TrattoError with
code == 'timeout' and status_code == 0:
client = Tratto('tratto_live_...', timeout=10)A send that timed out may still have reached the API. Retry it with the same idempotency key, so it cannot go out twice.
From 2.3.0 the SDK percent-encodes every id it puts in a URL path, so an id
containing /, ? or # cannot change the route called.
Send Email
from tratto import SendEmailOptions
result = client.emails.send(SendEmailOptions(
from_='hello@yourdomain.com',
to='user@example.com',
subject='Hello!',
html='<h1>Welcome!</h1>',
text='Welcome to Tratto',
))
print(result['id']) # email_abc123from_ can be omitted from 2.0.0 or newer: Tratto then uses the workspace transactional sender, or the general default sender if there is none. See Senders.
From 2.0.0, SendEmailOptions and the other options classes accept keyword
arguments only: SendEmailOptions('hello@yourdomain.com', ...) raises
TypeError.
Send with Template
result = client.emails.send(SendEmailOptions(
from_='hello@yourdomain.com',
to='user@example.com',
subject='Welcome to Acme!',
template_id='tmpl_abc123',
variables={
'firstName': 'Alice',
'code': 'SECRET123',
},
))Send with Markdown
The markdown field requires 1.1.0 or newer. A plain
pip install tratto-email takes the newest release, so a fresh install has it;
a pin below 1.1.0 does not.
Send Markdown and let the server render
responsive email HTML — markdown is mutually exclusive with html:
result = client.emails.send(SendEmailOptions(
from_='hello@yourdomain.com',
to='user@example.com',
subject='Welcome!',
markdown='# Welcome, {{firstName}}!',
variables={'firstName': 'Alice'},
))The same release added markdown to templates.create / templates.update
(creating format: 'emailmd' templates), with format, source and
renderWarnings in the template responses.
Schedule Email
result = client.emails.send(SendEmailOptions(
from_='hello@yourdomain.com',
to='user@example.com',
subject='Reminder',
text='See you tomorrow!',
scheduled_at='2025-07-01T09:00:00Z',
))While the email is still scheduled you can move it to another date or cancel
it. Both methods require 2.3.0 or newer:
client.emails.reschedule('email_abc123', '2025-07-02T09:00:00Z')
client.emails.cancel('email_abc123') # its quota goes back to the monthOnce the email has left scheduled, both raise TrattoError with code
CONFLICT (409). Moving it to another UTC month can raise QUOTA_EXCEEDED
(429) when that month is full, and RATE_LIMITED (429) is safe to retry.
Send a Batch
emails.send_batch() sends up to 100 emails in one request and requires
2.1.0 or newer. Each item is a SendEmailOptions without scheduled_at:
an item with scheduled_at gets a 422 whose details names the item.
The idempotency key covers the whole batch, so it goes
on the call: an item with its own idempotency_key raises ValueError.
import uuid
result = client.emails.send_batch(
[
SendEmailOptions(from_='hello@yourdomain.com', to='alice@example.com', subject='Your receipt', text='Thanks, Alice.'),
SendEmailOptions(from_='hello@yourdomain.com', to='bob@example.com', subject='Your receipt', text='Thanks, Bob.'),
],
idempotency_key=str(uuid.uuid4()),
)
print(result['accepted'], result['failed'])
for item in result['emails']: # same order as the request
if item['status'] == 'failed':
print(item['to'], item['error']['code'])A batch where some emails failed still returns the 202 payload, it is not
raised: a failed item has no id. Send only those again, in a new batch with
a new key. After a 503 or 500 TrattoError nothing was sent, and
retrying with the same key is safe. Quota, limits and errors are in
Send a Batch.
Attachments
Requires tratto-email 2.2.0 or newer, and a paid plan: the Free plan answers
403 PLAN_UPGRADE_REQUIRED. Upload the file first, then reference its id
from emails.send() or emails.send_batch().
with open('invoice.pdf', 'rb') as f:
file = client.attachments.upload(
f, filename='invoice.pdf', content_type='application/pdf'
)
client.emails.send(SendEmailOptions(
from_='billing@yourdomain.com',
to='alice@example.com',
subject='Your invoice',
html='<p>Attached.</p>',
attachments=[{'id': file['id']}],
))
meta = client.attachments.get(file['id']) # metadata only, never the bytes
client.attachments.delete(file['id'])The content can be bytes or a binary file object. Limits, expiry and storage are in Attachments.
Get Email Status
email = client.emails.get('email_abc123')
print(email['data']['status']) # 'delivered'List Emails
response = client.emails.list(limit=50)
for email in response['data']:
print(f"{email['id']}: {email['status']}")Register a Domain
domain = client.domains.add('hello.yourdomain.com')
for record in domain['data']['records']:
print(record['type'], record['host'], record['value'])Verify Domain
result = client.domains.verify('dom_abc123')
print(result['data']['status']) # 'pending', 'verified' or 'failed'Create Contact
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'},
))Create Campaign
from tratto import CreateCampaignOptions
campaign = client.campaigns.create(CreateCampaignOptions(
name='Q3 Sale',
template_id='tmpl_abc123',
audience_id='aud_abc123',
from_name='Acme Newsletter',
from_email='newsletter@yourdomain.com',
subject_a='Our top picks for Q3',
))
print(campaign['data']['id'])Send Campaign
sent = client.campaigns.send('camp_xyz789')
print(sent['data']['status']) # 'sending'Stop a Campaign
client.campaigns.pause('camp_xyz789')
client.campaigns.unschedule('camp_xyz789')unschedule requires 1.2.0 or newer. It applies only while the campaign is
scheduled and nothing has left: on a campaign that is sending, already
paused, or waiting out its probe wave it raises
TrattoError with code CONFLICT, because part of the list may already have
the email. Pause it instead.
Get Campaign Stats
stats = client.campaigns.get_stats('camp_xyz789')
print(stats['data']['stats']['delivered']) # 4950
print(stats['data']['rates']['openRate']) # 50 → 50%, rates are percentagesRegister Webhook
from tratto import CreateWebhookOptions
webhook = client.webhooks.create(CreateWebhookOptions(
url='https://yourapi.com/webhooks/tratto',
events=['sent', 'delivered', 'opened', 'clicked', 'bounced'],
))
print(webhook['data']['secret']) # whsec_xyz789... (returned only once)Verify Webhooks
verify_webhook checks the X-Tratto-Signature header of a delivery and
requires 2.3.0 or newer. Pass the raw body exactly as received, before any
JSON parsing. With 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, before any parsing
if not verify_webhook(
raw_body,
request.headers.get('X-Tratto-Signature'),
os.environ['TRATTO_WEBHOOK_SECRET'],
):
return {'error': 'Invalid signature'}, 401
event = json.loads(raw_body)
return {'ok': True}, 200It returns False for a missing, malformed, stale or non-matching header, and
raises ValueError only on an empty secret or a negative tolerance. The
tolerance is 5 minutes either way; change it with tolerance_seconds=600. How
the signature is built is in
Webhook Signature Verification.
List Flows
flows = client.flows.list(limit=20)
active = [f for f in flows['data'] if f['status'] == 'active']The status argument of flows.list is deprecated from 2.3.0: the API
ignores it and returns flows of every status, so passing it now emits a
DeprecationWarning. Filter on each flow's status (draft, active or
inactive) in the response instead, as above.
Error Handling
from tratto import TrattoError
try:
result = client.emails.send(SendEmailOptions(
from_='hello@unverified.com',
to='user@example.com',
subject='Hello',
text='Test',
))
except TrattoError as e:
if e.code == 'FORBIDDEN':
# The message names the domain that is not verified.
print(e)An error with code == 'timeout' and status_code == 0 is the client
timeout, not an API response.
Besides code and status_code, TrattoError carries two fields from the API
response, both from 1.2.0 or newer: suggestion, the API's own words on
what to do about the error, and docs, a link to the page that explains the
code. Both are None when the response does not include them.
On a 422 VALIDATION_ERROR, details lists what is wrong and where: for the
fourth email of a batch, e.details[0]['path'] is '/emails/3/to'. It
requires 2.1.0 or newer and is None when the response has none.
except TrattoError as e:
print(e.code, e.status_code)
if e.suggestion:
print(e.suggestion)suggestion is the field to read where one code covers several situations and
only the text tells them apart. The CONFLICT on campaigns.send and
campaigns.unschedule is the case to keep in mind: see
Error Codes.
Links
- PyPI: https://pypi.org/project/tratto-email/
- GitHub: https://github.com/tratto-email/tratto-python
- API Docs: API Reference
Edit this page on GitHub
Last updated on