Python SDK

The official shipmail package provides both synchronous and asynchronous clients. Requires Python 3.10+.

Last updated

Installation

pip install shipmailuv add shipmail

Quick start

from shipmail import ShipMail client = ShipMail("sm_live_...") message = client.messages.send({ "from": "hello@yourdomain.com", "to": ["recipient@example.com"], "subject": "Hello from shipmail", "text": "It works.", }) print(message["id"]) # msg_...

Async client

Use AsyncShipMail for async/await workflows. Works as a context manager for automatic cleanup.

from shipmail import AsyncShipMail async with AsyncShipMail("sm_live_...") as client: message = await client.messages.send({ "from": "hello@yourdomain.com", "to": ["recipient@example.com"], "subject": "Hello", "text": "Hi there", })

Configuration

client = ShipMail( "sm_live_...", base_url="https://shipmail.to/api/v1", # default max_retries=2, # default timeout=30.0, # seconds, default )
OptionDefaultDescription
api_keyrequiredYour sm_live_... key.
base_urlhttps://shipmail.to/api/v1API base URL.
max_retries2Retry count for 429 and 5xx responses.
timeout30.0Request timeout in seconds.

Resources

The client exposes each API resource as a property:

PropertyMethods
client.domainscreate, list, get, update, delete, verify, search, register
client.mailboxescreate, list, get, update, delete, update_auto_reply
client.messagessend, get
client.threadslist, get, reply
client.webhookscreate, list, get, update, delete, rotate_secret, test, list_deliveries
client.suppressionslist, remove
client.audiencescreate, list, get, update, delete
client.audiences.subscribersadd, add_batch, list, get, get_by_email, update, unsubscribe, remove
client.newsletterscreate, create_from_changelog, list, get, update, preview, preflight, send_test, schedule, cancel, resume
client.newsletters.domainslist
client.newsletters.assetslist, upload
client.statusget

Newsletter workflow

Use blocks for editable newsletters. Prose fields inside blocks are plain text; use newlines for paragraph breaks. Use body_html or custom_html only when you need raw HTML.

newsletter_domains = client.newsletters.domains.list({"limit": 25}) with open("feature.png", "rb") as f: image = client.newsletters.assets.upload({ "filename": "feature.png", "content_type": "image/png", "data": f.read(), }) existing_image = client.newsletters.assets.register_from_url({ "url": "https://cdn.shipmail.to/newsletter-images/org_123/demo.png", "filename": "demo.png", }) newsletter = client.newsletters.create({ "audience_id": "aud_...", "newsletter_domain_id": newsletter_domains["data"][0]["id"], "name": "July changelog", "subject": "What shipped in July", "preview_text": "A quick product update", "blocks": [ {"type": "heading", "level": 1, "text": "July updates"}, {"type": "callout", "variant": "info", "title": "Quick note", "body": "A short intro."}, {"type": "paragraph", "body": "Use blank lines for paragraph breaks."}, {"type": "image", "url": image["url"], "alt": "Product screenshot"}, {"type": "image", "url": existing_image["url"], "alt": "Existing CDN screenshot"}, { "type": "columns", "ratio": "50-50", "left": {"title": "For teams", "body": "Shared inbox improvements."}, "right": {"title": "For agents", "body": "API and MCP improvements."}, }, ], }) client.newsletters.preview(newsletter["id"]) client.newsletters.send_test(newsletter["id"], { "recipient_email": "owner@example.com", }) client.newsletters.preflight(newsletter["id"]) client.newsletters.schedule(newsletter["id"], { "scheduled_at": "2026-08-01T09:00:00.000Z", })

Preflight returns a URL breakdown. Links, images, and video thumbnails count toward the 20 unique URL limit.

Error handling

API errors raise typed subclasses of ShipMailError. Each error exposes status, type, request_id, and retryable. Newsletter guardrail failures on test-send and schedule are ValidationErrors with preflight fields in details.

from shipmail import ( ShipMailError, ValidationError, RateLimitError, APIConnectionError, ) try: client.messages.send({...}) except ValidationError as err: print(err.details) # field-level errors except RateLimitError as err: print(err.retry_after) # seconds to wait except ShipMailError as err: print(err.status) # HTTP status code print(err.request_id) # request ID for support
ClassStatus
AuthenticationError401
AuthorizationError403
ValidationError422
NotFoundError404
ConflictError409
RateLimitError429
InternalServerError500
APIConnectionErrorNetwork failure

Pagination

Manual cursor-based pagination:

page = client.domains.list({"limit": 10}) for domain in page["data"]: print(domain["name"]) if page["pagination"]["has_more"]: next_page = client.domains.list({ "cursor": page["pagination"]["next_cursor"], })

Auto-pagination iterates all pages automatically:

# Sync for domain in client.domains.list_auto_paginating(limit=25): print(domain["name"]) # Async async for domain in client.domains.list_auto_paginating(limit=25): print(domain["name"])

Webhook verification

The SDK exports a standalone verify_webhook function. No client instance needed.

from shipmail import verify_webhook, WebhookVerificationError try: event = verify_webhook(raw_body, headers, webhook_secret) print(event["event_type"]) # "message.received" except WebhookVerificationError: pass # invalid signature

Throws WebhookVerificationError if the signature is invalid or the timestamp is outside the 5-minute tolerance window. Pass tolerance_in_seconds=600 to customize.

Per-request options

Every method accepts an optional options argument for request-level overrides:

client.domains.create( {"name": "example.com"}, {"idempotency_key": "550e8400-...", "timeout": 10.0}, )

Links