Python SDK
The official shipmail package provides both synchronous and asynchronous clients. Requires Python 3.10+.
Last updated
Installation
pip install shipmailuv add shipmailQuick 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
)Resources
The client exposes each API resource as a property:
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 supportPagination
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 signatureThrows 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},
)