Official Python SDK for Senddy.io
Project description
Senddy Python SDK
Official Python SDK for the Senddy.io email API.
Full API reference → — every method, parameter, and return type.
Installation
Requires Python 3.9+.
pip install senddy
Quick Start
from senddy import Senddy, SendEmailParams
client = Senddy("senddy_live_api_key")
result = client.emails.send(SendEmailParams(
from_="sender@senddy.io",
to="recipient@example.com",
subject="Hello",
html="<p>Welcome!</p>",
))
print(result.id) # email_abc123
Async Support
from senddy import AsyncSenddy, SendEmailParams
async with AsyncSenddy("senddy_live_api_key") as client:
result = await client.emails.send(SendEmailParams(
from_="sender@senddy.io",
to="recipient@example.com",
subject="Hello",
html="<p>Welcome!</p>",
))
Configuration
All options are optional — sensible defaults are used if you don't override them.
client = Senddy(
"senddy_live_api_key",
timeout=30.0, # seconds
retries=2,
headers={"X-Custom": "value"}, # extra headers on every request
)
If no API key is passed to the constructor, the SDK reads from the SENDDY_API_KEY environment variable.
Both clients support context managers for proper resource cleanup:
with Senddy("senddy_live_api_key") as client:
# client.close() called automatically on exit
...
Emails
Send
from senddy import SendEmailParams, Attachment, RequestOptions
result = client.emails.send(
SendEmailParams(
from_="sender@senddy.io",
to=["alice@example.com", "bob@example.com"],
subject="Hello",
html="<p>Hi there</p>",
text="Hi there", # optional plain-text fallback
cc="cc@example.com", # optional
bcc="bcc@example.com", # optional
reply_to="reply@example.com", # optional
in_reply_to="<msg-1@senddy.io>", # optional: thread a reply (In-Reply-To)
references=["<msg-0@senddy.io>"], # optional: thread history (References)
headers={"X-Campaign-Id": "welcome"}, # optional custom headers
tags={"campaign": "welcome"}, # optional metadata
attachments=[Attachment( # optional
filename="report.pdf",
content=base64_string,
content_type="application/pdf",
)],
),
# optional — send() already auto-generates a key per call; set this only to
# dedupe the same send across separate calls (see Retries & idempotency).
options=RequestOptions(idempotency_key="order-1234-receipt"),
)
Note: The
from_parameter uses a trailing underscore becausefromis a Python reserved word. The SDK maps it tofromin the API request automatically.
Get
email = client.emails.get("email_abc123")
print(email.status) # 'delivered'
print(email.recipients) # list of EmailRecipient
print(email.events) # list of EmailEvent
List
from senddy import ListEmailsParams
result = client.emails.list(ListEmailsParams(
limit=25,
offset=0,
status="delivered",
since="2026-01-01T00:00:00Z",
))
for email in result.data:
print(email.subject)
print(result.pagination.total)
Get rendered content
content = client.emails.get_content("email_abc123")
print(content.html) # str or None
print(content.text) # str or None
Download EML
download = client.emails.download("email_abc123")
# download.content is bytes containing the raw EML
# download.content_type is 'message/rfc822'
Resend
resent = client.emails.resend("email_abc123")
print(resent.id, resent.original_id)
Suppressions
List
from senddy import ListSuppressionsParams
result = client.suppressions.list(ListSuppressionsParams(
limit=50,
search="example.com",
reason="hard_bounce",
))
Create
from senddy import CreateSuppressionParams
entry = client.suppressions.create(CreateSuppressionParams(
email_address="block@example.com",
))
Delete
result = client.suppressions.delete("block@example.com")
print(result.removed) # True
Export
from senddy import ExportSuppressionsParams
export = client.suppressions.export(ExportSuppressionsParams(format="csv"))
# export.content is a raw string (CSV by default, or a JSON string for format="json")
Domains
from senddy import CreateDomainParams
# Add a sending domain
domain = client.domains.create(CreateDomainParams(domain="mail.example.com"))
# Trigger DNS verification
client.domains.verify(domain.id)
# Add a subdomain under a verified root (inherits the root's verification)
from senddy import AddSubdomainParams
client.domains.add_subdomain(domain.id, AddSubdomainParams(subdomain="mail"))
# List, get, delete
client.domains.list()
client.domains.get(domain.id)
client.domains.delete(domain.id)
# Rotate DKIM keys
client.domains.regenerate_dkim(domain.id)
# Bulk DNS health check across all domains
client.domains.check_dns_health()
Inbound Routes
from senddy import CreateInboundRouteParams, UpdateInboundRouteParams
# Create a catchall route that forwards to your webhook
route = client.inbound_routes.create(CreateInboundRouteParams(
match_type="catchall",
webhook_url="https://your-app.example.com/inbound",
))
# List, get, update, delete
client.inbound_routes.list()
client.inbound_routes.get(route.id)
client.inbound_routes.update(route.id, UpdateInboundRouteParams(enabled=False))
client.inbound_routes.delete(route.id)
# Verify the MX record points at Senddy
client.inbound_routes.verify_mx(route.id)
See the inbound docs for match patterns, webhook signing, and full configuration.
Inbound Emails
from senddy import ListInboundEmailsParams
result = client.inbound_emails.list(ListInboundEmailsParams(route_id=42, limit=50))
email = client.inbound_emails.get("inbound_xyz")
print(email.text_body, email.attachments)
Webhook Endpoints
from senddy import CreateWebhookEndpointParams, UpdateWebhookEndpointParams
# Register an endpoint to receive event callbacks. event_type is one of:
# sent, delivered, bounced, complained, opened, clicked, or "*" for all events.
result = client.webhook_endpoints.create(CreateWebhookEndpointParams(
url="https://your-app.example.com/webhooks/senddy",
domain="mail.example.com",
event_type="delivered",
))
# Save result.secret — you'll need it to verify the signature on incoming webhooks.
# Test, list, update, delete
client.webhook_endpoints.test(result.id)
client.webhook_endpoints.list()
client.webhook_endpoints.update(result.id, UpdateWebhookEndpointParams(url="https://new-url.example.com"))
client.webhook_endpoints.delete(result.id)
# Inspect delivery history
deliveries = client.webhook_endpoints.deliveries(result.id)
Verifying webhook signatures
verify_webhook_signature is a stateless helper (no API key needed) — call it from
your webhook handler with the raw request body and the X-Webhook-* headers. It
returns the parsed event on success and raises WebhookVerificationError on a bad
signature or a stale (replayed) timestamp.
from senddy import verify_webhook_signature, WebhookVerificationError
# In your HTTP handler — pass the RAW body (str or bytes), not a re-serialized object.
try:
event = verify_webhook_signature(
payload=raw_body,
signature=request.headers["X-Webhook-Signature"],
timestamp=request.headers["X-Webhook-Timestamp"],
secret=os.environ["SENDDY_WEBHOOK_SECRET"],
# tolerance_seconds=300, # optional replay window (default 300; 0 disables)
)
# event is the parsed payload
except WebhookVerificationError:
... # reject with 400 — do not process
Recipient Lists
Recipient allow-lists scope which addresses an API key may send to.
from senddy import CreateRecipientListParams, UpdateRecipientListParams
# Create a list, then list / update / delete
rl = client.recipient_lists.create(
CreateRecipientListParams(name="Internal", patterns=["*@example.com"])
)
result = client.recipient_lists.list() # each item includes active_key_count
client.recipient_lists.update(rl.id, UpdateRecipientListParams(patterns=["*@example.com"]))
client.recipient_lists.delete(rl.id)
Billing
from senddy import (
BillingUsageParams,
ChangeTierParams,
CreateCheckoutParams,
ListBillingHistoryParams,
UpdateAutoRefillParams,
)
# Reads
balance = client.billing.get_balance()
print(balance.credit_balance, balance.billing_tier)
client.billing.get_history(ListBillingHistoryParams(limit=50, type="deduction")) # paginated
client.billing.get_payments() # paginated
client.billing.get_auto_refill()
client.billing.get_tier() # includes projected invoice on Pro
client.billing.get_usage(BillingUsageParams(days=30))
# Writes
session = client.billing.create_checkout(CreateCheckoutParams(credits=10000))
print(session.checkout_url)
client.billing.update_auto_refill(
UpdateAutoRefillParams(enabled=True, threshold=1000, amount_cents=2000)
)
client.billing.change_tier(ChangeTierParams(tier="pro"))
Error Handling
from senddy import (
APIError,
ValidationError,
RateLimitError,
AuthenticationError,
)
try:
client.emails.send(params)
except ValidationError as err:
print(f"Validation: {err.details}")
except RateLimitError as err:
print(f"Rate limited. Retry after {err.retry_after}s")
except AuthenticationError as err:
print(f"Auth error: {err}")
except APIError as err:
print(f"API error {err.status_code}: {err}")
Error Types
| Class | Status | Description |
|---|---|---|
ValidationError |
400 | Invalid request, includes .details list |
AuthenticationError |
401 | Missing or invalid API key |
ForbiddenError |
403 | Insufficient permissions |
NotFoundError |
404 | Resource not found |
RateLimitError |
429 | Rate limit exceeded, includes .retry_after |
InternalError |
5xx | Server error |
All error classes inherit from APIError, which inherits from Exception.
Pagination
List methods return a result with .data and .pagination (limit, offset, total, has_more). Page through results by advancing offset:
from senddy import ListEmailsParams
offset, limit = 0, 100
while True:
result = client.emails.list(ListEmailsParams(limit=limit, offset=offset))
for email in result.data:
... # process email
if not result.pagination.has_more:
break
offset += len(result.data)
Or let the SDK do the paging for you with list_all, a generator that fetches one page at a time (available on emails, suppressions, domains, and inbound_emails). limit sets the page size; filters are preserved across pages. The async client returns an async iterator:
# Sync
for email in client.emails.list_all(ListEmailsParams(status="delivered", limit=100)):
... # process every delivered email, fetched page by page
# Async
async for email in aclient.emails.list_all(ListEmailsParams(status="delivered")):
...
Retries & idempotency
The SDK retries transient failures (HTTP 429, 5xx, network errors, and request timeouts) with exponential backoff and jitter, honoring the Retry-After header. Non-retryable errors (4xx other than 429) are raised immediately. Pass retries=0 to the client to disable retries. The timeout is set by timeout on the client (seconds); override it per request with RequestOptions(timeout=...).
To avoid sending a request twice, only safe requests are retried automatically:
- Reads (
GET/HEAD/OPTIONS) are always retried. - Writes (
POST/PUT/PATCH/DELETE) are retried only when they carry anIdempotency-Key; otherwise they're attempted exactly once. emails.send()is retry-safe by default — it auto-generates oneIdempotency-Keyper call and reuses it across that call's retries, so a 429/5xx never produces a duplicate send.
Pass your own key to dedupe a send across separate calls (e.g. a retry from your own task queue) — the server treats a repeat of the same key as the same request:
from senddy import RequestOptions
client.emails.send(params, options=RequestOptions(idempotency_key="order-1234-receipt"))
API coverage
The SDK wraps the full public API surface (every method exists on both Senddy and AsyncSenddy):
emails—send,get,list,get_content,download,resendsuppressions—list,create,delete,exportdomains—list,get,create,add_subdomain,verify,delete,regenerate_dkim,check_dns_healthinbound_routes,inbound_emails,webhook_endpointsrecipient_lists—list,create,update,deletebilling—get_balance,get_history,get_payments,get_auto_refill,get_tier,get_usage,create_checkout,update_auto_refill,change_tier- the stateless
verify_webhook_signature()helper
See the API reference for every method signature and type.
Requirements
- Python >= 3.9
- Runtime dependency: httpx
Type Safety
The package ships with a py.typed marker (PEP 561) and all public types are fully annotated — works with mypy, pyright, and IDE autocompletion out of the box. Response models are generated from Senddy's OpenAPI specification, so they track the API exactly.
License
MIT
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
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 senddy-0.3.0.tar.gz.
File metadata
- Download URL: senddy-0.3.0.tar.gz
- Upload date:
- Size: 44.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a189261ab7bbe8898aaa1d9c038e1a63bddd797ecbf30668ed49498106b7ecc5
|
|
| MD5 |
cc919a4793cdcefe5ff1a3b1ffac17b3
|
|
| BLAKE2b-256 |
3c8ee94f95f71c98a26345d7f025e22a1382b6506222108a5d15dde1de673f63
|
File details
Details for the file senddy-0.3.0-py3-none-any.whl.
File metadata
- Download URL: senddy-0.3.0-py3-none-any.whl
- Upload date:
- Size: 32.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
91cb953f8745d52ac7e7680322eb91894abc7f441af059850e1b84b22a247913
|
|
| MD5 |
1984201dcce33cba996b599838a847f1
|
|
| BLAKE2b-256 |
149e098be0db03e283bc399ba38a4a30fc539846d0fcdd2efa8f7c406a3228fd
|