Python SDK
Official Python SDK for the eusend API. Zero HTTP dependencies, typed, and shaped to mirror resend-python.
Official Python SDK for the eusend API. Its shape mirrors
resend-python, so migrating from
Resend is largely a resend → eusend rename. Zero HTTP dependencies (built on
the standard library), typed, and ships py.typed. Requires Python 3.8+.
⬡ Node.js / TypeScript
@eusend_dev/sdk
🐍 Python
pip install eusend
🐹 Go
go get github.com/eusend-dev/eusend-go
Installation
pip install eusendGetting started
Configure a module-level API key, then call the resource classes directly. The
key can also be read from the EUSEND_API_KEY environment variable.
import eusend
eusend.api_key = 'eu_live_...' # or set EUSEND_API_KEYResponses are dicts with snake_case keys — access fields with email['id'].
On failure, methods raise eusend.EusendError (see Error handling).
Sending an email
from and to are required; provide at least one of html, text, or template_id.
email = eusend.Emails.send({
# `from` accepts a bare email or a display-name form: "Acme <[email protected]>"
'from': '[email protected]',
'to': '[email protected]',
'subject': 'Your order is confirmed',
'html': '<p>Thanks for your order!</p>',
'text': 'Thanks for your order!',
})
print(email['id']) # 9a8b7c6d-...Send options
| Key | Type | Description |
|---|---|---|
from | str | Sender address — bare email or "Name <email>" |
to | str | list[str] | Recipient(s), max 50 |
cc / bcc / reply_to | str | list[str] | Max 50 each |
subject | str | Email subject |
html / text | str | HTML / plain-text body |
template_id | str | ID of a saved template |
variables | dict | Template variable substitutions (HTML-escaped) |
headers | dict[str, str] | Custom headers; no line breaks in names or values |
track_opens / track_clicks | bool | Default True |
attachments | list[dict] | Up to 20, 10 MB combined. See below. |
scheduled_at | str | Schedule for a future time, at most 30 days out |
At least one of html, text, or template_id is required.
Attachments
Each attachment is a dict. content accepts raw bytes (base64-encoded for you)
or an already-base64 str; alternatively pass path (a public URL fetched at
send time). Set content_id for an inline <img src="cid:...">.
with open('invoice.pdf', 'rb') as f:
eusend.Emails.send({
'from': '[email protected]',
'to': '[email protected]',
'subject': 'Your invoice',
'html': '<p>Attached.</p>',
'attachments': [
{'filename': 'invoice.pdf', 'content': f.read(), 'content_type': 'application/pdf'},
],
})Idempotent sends
Pass an options dict with an idempotency_key to safely retry without duplicating.
eusend.Emails.send(
{'from': '[email protected]', 'to': '[email protected]',
'subject': 'Receipt', 'html': '<p>Thanks</p>'},
options={'idempotency_key': f'receipt-{order_id}'},
)Batch send
Send up to 100 emails in a single request. Attachments and scheduling are not
supported on the batch endpoint and are stripped from each item. The result maps
positionally to the input: queued items carry id, rejected items carry error
and code.
res = eusend.Batch.send([
{'from': '[email protected]', 'to': '[email protected]', 'subject': 'Hi', 'html': '<p>Hi</p>'},
{'from': '[email protected]', 'to': '[email protected]', 'subject': 'Hi', 'html': '<p>Hi</p>'},
])
for item in res['data']:
print(item.get('id') or f"{item['code']}: {item['error']}")Scheduled sends
scheduled_at accepts an ISO 8601 string or natural language like "in 1 hour"
(at most 30 days out), parsed server-side in UTC. While the email's status is
scheduled you can reschedule or cancel it.
sent = eusend.Emails.send({
'from': '[email protected]',
'to': '[email protected]',
'subject': 'Your trial ends tomorrow',
'html': '<p>Reminder: your trial ends in 24 hours.</p>',
'scheduled_at': 'in 1 hour',
})
eusend.Emails.update({'id': sent['id'], 'scheduled_at': '2026-07-04T09:00:00Z'}) # reschedule
eusend.Emails.cancel(sent['id']) # cancelRetrieve & list emails
email = eusend.Emails.get('9a8b7c6d-...')
print(email['status']) # 'delivered'
print(email['events'][0]['type'])
page = eusend.Emails.list({'status': 'delivered', 'limit': 50})
print(page['data']) # list of emails
print(page['next_cursor']) # pass as {'cursor': ...} for next pageDomains
created = eusend.Domains.create('yourdomain.com')
print(created['dkim']['name'], created['dkim']['value']) # DNS records to add
print(created['spf'], created['dmarc'])
eusend.Domains.verify(created['id']) # after publishing the records
eusend.Domains.list()
eusend.Domains.get(created['id'])
eusend.Domains.remove(created['id'])API keys
key = eusend.ApiKeys.create({'name': 'Production'})
print(key['key']) # eu_live_... — returned only once
eusend.ApiKeys.create({'name': 'Sandbox', 'test_mode': True}) # eu_test_... key
eusend.ApiKeys.list() # prefixes only
eusend.ApiKeys.remove(key['id'])Templates
{{variable}} placeholders are substituted at send time; values are HTML-escaped.
tpl = eusend.Templates.create({
'name': 'Welcome email',
'subject': 'Welcome, {{name}}!',
'html': '<h1>Hi {{name}}</h1><p>Welcome to {{product}}.</p>',
})
eusend.Emails.send({
'from': '[email protected]', 'to': '[email protected]',
'template_id': tpl['id'],
'variables': {'name': 'Jane', 'product': 'Acme'},
})
eusend.Templates.list()
eusend.Templates.get(tpl['id'])
eusend.Templates.update(tpl['id'], {'subject': 'New subject'})
eusend.Templates.remove(tpl['id'])Audiences & contacts
Contact operations are grouped under Audiences (they live under a specific audience).
audience = eusend.Audiences.create('Newsletter')
eusend.Audiences.create_contact(audience['id'], {'email': '[email protected]', 'first_name': 'Jane'})
# Bulk upsert (up to 1,000) → {'count': N}
eusend.Audiences.batch_create_contacts(audience['id'], [
{'email': '[email protected]', 'first_name': 'Alice'},
{'email': '[email protected]', 'first_name': 'Bob'},
])
page = eusend.Audiences.list_contacts(audience['id'], {'subscribed': True, 'search': 'gmail.com'})
contact = page['data'][0]
eusend.Audiences.update_contact(audience['id'], contact['id'], {'unsubscribed': True})
eusend.Audiences.get_contact(audience['id'], contact['id'])
eusend.Audiences.remove_contact(audience['id'], contact['id'])
eusend.Audiences.list()
eusend.Audiences.remove(audience['id'])Broadcasts
{{first_name}}, {{last_name}}, {{full_name}}, and {{email}} are available
per recipient, and RFC 8058 one-click unsubscribe headers are added automatically.
bc = eusend.Broadcasts.create({
'name': 'May newsletter',
'audience_id': audience['id'],
'from': 'Sivert <[email protected]>',
'subject': 'May update',
'html': '<p>Hi {{first_name}}, your monthly update is here...</p>',
})
eusend.Broadcasts.send(bc['id']) # send now
eusend.Broadcasts.send(bc['id'], {'scheduled_at': '2026-06-01T09:00:00Z'}) # or schedule
eusend.Broadcasts.cancel(bc['id'])
eusend.Broadcasts.list()
eusend.Broadcasts.get(bc['id']) # includes delivery stats
eusend.Broadcasts.update(bc['id'], {'subject': 'Updated subject'})
eusend.Broadcasts.remove(bc['id'])Webhooks
hook = eusend.Webhooks.create({
'url': 'https://yourapp.com/webhooks/eusend',
'events': ['email.sent', 'email.delivered', 'email.bounced', 'email.complained'],
# or: 'events': ['*'] to receive everything
})
print(hook['secret']) # signing secret — store it securely, only returned once
eusend.Webhooks.list()
eusend.Webhooks.get(hook['id']) # includes recent deliveries
eusend.Webhooks.update(hook['id'], {'events': ['email.bounced']})
eusend.Webhooks.remove(hook['id'])Verifying webhook signatures
Every delivery is signed with HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{body}:
import base64
import hashlib
import hmac
def verify(headers, body: bytes, secret: str) -> bool:
signed = f"{headers['webhook-id']}.{headers['webhook-timestamp']}.{body.decode()}"
mac = hmac.new(secret.encode(), signed.encode(), hashlib.sha256)
expected = 'v1,' + base64.b64encode(mac.digest()).decode()
return hmac.compare_digest(headers['webhook-signature'], expected)Error handling
Any non-2xx response raises a subclass of eusend.EusendError. Network failures
that never reach the server raise ApplicationError (with status_code == None).
Branch on e.code.
import eusend
from eusend import EusendError, RateLimitError
try:
eusend.Emails.send({'from': '[email protected]', 'to': '[email protected]',
'subject': 'Hi', 'html': '<p>Hi</p>'})
except RateLimitError as e:
print(e.code) # 'MONTHLY_LIMIT_EXCEEDED'
print(e.status_code) # 429
except EusendError as e:
print(e.code, e.message, e.status_code)Exception classes — all subclasses of EusendError: MissingApiKeyError,
InvalidApiKeyError, ValidationError, NotFoundError, RateLimitError,
ApplicationError.
| Code | Status | Exception |
|---|---|---|
UNAUTHORIZED | 401 | InvalidApiKeyError |
FORBIDDEN | 403 | EusendError |
NOT_FOUND | 404 | NotFoundError |
VALIDATION_ERROR / BAD_REQUEST | 400 | ValidationError |
CONFLICT | 409 | EusendError |
RATE_LIMITED | 429 | RateLimitError |
MONTHLY_LIMIT_EXCEEDED | 429 | RateLimitError |
DAILY_LIMIT_EXCEEDED | 429 | RateLimitError |
PLAN_LIMIT_EXCEEDED | 403 | EusendError |
DOMAIN_NOT_VERIFIED | 403 | EusendError |
ALL_SUPPRESSED | 422 | EusendError |
SENDING_SUSPENDED | 403 | EusendError |
SERVICE_PAUSED | 503 | EusendError |
INTERNAL_ERROR | 500 | ApplicationError |
application_error | — | ApplicationError (network failure) |
View the full source and changelog on GitHub.