Python

Synchronous client for Python 3.8+. Built on httpx.

Install

pip install sevk

Poetry and uv both work: poetry add sevk, uv add sevk.

Initialize

Pass the API key directly. Resource methods accept dictionaries; from is reserved in Python, but it is fine as a dictionary key.

from sevk import Sevk
sevk = Sevk("sevk_full_xxxxxxxx")

Send Email

HTML body

from sevk import Sevk
sevk = Sevk("sevk_full_xxxxxxxx")
result = sevk.emails.send({
"from": "[email protected]",
"subject": "Welcome",
"html": "<h1>Welcome</h1><p>Thanks for signing up.</p>",
})
print(result.get("id"))

sevk Markup body

Render sevk markup locally with sevk.markup.render and pass the resulting HTML as the html field.

html = sevk.markup.render("""
<section padding="32px">
<heading level="1">Welcome</heading>
<paragraph>Thanks for signing up.</paragraph>
<button href="https://app.example.com">Open the app</button>
</section>
""")
sevk.emails.send({
"from": "[email protected]",
"subject": "Welcome",
"html": html,
})

Bulk send

result = sevk.emails.send_bulk([
{
"from": "[email protected]",
"subject": "Welcome",
"html": "<h1>Hi user 1</h1>",
},
{
"from": "[email protected]",
"subject": "Invoice",
"html": "<p>Your invoice</p>",
"attachments": [
{
"filename": "invoice.pdf",
"content": base64_content,
"content_type": "application/pdf",
}
],
},
])
print(result["success"], result["failed"])

Scheduled Email

Use scheduled_at for a future RFC 3339 timestamp with a timezone, up to 30 days ahead. The SDK sends it as scheduledAt on the wire. Scheduled emails cannot include attachments or use send_bulk. Quota and balance are reserved when the request is accepted.

Schedule
from datetime import datetime, timedelta, timezone
scheduled_at = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
result = sevk.emails.send({
"from": "[email protected]",
"subject": "Appointment reminder",
"text": "Your appointment is tomorrow.",
"scheduled_at": scheduled_at,
}, {"idempotency_key": "appointment-reminder/apt_123"})
email_id = result.get("id")
if not isinstance(email_id, str):
raise RuntimeError("Expected a single email id")
email = sevk.emails.get(email_id)
print(email["status"], email["scheduledAt"])

The returned id identifies an accepted email, not a completed delivery. Persist the timestamp, payload, and idempotency key with the appointment and reuse them unchanged on retries.

Reschedule or cancel

Reschedule
sevk.emails.update(email_id, {
"scheduled_at": (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat(),
})
Cancel
sevk.emails.cancel(email_id)

Both operations require an email whose provider delivery has not started. Cancellation releases reserved quota and balance and is idempotent once the email is CANCELLED. A 409 signals a state conflict. See reschedule and cancel.

Contacts

contact = sevk.contacts.create({
"email": "[email protected]",
"data": {"firstName": "John", "plan": "pro"},
})
sevk.contacts.get(contact["id"])
sevk.contacts.update(contact["id"], {
"data": {"plan": "enterprise"},
})
page = sevk.contacts.list({"limit": 50})
for c in page["items"]:
print(c["email"])
sevk.contacts.delete(contact["id"])

Audiences

audience = sevk.audiences.create({"name": "Newsletter"})
sevk.audiences.add_contacts(
audience["id"],
[
"b2c3d4e5-f678-4901-abcd-ef1234567890",
"b2c3d4e5-f678-4901-abcd-ef1234567891",
],
)
contacts = sevk.audiences.list_contacts(audience["id"])
for c in contacts["items"]:
print(c["email"])

Render Markup

from sevk import Sevk
sevk = Sevk("sevk_full_xxxxxxxx")
html = sevk.markup.render("""
<section padding="32px">
<heading level="1">Hello</heading>
<paragraph>Rendered locally.</paragraph>
</section>
""")

Error Handling

Failed requests raise a typed SevkError subclass carrying status_code, code, message, fields, details, and response_body. JSON bodies are decoded; non-JSON bodies remain strings. 429s raise RateLimitError with the parsed retry_after. Every typed error inherits Exception, so existing broad catches keep working.

import time
from sevk import (
Sevk,
SevkError,
ValidationError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
ConflictError,
RateLimitError,
)
sevk = Sevk("sevk_full_xxxxxxxx")
try:
sevk.emails.send({
"from": "[email protected]",
"to": "not-an-email",
"subject": "Test",
"text": "Hi",
})
except ValidationError as err:
for field, messages in err.fields.items():
print(f"{field}: {', '.join(messages)}")
except UnauthorizedError:
print("Check your API key")
except ForbiddenError as err:
print(f"Forbidden: {err.message}")
except NotFoundError:
print("Resource not found")
except ConflictError as err:
print(f"Already exists: {err.message}")
except RateLimitError as err:
time.sleep(err.retry_after or 1)
except SevkError as err:
print(f"[{err.status_code} {err.code}] {err.message}")
raise