mailerbot 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mailerbot-0.1.0/.gitignore +17 -0
- mailerbot-0.1.0/PKG-INFO +343 -0
- mailerbot-0.1.0/README.md +318 -0
- mailerbot-0.1.0/mailerbot/__init__.py +50 -0
- mailerbot-0.1.0/mailerbot/_http.py +53 -0
- mailerbot-0.1.0/mailerbot/_pagination.py +43 -0
- mailerbot-0.1.0/mailerbot/async_client.py +604 -0
- mailerbot-0.1.0/mailerbot/client.py +64 -0
- mailerbot-0.1.0/mailerbot/exceptions.py +30 -0
- mailerbot-0.1.0/mailerbot/models/__init__.py +28 -0
- mailerbot-0.1.0/mailerbot/models/assets.py +13 -0
- mailerbot-0.1.0/mailerbot/models/auth.py +62 -0
- mailerbot-0.1.0/mailerbot/models/campaigns.py +12 -0
- mailerbot-0.1.0/mailerbot/models/common.py +22 -0
- mailerbot-0.1.0/mailerbot/models/contacts.py +43 -0
- mailerbot-0.1.0/mailerbot/models/coupons.py +38 -0
- mailerbot-0.1.0/mailerbot/models/dashboard.py +25 -0
- mailerbot-0.1.0/mailerbot/models/documents.py +16 -0
- mailerbot-0.1.0/mailerbot/models/mailings.py +102 -0
- mailerbot-0.1.0/mailerbot/models/merge_tags.py +9 -0
- mailerbot-0.1.0/mailerbot/models/oauth.py +28 -0
- mailerbot-0.1.0/mailerbot/models/payments.py +20 -0
- mailerbot-0.1.0/mailerbot/models/postcards.py +32 -0
- mailerbot-0.1.0/mailerbot/models/qr.py +37 -0
- mailerbot-0.1.0/mailerbot/resources/__init__.py +0 -0
- mailerbot-0.1.0/mailerbot/resources/assets.py +40 -0
- mailerbot-0.1.0/mailerbot/resources/auth.py +58 -0
- mailerbot-0.1.0/mailerbot/resources/campaigns.py +62 -0
- mailerbot-0.1.0/mailerbot/resources/contact_lists.py +53 -0
- mailerbot-0.1.0/mailerbot/resources/contacts.py +97 -0
- mailerbot-0.1.0/mailerbot/resources/coupons.py +73 -0
- mailerbot-0.1.0/mailerbot/resources/dashboard.py +26 -0
- mailerbot-0.1.0/mailerbot/resources/documents.py +62 -0
- mailerbot-0.1.0/mailerbot/resources/mailings.py +145 -0
- mailerbot-0.1.0/mailerbot/resources/merge_tags.py +16 -0
- mailerbot-0.1.0/mailerbot/resources/oauth.py +38 -0
- mailerbot-0.1.0/mailerbot/resources/payments.py +33 -0
- mailerbot-0.1.0/mailerbot/resources/postcards.py +57 -0
- mailerbot-0.1.0/mailerbot/resources/pricing.py +35 -0
- mailerbot-0.1.0/mailerbot/resources/qr.py +54 -0
- mailerbot-0.1.0/pyproject.toml +44 -0
- mailerbot-0.1.0/test_local.py +681 -0
- mailerbot-0.1.0/tests/test_client.py +51 -0
- mailerbot-0.1.0/tests/test_contacts.py +62 -0
- mailerbot-0.1.0/tests/test_pagination.py +39 -0
- mailerbot-0.1.0/uv.lock +439 -0
mailerbot-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mailerbot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the MailerBot direct mail API
|
|
5
|
+
Project-URL: Homepage, https://mailerbot.com
|
|
6
|
+
Project-URL: Documentation, https://mailerbot.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/mailerbot-hq/MailerBot/tree/dev/sdk/python
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/mailerbot-hq/MailerBot/issues
|
|
9
|
+
Author-email: MailerBot <dev@mailerbot.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: api,direct mail,mailerbot,postal,sdk
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: pydantic[email]>=2.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# MailerBot Python SDK
|
|
27
|
+
|
|
28
|
+
Official Python SDK for the [MailerBot](https://mailerbot.com) direct mail API. Send letters and postcards programmatically.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install mailerbot
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires Python 3.10+.
|
|
37
|
+
|
|
38
|
+
## Authentication
|
|
39
|
+
|
|
40
|
+
Generate an API key in your [MailerBot dashboard](https://app.mailerbot.com/settings/api-keys), then pass it to the client:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import mailerbot
|
|
44
|
+
|
|
45
|
+
client = mailerbot.MailerBot(api_key="mb_live_...")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
### Synchronous
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import mailerbot
|
|
54
|
+
|
|
55
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
56
|
+
# List contacts
|
|
57
|
+
page = client.contacts.list(page=1, page_size=25)
|
|
58
|
+
print(f"{page.total} total contacts")
|
|
59
|
+
for contact in page.items:
|
|
60
|
+
print(f" {contact.first_name} {contact.last_name}, {contact.city}, {contact.state}")
|
|
61
|
+
|
|
62
|
+
# Dashboard stats
|
|
63
|
+
stats = client.dashboard.stats()
|
|
64
|
+
print(f"Letters sent: {stats.letters_sent}")
|
|
65
|
+
print(f"Total spent: ${stats.total_spent:.2f}")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Asynchronous
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import asyncio
|
|
72
|
+
import mailerbot
|
|
73
|
+
|
|
74
|
+
async def main():
|
|
75
|
+
async with mailerbot.AsyncMailerBot(api_key="mb_live_...") as client:
|
|
76
|
+
page = await client.contacts.list()
|
|
77
|
+
print(f"{page.total} contacts")
|
|
78
|
+
|
|
79
|
+
asyncio.run(main())
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Common Workflows
|
|
83
|
+
|
|
84
|
+
### Create and send a letter mailing
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import mailerbot
|
|
88
|
+
|
|
89
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
90
|
+
# 1. Create a contact list
|
|
91
|
+
contact_list = client.contact_lists.create("My Campaign List")
|
|
92
|
+
|
|
93
|
+
# 2. Add contacts (country_code defaults to "US" if omitted)
|
|
94
|
+
contact = client.contacts.create(
|
|
95
|
+
first_name="Jane",
|
|
96
|
+
last_name="Smith",
|
|
97
|
+
address_line1="123 Main St",
|
|
98
|
+
city="Austin",
|
|
99
|
+
state="TX",
|
|
100
|
+
zip="78701",
|
|
101
|
+
)
|
|
102
|
+
client.contact_lists.add_contacts(contact_list.id, [contact.id])
|
|
103
|
+
|
|
104
|
+
# 3. Write a letter document
|
|
105
|
+
doc = client.documents.create(
|
|
106
|
+
title="Spring Promo Letter",
|
|
107
|
+
content="<p>Dear {{first_name}},</p><p>Check out our spring deals!</p>",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# 4. Create the mailing (postage defaults to cheapest rate per zone)
|
|
111
|
+
mailing = client.mailings.create(
|
|
112
|
+
name="Spring 2026 Promo",
|
|
113
|
+
type="letter",
|
|
114
|
+
contact_list_id=contact_list.id,
|
|
115
|
+
document_id=doc.id,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# 5. Review the cost
|
|
119
|
+
cost = client.mailings.calculate_cost(mailing.id)
|
|
120
|
+
print(f"Product cost: ${cost.total_product_cost:.2f} ({cost.recipient_count} recipients)")
|
|
121
|
+
for zone in cost.zone_counts:
|
|
122
|
+
print(f" {zone.postage_zone_name}: {zone.recipient_count} recipients")
|
|
123
|
+
|
|
124
|
+
# 6. Create a Stripe payment intent, complete payment on your end, then send
|
|
125
|
+
intent = client.payments.create_payment_intent(mailing.id)
|
|
126
|
+
# ... complete Stripe payment using intent.client_secret ...
|
|
127
|
+
|
|
128
|
+
# 7. Send
|
|
129
|
+
sent = client.mailings.send(mailing.id)
|
|
130
|
+
print(f"Mailing status: {sent.status}")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Bulk import contacts from CSV
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
137
|
+
result = client.contacts.import_csv(
|
|
138
|
+
contacts=[
|
|
139
|
+
{"firstName": "Alice", "lastName": "Wu", "addressLine1": "456 Oak Ave",
|
|
140
|
+
"city": "Dallas", "state": "TX", "zip": "75201"},
|
|
141
|
+
{"firstName": "Bob", "lastName": "Smith", "addressLine1": "789 Pine Rd",
|
|
142
|
+
"city": "Houston", "state": "TX", "zip": "77001"},
|
|
143
|
+
],
|
|
144
|
+
list_name="Imported List",
|
|
145
|
+
)
|
|
146
|
+
print(f"Imported {result.imported_count} contacts into list {result.list_id}")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### International contacts
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
153
|
+
# List available countries
|
|
154
|
+
countries = client.pricing.countries()
|
|
155
|
+
for c in countries:
|
|
156
|
+
print(f" {c['code']} — {c['name']} (active: {c['isActive']})")
|
|
157
|
+
|
|
158
|
+
# Create a Canadian contact
|
|
159
|
+
contact = client.contacts.create(
|
|
160
|
+
first_name="Marie",
|
|
161
|
+
last_name="Tremblay",
|
|
162
|
+
address_line1="350 Rue Saint-Paul",
|
|
163
|
+
city="Montréal",
|
|
164
|
+
state="QC",
|
|
165
|
+
zip="H2Y 1H2",
|
|
166
|
+
country_code="CA",
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Estimate cost before creating a mailing
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
174
|
+
cost = client.mailings.estimate_cost(
|
|
175
|
+
type="postcard",
|
|
176
|
+
contact_list_id="<list_id>",
|
|
177
|
+
)
|
|
178
|
+
print(f"Product cost: ${cost.total_product_cost:.2f}")
|
|
179
|
+
for zone in cost.zone_counts:
|
|
180
|
+
print(f" {zone.postage_zone_name}: {zone.recipient_count} recipients")
|
|
181
|
+
for u in cost.unavailable:
|
|
182
|
+
print(f" ⚠ {u.country_name}: {u.recipient_count} recipients ({u.reason})")
|
|
183
|
+
|
|
184
|
+
# See available postage rates
|
|
185
|
+
rates = client.pricing.postage_rates(product_type="postcard")
|
|
186
|
+
for r in rates:
|
|
187
|
+
print(f" {r['postageZoneName']} — {r['label']}: ${r['costPerPiece']:.2f}")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Iterate over all contacts (auto-pagination)
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
194
|
+
for contact in client.contacts.iter_all(page_size=100):
|
|
195
|
+
print(contact.first_name, contact.last_name)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Async equivalent:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
async with mailerbot.AsyncMailerBot(api_key="mb_live_...") as client:
|
|
202
|
+
async for contact in client.contacts.iter_all():
|
|
203
|
+
print(contact.first_name, contact.last_name)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Send a postcard mailing
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
210
|
+
# Browse available templates
|
|
211
|
+
templates = client.postcards.list_templates()
|
|
212
|
+
print(f"{len(templates)} templates available")
|
|
213
|
+
|
|
214
|
+
# Create a postcard from scratch (or use the canvas builder in the dashboard)
|
|
215
|
+
postcard = client.postcards.create(title="Summer Sale Card")
|
|
216
|
+
|
|
217
|
+
mailing = client.mailings.create(
|
|
218
|
+
name="Summer Postcard Drop",
|
|
219
|
+
type="postcard",
|
|
220
|
+
contact_list_id="<list_id>",
|
|
221
|
+
postcard_id=postcard.id,
|
|
222
|
+
)
|
|
223
|
+
cost = client.mailings.calculate_cost(mailing.id)
|
|
224
|
+
print(f"${cost.total_product_cost:.2f} for {cost.recipient_count} postcards")
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Track QR code scans
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
231
|
+
# Create a trackable short link
|
|
232
|
+
link = client.qr.create("https://yoursite.com/promo")
|
|
233
|
+
print(f"Short URL: {link.short_url}")
|
|
234
|
+
|
|
235
|
+
# Get analytics
|
|
236
|
+
analytics = client.qr.analytics(days=30)
|
|
237
|
+
print(f"{analytics.total_scans} scans across {analytics.unique_links} links")
|
|
238
|
+
for day in analytics.scans_by_day:
|
|
239
|
+
print(f" {day.date}: {day.count} scans")
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Track USPS delivery per piece
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
246
|
+
mailing = client.mailings.get("<mailing_id>")
|
|
247
|
+
print(f"{mailing.items_delivered}/{mailing.item_count} delivered, {mailing.items_returned} returned")
|
|
248
|
+
|
|
249
|
+
# Pieces USPS flagged as return-to-sender (filter: none, in_transit,
|
|
250
|
+
# out_for_delivery, forwarded, delivered, returned)
|
|
251
|
+
for item in client.mailings.iter_items(mailing.id, tracking_status="returned"):
|
|
252
|
+
print(f"{item.recipient_name}: {item.last_scan_label} at {item.last_scan_location}")
|
|
253
|
+
|
|
254
|
+
# Full scan history for one piece
|
|
255
|
+
page = client.mailings.list_items(mailing.id, page_size=1)
|
|
256
|
+
for scan in client.mailings.list_item_scans(mailing.id, page.items[0].id):
|
|
257
|
+
print(f" {scan.scan_datetime} {scan.label} ({scan.facility_city}, {scan.facility_state})")
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Or subscribe to the `mail_delivered` and `mail_returned` webhook events to be pushed these updates instead of polling.
|
|
261
|
+
|
|
262
|
+
### Use coupon codes in mailings
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
266
|
+
# Create a coupon list and import codes
|
|
267
|
+
coupon_list = client.coupons.create("Spring Sale Coupons")
|
|
268
|
+
result = client.coupons.import_codes(coupon_list.id, ["SAVE10", "SAVE20", "SAVE30"])
|
|
269
|
+
print(f"Imported {result.imported_count} codes")
|
|
270
|
+
|
|
271
|
+
# Check there are enough codes before sending
|
|
272
|
+
avail = client.coupons.check_availability(coupon_list.id, count=500)
|
|
273
|
+
if not avail.sufficient:
|
|
274
|
+
print(f"Only {avail.available_codes} codes available, need 500")
|
|
275
|
+
|
|
276
|
+
# Attach to a mailing — each recipient gets a unique code
|
|
277
|
+
mailing = client.mailings.create(
|
|
278
|
+
name="Spring Promo",
|
|
279
|
+
type="letter",
|
|
280
|
+
contact_list_id="<list_id>",
|
|
281
|
+
document_id="<doc_id>",
|
|
282
|
+
coupon_list_id=coupon_list.id,
|
|
283
|
+
)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Error Handling
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
import mailerbot
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
with mailerbot.MailerBot(api_key="bad_key") as client:
|
|
293
|
+
client.contacts.list()
|
|
294
|
+
except mailerbot.AuthenticationError as e:
|
|
295
|
+
print(f"Auth failed: {e}")
|
|
296
|
+
except mailerbot.NotFoundError as e:
|
|
297
|
+
print(f"Resource not found: {e}")
|
|
298
|
+
except mailerbot.ValidationError as e:
|
|
299
|
+
print(f"Bad request: {e} — details: {e.response}")
|
|
300
|
+
except mailerbot.MailerBotError as e:
|
|
301
|
+
print(f"API error {e.status_code}: {e}")
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Exception hierarchy
|
|
305
|
+
|
|
306
|
+
| Exception | HTTP status |
|
|
307
|
+
|-----------|-------------|
|
|
308
|
+
| `AuthenticationError` | 401 |
|
|
309
|
+
| `PermissionError` | 403 |
|
|
310
|
+
| `NotFoundError` | 404 |
|
|
311
|
+
| `ValidationError` | 422 |
|
|
312
|
+
| `RateLimitError` | 429 |
|
|
313
|
+
| `ServerError` | 5xx |
|
|
314
|
+
| `MailerBotError` | base class / other |
|
|
315
|
+
|
|
316
|
+
## Configuration
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
client = mailerbot.MailerBot(
|
|
320
|
+
api_key="mb_live_...",
|
|
321
|
+
base_url="https://api.mailerbot.com/api/v1", # default
|
|
322
|
+
timeout=30.0, # seconds, default 30
|
|
323
|
+
)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
You can also inject your own `httpx.Client` (or `httpx.AsyncClient` for the async variant) for custom transport, proxies, or test mocking:
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
import httpx
|
|
330
|
+
import mailerbot
|
|
331
|
+
|
|
332
|
+
transport = httpx.MockTransport(...)
|
|
333
|
+
with mailerbot.MailerBot(api_key="...", http_client=httpx.Client(transport=transport)) as client:
|
|
334
|
+
...
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Full API Reference
|
|
338
|
+
|
|
339
|
+
See [https://mailerbot.com/docs](https://mailerbot.com/docs) for complete endpoint documentation.
|
|
340
|
+
|
|
341
|
+
## License
|
|
342
|
+
|
|
343
|
+
MIT
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# MailerBot Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [MailerBot](https://mailerbot.com) direct mail API. Send letters and postcards programmatically.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install mailerbot
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.10+.
|
|
12
|
+
|
|
13
|
+
## Authentication
|
|
14
|
+
|
|
15
|
+
Generate an API key in your [MailerBot dashboard](https://app.mailerbot.com/settings/api-keys), then pass it to the client:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import mailerbot
|
|
19
|
+
|
|
20
|
+
client = mailerbot.MailerBot(api_key="mb_live_...")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### Synchronous
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import mailerbot
|
|
29
|
+
|
|
30
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
31
|
+
# List contacts
|
|
32
|
+
page = client.contacts.list(page=1, page_size=25)
|
|
33
|
+
print(f"{page.total} total contacts")
|
|
34
|
+
for contact in page.items:
|
|
35
|
+
print(f" {contact.first_name} {contact.last_name}, {contact.city}, {contact.state}")
|
|
36
|
+
|
|
37
|
+
# Dashboard stats
|
|
38
|
+
stats = client.dashboard.stats()
|
|
39
|
+
print(f"Letters sent: {stats.letters_sent}")
|
|
40
|
+
print(f"Total spent: ${stats.total_spent:.2f}")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Asynchronous
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import asyncio
|
|
47
|
+
import mailerbot
|
|
48
|
+
|
|
49
|
+
async def main():
|
|
50
|
+
async with mailerbot.AsyncMailerBot(api_key="mb_live_...") as client:
|
|
51
|
+
page = await client.contacts.list()
|
|
52
|
+
print(f"{page.total} contacts")
|
|
53
|
+
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Common Workflows
|
|
58
|
+
|
|
59
|
+
### Create and send a letter mailing
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import mailerbot
|
|
63
|
+
|
|
64
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
65
|
+
# 1. Create a contact list
|
|
66
|
+
contact_list = client.contact_lists.create("My Campaign List")
|
|
67
|
+
|
|
68
|
+
# 2. Add contacts (country_code defaults to "US" if omitted)
|
|
69
|
+
contact = client.contacts.create(
|
|
70
|
+
first_name="Jane",
|
|
71
|
+
last_name="Smith",
|
|
72
|
+
address_line1="123 Main St",
|
|
73
|
+
city="Austin",
|
|
74
|
+
state="TX",
|
|
75
|
+
zip="78701",
|
|
76
|
+
)
|
|
77
|
+
client.contact_lists.add_contacts(contact_list.id, [contact.id])
|
|
78
|
+
|
|
79
|
+
# 3. Write a letter document
|
|
80
|
+
doc = client.documents.create(
|
|
81
|
+
title="Spring Promo Letter",
|
|
82
|
+
content="<p>Dear {{first_name}},</p><p>Check out our spring deals!</p>",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# 4. Create the mailing (postage defaults to cheapest rate per zone)
|
|
86
|
+
mailing = client.mailings.create(
|
|
87
|
+
name="Spring 2026 Promo",
|
|
88
|
+
type="letter",
|
|
89
|
+
contact_list_id=contact_list.id,
|
|
90
|
+
document_id=doc.id,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# 5. Review the cost
|
|
94
|
+
cost = client.mailings.calculate_cost(mailing.id)
|
|
95
|
+
print(f"Product cost: ${cost.total_product_cost:.2f} ({cost.recipient_count} recipients)")
|
|
96
|
+
for zone in cost.zone_counts:
|
|
97
|
+
print(f" {zone.postage_zone_name}: {zone.recipient_count} recipients")
|
|
98
|
+
|
|
99
|
+
# 6. Create a Stripe payment intent, complete payment on your end, then send
|
|
100
|
+
intent = client.payments.create_payment_intent(mailing.id)
|
|
101
|
+
# ... complete Stripe payment using intent.client_secret ...
|
|
102
|
+
|
|
103
|
+
# 7. Send
|
|
104
|
+
sent = client.mailings.send(mailing.id)
|
|
105
|
+
print(f"Mailing status: {sent.status}")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Bulk import contacts from CSV
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
112
|
+
result = client.contacts.import_csv(
|
|
113
|
+
contacts=[
|
|
114
|
+
{"firstName": "Alice", "lastName": "Wu", "addressLine1": "456 Oak Ave",
|
|
115
|
+
"city": "Dallas", "state": "TX", "zip": "75201"},
|
|
116
|
+
{"firstName": "Bob", "lastName": "Smith", "addressLine1": "789 Pine Rd",
|
|
117
|
+
"city": "Houston", "state": "TX", "zip": "77001"},
|
|
118
|
+
],
|
|
119
|
+
list_name="Imported List",
|
|
120
|
+
)
|
|
121
|
+
print(f"Imported {result.imported_count} contacts into list {result.list_id}")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### International contacts
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
128
|
+
# List available countries
|
|
129
|
+
countries = client.pricing.countries()
|
|
130
|
+
for c in countries:
|
|
131
|
+
print(f" {c['code']} — {c['name']} (active: {c['isActive']})")
|
|
132
|
+
|
|
133
|
+
# Create a Canadian contact
|
|
134
|
+
contact = client.contacts.create(
|
|
135
|
+
first_name="Marie",
|
|
136
|
+
last_name="Tremblay",
|
|
137
|
+
address_line1="350 Rue Saint-Paul",
|
|
138
|
+
city="Montréal",
|
|
139
|
+
state="QC",
|
|
140
|
+
zip="H2Y 1H2",
|
|
141
|
+
country_code="CA",
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Estimate cost before creating a mailing
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
149
|
+
cost = client.mailings.estimate_cost(
|
|
150
|
+
type="postcard",
|
|
151
|
+
contact_list_id="<list_id>",
|
|
152
|
+
)
|
|
153
|
+
print(f"Product cost: ${cost.total_product_cost:.2f}")
|
|
154
|
+
for zone in cost.zone_counts:
|
|
155
|
+
print(f" {zone.postage_zone_name}: {zone.recipient_count} recipients")
|
|
156
|
+
for u in cost.unavailable:
|
|
157
|
+
print(f" ⚠ {u.country_name}: {u.recipient_count} recipients ({u.reason})")
|
|
158
|
+
|
|
159
|
+
# See available postage rates
|
|
160
|
+
rates = client.pricing.postage_rates(product_type="postcard")
|
|
161
|
+
for r in rates:
|
|
162
|
+
print(f" {r['postageZoneName']} — {r['label']}: ${r['costPerPiece']:.2f}")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Iterate over all contacts (auto-pagination)
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
169
|
+
for contact in client.contacts.iter_all(page_size=100):
|
|
170
|
+
print(contact.first_name, contact.last_name)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Async equivalent:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
async with mailerbot.AsyncMailerBot(api_key="mb_live_...") as client:
|
|
177
|
+
async for contact in client.contacts.iter_all():
|
|
178
|
+
print(contact.first_name, contact.last_name)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Send a postcard mailing
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
185
|
+
# Browse available templates
|
|
186
|
+
templates = client.postcards.list_templates()
|
|
187
|
+
print(f"{len(templates)} templates available")
|
|
188
|
+
|
|
189
|
+
# Create a postcard from scratch (or use the canvas builder in the dashboard)
|
|
190
|
+
postcard = client.postcards.create(title="Summer Sale Card")
|
|
191
|
+
|
|
192
|
+
mailing = client.mailings.create(
|
|
193
|
+
name="Summer Postcard Drop",
|
|
194
|
+
type="postcard",
|
|
195
|
+
contact_list_id="<list_id>",
|
|
196
|
+
postcard_id=postcard.id,
|
|
197
|
+
)
|
|
198
|
+
cost = client.mailings.calculate_cost(mailing.id)
|
|
199
|
+
print(f"${cost.total_product_cost:.2f} for {cost.recipient_count} postcards")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Track QR code scans
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
206
|
+
# Create a trackable short link
|
|
207
|
+
link = client.qr.create("https://yoursite.com/promo")
|
|
208
|
+
print(f"Short URL: {link.short_url}")
|
|
209
|
+
|
|
210
|
+
# Get analytics
|
|
211
|
+
analytics = client.qr.analytics(days=30)
|
|
212
|
+
print(f"{analytics.total_scans} scans across {analytics.unique_links} links")
|
|
213
|
+
for day in analytics.scans_by_day:
|
|
214
|
+
print(f" {day.date}: {day.count} scans")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Track USPS delivery per piece
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
221
|
+
mailing = client.mailings.get("<mailing_id>")
|
|
222
|
+
print(f"{mailing.items_delivered}/{mailing.item_count} delivered, {mailing.items_returned} returned")
|
|
223
|
+
|
|
224
|
+
# Pieces USPS flagged as return-to-sender (filter: none, in_transit,
|
|
225
|
+
# out_for_delivery, forwarded, delivered, returned)
|
|
226
|
+
for item in client.mailings.iter_items(mailing.id, tracking_status="returned"):
|
|
227
|
+
print(f"{item.recipient_name}: {item.last_scan_label} at {item.last_scan_location}")
|
|
228
|
+
|
|
229
|
+
# Full scan history for one piece
|
|
230
|
+
page = client.mailings.list_items(mailing.id, page_size=1)
|
|
231
|
+
for scan in client.mailings.list_item_scans(mailing.id, page.items[0].id):
|
|
232
|
+
print(f" {scan.scan_datetime} {scan.label} ({scan.facility_city}, {scan.facility_state})")
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Or subscribe to the `mail_delivered` and `mail_returned` webhook events to be pushed these updates instead of polling.
|
|
236
|
+
|
|
237
|
+
### Use coupon codes in mailings
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
with mailerbot.MailerBot(api_key="mb_live_...") as client:
|
|
241
|
+
# Create a coupon list and import codes
|
|
242
|
+
coupon_list = client.coupons.create("Spring Sale Coupons")
|
|
243
|
+
result = client.coupons.import_codes(coupon_list.id, ["SAVE10", "SAVE20", "SAVE30"])
|
|
244
|
+
print(f"Imported {result.imported_count} codes")
|
|
245
|
+
|
|
246
|
+
# Check there are enough codes before sending
|
|
247
|
+
avail = client.coupons.check_availability(coupon_list.id, count=500)
|
|
248
|
+
if not avail.sufficient:
|
|
249
|
+
print(f"Only {avail.available_codes} codes available, need 500")
|
|
250
|
+
|
|
251
|
+
# Attach to a mailing — each recipient gets a unique code
|
|
252
|
+
mailing = client.mailings.create(
|
|
253
|
+
name="Spring Promo",
|
|
254
|
+
type="letter",
|
|
255
|
+
contact_list_id="<list_id>",
|
|
256
|
+
document_id="<doc_id>",
|
|
257
|
+
coupon_list_id=coupon_list.id,
|
|
258
|
+
)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Error Handling
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
import mailerbot
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
with mailerbot.MailerBot(api_key="bad_key") as client:
|
|
268
|
+
client.contacts.list()
|
|
269
|
+
except mailerbot.AuthenticationError as e:
|
|
270
|
+
print(f"Auth failed: {e}")
|
|
271
|
+
except mailerbot.NotFoundError as e:
|
|
272
|
+
print(f"Resource not found: {e}")
|
|
273
|
+
except mailerbot.ValidationError as e:
|
|
274
|
+
print(f"Bad request: {e} — details: {e.response}")
|
|
275
|
+
except mailerbot.MailerBotError as e:
|
|
276
|
+
print(f"API error {e.status_code}: {e}")
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Exception hierarchy
|
|
280
|
+
|
|
281
|
+
| Exception | HTTP status |
|
|
282
|
+
|-----------|-------------|
|
|
283
|
+
| `AuthenticationError` | 401 |
|
|
284
|
+
| `PermissionError` | 403 |
|
|
285
|
+
| `NotFoundError` | 404 |
|
|
286
|
+
| `ValidationError` | 422 |
|
|
287
|
+
| `RateLimitError` | 429 |
|
|
288
|
+
| `ServerError` | 5xx |
|
|
289
|
+
| `MailerBotError` | base class / other |
|
|
290
|
+
|
|
291
|
+
## Configuration
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
client = mailerbot.MailerBot(
|
|
295
|
+
api_key="mb_live_...",
|
|
296
|
+
base_url="https://api.mailerbot.com/api/v1", # default
|
|
297
|
+
timeout=30.0, # seconds, default 30
|
|
298
|
+
)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
You can also inject your own `httpx.Client` (or `httpx.AsyncClient` for the async variant) for custom transport, proxies, or test mocking:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
import httpx
|
|
305
|
+
import mailerbot
|
|
306
|
+
|
|
307
|
+
transport = httpx.MockTransport(...)
|
|
308
|
+
with mailerbot.MailerBot(api_key="...", http_client=httpx.Client(transport=transport)) as client:
|
|
309
|
+
...
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Full API Reference
|
|
313
|
+
|
|
314
|
+
See [https://mailerbot.com/docs](https://mailerbot.com/docs) for complete endpoint documentation.
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
MIT
|