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 Sevksevk = Sevk("sevk_full_xxxxxxxx")
Send Email
HTML body
from sevk import Sevksevk = Sevk("sevk_full_xxxxxxxx")result = sevk.emails.send({"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({"subject": "Welcome","html": html,})
Bulk send
result = sevk.emails.send_bulk([{"subject": "Welcome","html": "<h1>Hi user 1</h1>",},{"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.
from datetime import datetime, timedelta, timezonescheduled_at = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()result = sevk.emails.send({"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
sevk.emails.update(email_id, {"scheduled_at": (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat(),})
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({"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 Sevksevk = 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 timefrom sevk import (Sevk,SevkError,ValidationError,UnauthorizedError,ForbiddenError,NotFoundError,ConflictError,RateLimitError,)sevk = Sevk("sevk_full_xxxxxxxx")try:sevk.emails.send({"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