Skip to main content

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 because from is a Python reserved word. The SDK maps it to from in 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 an Idempotency-Key; otherwise they're attempted exactly once.
  • emails.send() is retry-safe by default — it auto-generates one Idempotency-Key per 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):

  • emailssend, get, list, get_content, download, resend
  • suppressionslist, create, delete, export
  • domainslist, get, create, add_subdomain, verify, delete, regenerate_dkim, check_dns_health
  • inbound_routes, inbound_emails, webhook_endpoints
  • recipient_listslist, create, update, delete
  • billingget_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

senddy-0.3.0.tar.gz (44.9 kB view details)

Uploaded Source

Built Distribution

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

senddy-0.3.0-py3-none-any.whl (32.2 kB view details)

Uploaded Python 3

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

Hashes for senddy-0.3.0.tar.gz
Algorithm Hash digest
SHA256 a189261ab7bbe8898aaa1d9c038e1a63bddd797ecbf30668ed49498106b7ecc5
MD5 cc919a4793cdcefe5ff1a3b1ffac17b3
BLAKE2b-256 3c8ee94f95f71c98a26345d7f025e22a1382b6506222108a5d15dde1de673f63

See more details on using hashes here.

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

Hashes for senddy-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 91cb953f8745d52ac7e7680322eb91894abc7f441af059850e1b84b22a247913
MD5 1984201dcce33cba996b599838a847f1
BLAKE2b-256 149e098be0db03e283bc399ba38a4a30fc539846d0fcdd2efa8f7c406a3228fd

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