Skip to main content

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 resendeusend rename.

  • Module-level configeusend.api_key = "...", then call eusend.Emails.send(...).
  • Zero HTTP dependencies — the transport is built on the standard library.
  • TypedTypedDict params and responses; ships py.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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

eusend-0.1.0.tar.gz (13.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

eusend-0.1.0-py3-none-any.whl (19.7 kB view details)

Uploaded Python 3

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

Hashes for eusend-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f36b4f06029dbace6780a0737fc334fa6b154a8f5d34f9b1f840a287460c1f79
MD5 64ed94225f5201a0eb006a7bf84dc87c
BLAKE2b-256 c7a9119ef1db9ca4435e15eb4ea6e9b6805a3899c5f78ed22a7cef7aee8a9b8e

See more details on using hashes here.

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

Hashes for eusend-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4b496f1dbc19363e80cd6b8308e3417f7728f1229265b5d42ff4e358f8cc18b2
MD5 db0636ff53039b608ade81e648eab95c
BLAKE2b-256 8dcd220db0971d2221a78297362cea2ff03cac178ff9a7e36fffe9ed27edc768

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page