Official Python SDK for the Eusend API — the EU-native transactional email platform.
Project description
eusend
Official Python SDK for the Eusend API — the EU-native transactional email platform.
Its shape mirrors resend-python, so migrating from Resend is largely a resend → eusend rename.
- Module-level config —
eusend.api_key = "...", then calleusend.Emails.send(...). - Zero HTTP dependencies — the transport is built on the standard library.
- Typed —
TypedDictparams and responses; shipspy.typed.
pip install eusend
Requires Python 3.8+.
Getting started
import eusend
eusend.api_key = "eu_live_..."
params: eusend.Emails.SendParams = {
# `from` accepts a bare email or a display-name form: "Acme <you@yourdomain.com>"
"from": "Acme <you@yourdomain.com>",
"to": ["user@example.com"],
"subject": "Hello",
"html": "<p>Hello world</p>",
}
email = eusend.Emails.send(params)
print(email["id"]) # 9a8b7c6d-... (UUID)
The key can also come from the EUSEND_API_KEY environment variable, in which
case you can skip setting eusend.api_key.
Responses are dicts with snake_case keys — access fields with email["id"].
On failure, methods raise eusend.EusendError (see Error handling).
Emails
Send
from and to are required; provide at least one of html, text, or template_id.
| Key | Type | Notes |
|---|---|---|
from |
str |
Verified domain; bare or display-name form. |
to cc bcc reply_to |
str | list[str] |
Max 50 each. |
subject |
str |
|
html / text |
str |
|
template_id |
str |
Saved template. |
variables |
dict |
Template substitutions (HTML-escaped). |
headers |
dict[str, str] |
No line breaks in names or values. |
track_opens / track_clicks |
bool |
Default True. |
attachments |
list[Attachment] |
See below. Up to 20, 10 MB combined. |
scheduled_at |
str |
Future send, ≤ 30 days. |
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": "you@yourdomain.com",
"to": "user@example.com",
"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": "you@yourdomain.com", "to": "user@example.com",
"subject": "Your receipt", "html": "<p>Thanks!</p>"},
options={"idempotency_key": f"receipt-{order_id}"},
)
Scheduled sends
scheduled_at accepts an ISO 8601 string or natural language ("in 1 hour",
"tomorrow at 9am"), parsed server-side in UTC.
sent = eusend.Emails.send({
"from": "you@yourdomain.com", "to": "user@example.com",
"subject": "Reminder", "html": "<p>Soon.</p>",
"scheduled_at": "in 1 hour",
})
eusend.Emails.update({"id": sent["id"], "scheduled_at": "in 2 hours"}) # reschedule
eusend.Emails.cancel(sent["id"]) # cancel before it sends
Batch
Up to 100 emails in one request. Attachments and scheduling are stripped (not
supported on the batch endpoint). The result maps positionally to the input:
queued items carry id, rejected items carry error and code.
res = eusend.Batch.send([
{"from": "you@yourdomain.com", "to": "alice@example.com", "subject": "Hi", "html": "<p>Hi</p>"},
{"from": "you@yourdomain.com", "to": "bob@example.com", "subject": "Hi", "html": "<p>Hi</p>"},
])
for item in res["data"]:
print(item.get("id") or f"{item['code']}: {item['error']}")
Retrieve & list
email = eusend.Emails.get("9a8b7c6d-...")
print(email["status"], email["events"][0]["type"])
page = eusend.Emails.list({"limit": 20, "status": "delivered"})
for e in page["data"]:
print(e["id"], e["subject"])
if page["next_cursor"]:
page = eusend.Emails.list({"cursor": page["next_cursor"]})
Filter by status, from, to. Statuses: queued scheduled sending sent
delivered bounced complained suppressed failed.
Domains
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 DNS 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"])
Emails sent with a test key are accepted and tracked but never delivered.
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": "user@example.com", "first_name": "Jane"})
# Bulk upsert (up to 1,000) → {"count": N}
eusend.Audiences.batch_create_contacts(audience["id"], [
{"email": "alice@example.com", "first_name": "Alice"},
{"email": "bob@example.com", "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"])
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": "you@yourdomain.com", "to": "user@example.com",
"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"])
Webhooks
hook = eusend.Webhooks.create({
"url": "https://yourapp.com/webhooks/eusend",
"events": ["email.delivered", "email.bounced", "email.complained"], # or ["*"]
})
print(hook["secret"]) # signing secret — returned only 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"])
Events: email.sent email.delivered email.bounced email.complained
email.opened email.clicked. The endpoint must be a public http(s) URL
returning 2xx directly (redirects count as failures).
Verifying 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)
Broadcasts
Send one email to every contact in an audience. {{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 <hello@yourdomain.com>",
"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"])
Calling send on a paused broadcast resumes it from where it stopped.
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).
import eusend
from eusend import EusendError, RateLimitError
try:
eusend.Emails.send({"from": "you@yourdomain.com", "to": "user@example.com",
"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: MissingApiKeyError, InvalidApiKeyError, ValidationError,
NotFoundError, RateLimitError, ApplicationError — all subclasses of
EusendError. Branch on e.code:
| 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 |
SENDING_SUSPENDED |
403 | EusendError |
ALL_SUPPRESSED |
422 | EusendError |
SERVICE_PAUSED |
503 | EusendError |
INTERNAL_ERROR |
500 | ApplicationError |
application_error |
— | ApplicationError (network failure) |
Configuration
import eusend
eusend.api_key = "eu_live_..." # or EUSEND_API_KEY
eusend.api_url = "https://api.eusend.dev" # override for testing
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file eusend-0.1.0.tar.gz.
File metadata
- Download URL: eusend-0.1.0.tar.gz
- Upload date:
- Size: 13.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f36b4f06029dbace6780a0737fc334fa6b154a8f5d34f9b1f840a287460c1f79
|
|
| MD5 |
64ed94225f5201a0eb006a7bf84dc87c
|
|
| BLAKE2b-256 |
c7a9119ef1db9ca4435e15eb4ea6e9b6805a3899c5f78ed22a7cef7aee8a9b8e
|
File details
Details for the file eusend-0.1.0-py3-none-any.whl.
File metadata
- Download URL: eusend-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4b496f1dbc19363e80cd6b8308e3417f7728f1229265b5d42ff4e358f8cc18b2
|
|
| MD5 |
db0636ff53039b608ade81e648eab95c
|
|
| BLAKE2b-256 |
8dcd220db0971d2221a78297362cea2ff03cac178ff9a7e36fffe9ed27edc768
|