Metadata-Version: 2.5
Name: mailerbot
Version: 0.1.0
Summary: Python SDK for the MailerBot direct mail API
Project-URL: Homepage, https://mailerbot.com
Project-URL: Documentation, https://mailerbot.com/docs
Project-URL: Repository, https://github.com/mailerbot-hq/MailerBot/tree/dev/sdk/python
Project-URL: Bug Tracker, https://github.com/mailerbot-hq/MailerBot/issues
Author-email: MailerBot <dev@mailerbot.com>
License: MIT
Keywords: api,direct mail,mailerbot,postal,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic[email]>=2.0
Description-Content-Type: text/markdown

# MailerBot Python SDK

Official Python SDK for the [MailerBot](https://mailerbot.com) direct mail API. Send letters and postcards programmatically.

## Installation

```bash
pip install mailerbot
```

Requires Python 3.10+.

## Authentication

Generate an API key in your [MailerBot dashboard](https://app.mailerbot.com/settings/api-keys), then pass it to the client:

```python
import mailerbot

client = mailerbot.MailerBot(api_key="mb_live_...")
```

## Quick Start

### Synchronous

```python
import mailerbot

with mailerbot.MailerBot(api_key="mb_live_...") as client:
    # List contacts
    page = client.contacts.list(page=1, page_size=25)
    print(f"{page.total} total contacts")
    for contact in page.items:
        print(f"  {contact.first_name} {contact.last_name}, {contact.city}, {contact.state}")

    # Dashboard stats
    stats = client.dashboard.stats()
    print(f"Letters sent: {stats.letters_sent}")
    print(f"Total spent: ${stats.total_spent:.2f}")
```

### Asynchronous

```python
import asyncio
import mailerbot

async def main():
    async with mailerbot.AsyncMailerBot(api_key="mb_live_...") as client:
        page = await client.contacts.list()
        print(f"{page.total} contacts")

asyncio.run(main())
```

## Common Workflows

### Create and send a letter mailing

```python
import mailerbot

with mailerbot.MailerBot(api_key="mb_live_...") as client:
    # 1. Create a contact list
    contact_list = client.contact_lists.create("My Campaign List")

    # 2. Add contacts (country_code defaults to "US" if omitted)
    contact = client.contacts.create(
        first_name="Jane",
        last_name="Smith",
        address_line1="123 Main St",
        city="Austin",
        state="TX",
        zip="78701",
    )
    client.contact_lists.add_contacts(contact_list.id, [contact.id])

    # 3. Write a letter document
    doc = client.documents.create(
        title="Spring Promo Letter",
        content="<p>Dear {{first_name}},</p><p>Check out our spring deals!</p>",
    )

    # 4. Create the mailing (postage defaults to cheapest rate per zone)
    mailing = client.mailings.create(
        name="Spring 2026 Promo",
        type="letter",
        contact_list_id=contact_list.id,
        document_id=doc.id,
    )

    # 5. Review the cost
    cost = client.mailings.calculate_cost(mailing.id)
    print(f"Product cost: ${cost.total_product_cost:.2f} ({cost.recipient_count} recipients)")
    for zone in cost.zone_counts:
        print(f"  {zone.postage_zone_name}: {zone.recipient_count} recipients")

    # 6. Create a Stripe payment intent, complete payment on your end, then send
    intent = client.payments.create_payment_intent(mailing.id)
    # ... complete Stripe payment using intent.client_secret ...

    # 7. Send
    sent = client.mailings.send(mailing.id)
    print(f"Mailing status: {sent.status}")
```

### Bulk import contacts from CSV

```python
with mailerbot.MailerBot(api_key="mb_live_...") as client:
    result = client.contacts.import_csv(
        contacts=[
            {"firstName": "Alice", "lastName": "Wu", "addressLine1": "456 Oak Ave",
             "city": "Dallas", "state": "TX", "zip": "75201"},
            {"firstName": "Bob", "lastName": "Smith", "addressLine1": "789 Pine Rd",
             "city": "Houston", "state": "TX", "zip": "77001"},
        ],
        list_name="Imported List",
    )
    print(f"Imported {result.imported_count} contacts into list {result.list_id}")
```

### International contacts

```python
with mailerbot.MailerBot(api_key="mb_live_...") as client:
    # List available countries
    countries = client.pricing.countries()
    for c in countries:
        print(f"  {c['code']} — {c['name']} (active: {c['isActive']})")

    # Create a Canadian contact
    contact = client.contacts.create(
        first_name="Marie",
        last_name="Tremblay",
        address_line1="350 Rue Saint-Paul",
        city="Montréal",
        state="QC",
        zip="H2Y 1H2",
        country_code="CA",
    )
```

### Estimate cost before creating a mailing

```python
with mailerbot.MailerBot(api_key="mb_live_...") as client:
    cost = client.mailings.estimate_cost(
        type="postcard",
        contact_list_id="<list_id>",
    )
    print(f"Product cost: ${cost.total_product_cost:.2f}")
    for zone in cost.zone_counts:
        print(f"  {zone.postage_zone_name}: {zone.recipient_count} recipients")
    for u in cost.unavailable:
        print(f"  ⚠ {u.country_name}: {u.recipient_count} recipients ({u.reason})")

    # See available postage rates
    rates = client.pricing.postage_rates(product_type="postcard")
    for r in rates:
        print(f"  {r['postageZoneName']} — {r['label']}: ${r['costPerPiece']:.2f}")
```

### Iterate over all contacts (auto-pagination)

```python
with mailerbot.MailerBot(api_key="mb_live_...") as client:
    for contact in client.contacts.iter_all(page_size=100):
        print(contact.first_name, contact.last_name)
```

Async equivalent:

```python
async with mailerbot.AsyncMailerBot(api_key="mb_live_...") as client:
    async for contact in client.contacts.iter_all():
        print(contact.first_name, contact.last_name)
```

### Send a postcard mailing

```python
with mailerbot.MailerBot(api_key="mb_live_...") as client:
    # Browse available templates
    templates = client.postcards.list_templates()
    print(f"{len(templates)} templates available")

    # Create a postcard from scratch (or use the canvas builder in the dashboard)
    postcard = client.postcards.create(title="Summer Sale Card")

    mailing = client.mailings.create(
        name="Summer Postcard Drop",
        type="postcard",
        contact_list_id="<list_id>",
        postcard_id=postcard.id,
    )
    cost = client.mailings.calculate_cost(mailing.id)
    print(f"${cost.total_product_cost:.2f} for {cost.recipient_count} postcards")
```

### Track QR code scans

```python
with mailerbot.MailerBot(api_key="mb_live_...") as client:
    # Create a trackable short link
    link = client.qr.create("https://yoursite.com/promo")
    print(f"Short URL: {link.short_url}")

    # Get analytics
    analytics = client.qr.analytics(days=30)
    print(f"{analytics.total_scans} scans across {analytics.unique_links} links")
    for day in analytics.scans_by_day:
        print(f"  {day.date}: {day.count} scans")
```

### Track USPS delivery per piece

```python
with mailerbot.MailerBot(api_key="mb_live_...") as client:
    mailing = client.mailings.get("<mailing_id>")
    print(f"{mailing.items_delivered}/{mailing.item_count} delivered, {mailing.items_returned} returned")

    # Pieces USPS flagged as return-to-sender (filter: none, in_transit,
    # out_for_delivery, forwarded, delivered, returned)
    for item in client.mailings.iter_items(mailing.id, tracking_status="returned"):
        print(f"{item.recipient_name}: {item.last_scan_label} at {item.last_scan_location}")

    # Full scan history for one piece
    page = client.mailings.list_items(mailing.id, page_size=1)
    for scan in client.mailings.list_item_scans(mailing.id, page.items[0].id):
        print(f"  {scan.scan_datetime} {scan.label} ({scan.facility_city}, {scan.facility_state})")
```

Or subscribe to the `mail_delivered` and `mail_returned` webhook events to be pushed these updates instead of polling.

### Use coupon codes in mailings

```python
with mailerbot.MailerBot(api_key="mb_live_...") as client:
    # Create a coupon list and import codes
    coupon_list = client.coupons.create("Spring Sale Coupons")
    result = client.coupons.import_codes(coupon_list.id, ["SAVE10", "SAVE20", "SAVE30"])
    print(f"Imported {result.imported_count} codes")

    # Check there are enough codes before sending
    avail = client.coupons.check_availability(coupon_list.id, count=500)
    if not avail.sufficient:
        print(f"Only {avail.available_codes} codes available, need 500")

    # Attach to a mailing — each recipient gets a unique code
    mailing = client.mailings.create(
        name="Spring Promo",
        type="letter",
        contact_list_id="<list_id>",
        document_id="<doc_id>",
        coupon_list_id=coupon_list.id,
    )
```

## Error Handling

```python
import mailerbot

try:
    with mailerbot.MailerBot(api_key="bad_key") as client:
        client.contacts.list()
except mailerbot.AuthenticationError as e:
    print(f"Auth failed: {e}")
except mailerbot.NotFoundError as e:
    print(f"Resource not found: {e}")
except mailerbot.ValidationError as e:
    print(f"Bad request: {e} — details: {e.response}")
except mailerbot.MailerBotError as e:
    print(f"API error {e.status_code}: {e}")
```

### Exception hierarchy

| Exception | HTTP status |
|-----------|-------------|
| `AuthenticationError` | 401 |
| `PermissionError` | 403 |
| `NotFoundError` | 404 |
| `ValidationError` | 422 |
| `RateLimitError` | 429 |
| `ServerError` | 5xx |
| `MailerBotError` | base class / other |

## Configuration

```python
client = mailerbot.MailerBot(
    api_key="mb_live_...",
    base_url="https://api.mailerbot.com/api/v1",  # default
    timeout=30.0,                                  # seconds, default 30
)
```

You can also inject your own `httpx.Client` (or `httpx.AsyncClient` for the async variant) for custom transport, proxies, or test mocking:

```python
import httpx
import mailerbot

transport = httpx.MockTransport(...)
with mailerbot.MailerBot(api_key="...", http_client=httpx.Client(transport=transport)) as client:
    ...
```

## Full API Reference

See [https://mailerbot.com/docs](https://mailerbot.com/docs) for complete endpoint documentation.

## License

MIT
