Handwrytten Python SDK
The official Python SDK for the Handwrytten API — send real handwritten notes at scale using robots with real pens.
Installation
pip install handwrytten
Quick Start
from handwrytten import Handwrytten
client = Handwrytten("your_api_key")
# Browse available cards and fonts
cards = client.cards.list()
fonts = client.fonts.list()
# Send a handwritten note in one call
result = client.orders.send(
card_id=cards[0].id,
font=fonts[0].id,
message="Thanks for being an amazing customer!",
wishes="Best,\nThe Handwrytten Team",
sender={
"firstName": "David",
"lastName": "Wachs",
"street1": "100 S Mill Ave",
"city": "Tempe",
"state": "AZ",
"zip": "85281",
},
recipient={
"firstName": "Jane",
"lastName": "Doe",
"street1": "123 Main Street",
"city": "Phoenix",
"state": "AZ",
"zip": "85001",
},
)
Usage
Send a Single Note
result = client.orders.send(
card_id="12345",
font="hwDavid",
message="Thank you for your business!",
wishes="Best,\nThe Team",
sender={
"firstName": "David",
"lastName": "Wachs",
"street1": "100 S Mill Ave",
"city": "Tempe",
"state": "AZ",
"zip": "85281",
},
recipient={
"firstName": "Jane",
"lastName": "Doe",
"street1": "123 Main St",
"city": "Phoenix",
"state": "AZ",
"zip": "85001",
},
)
Send Bulk — Multiple Recipients with Per-Recipient Overrides
Each recipient can have its own message, wishes, and sender. Top-level values serve as defaults for any recipient that doesn't specify its own.
result = client.orders.send(
card_id="12345",
font="hwDavid",
sender={"firstName": "David", "lastName": "Wachs",
"street1": "100 S Mill Ave", "city": "Tempe",
"state": "AZ", "zip": "85281"},
recipient=[
{
"firstName": "Jane",
"lastName": "Doe",
"street1": "123 Main St",
"city": "Phoenix",
"state": "AZ",
"zip": "85001",
"message": "Thanks for your loyalty, Jane!",
"wishes": "Warmly,\nThe Team",
},
{
"firstName": "John",
"lastName": "Smith",
"street1": "456 Oak Ave",
"city": "Tempe",
"state": "AZ",
"zip": "85281",
"message": "Great working with you, John!",
"sender": {"firstName": "Other", "lastName": "Person",
"street1": "789 Elm St", "city": "Mesa",
"state": "AZ", "zip": "85201"},
},
],
)
Use Saved Address IDs
If you have addresses saved in your Handwrytten account, pass their IDs directly:
result = client.orders.send(
card_id="12345",
font="hwDavid",
message="Thank you!",
sender=98765, # saved return-address ID
recipient=67890, # saved recipient address ID
)
# Bulk with multiple saved IDs (top-level message/wishes applies to all)
result = client.orders.send(
card_id="12345",
font="hwDavid",
message="Hello!",
sender=98765,
recipient=[67890, 11111, 22222],
)
recipientmust be either all saved-address IDs or all full addresses (Recipient/ dict) — not a mix. Saved IDs use the top-levelmessage/wishes; full addresses carry their own per-row values. The API handles the two modes differently, so the SDK raisesValueErrorif you mix them.
Use Typed Models
from handwrytten import Recipient, Sender
sender = Sender(
first_name="David",
last_name="Wachs",
street1="100 S Mill Ave",
city="Tempe",
state="AZ",
zip="85281",
)
recipient = Recipient(
first_name="Jane",
last_name="Doe",
street1="123 Main Street",
city="Phoenix",
state="AZ",
zip="85001",
)
result = client.orders.send(
card_id="12345",
font="hwDavid",
message="Welcome aboard!",
sender=sender,
recipient=recipient,
)
Custom Cards
Create custom cards with your own cover images and logos.
# 1. Get available card dimensions
dims = client.custom_cards.dimensions()
for d in dims:
print(d.id, d) # e.g. "1 7.000x5.000 flat (landscape)"
# Filter by format and/or orientation
flat_dims = client.custom_cards.dimensions(format="flat")
landscape = client.custom_cards.dimensions(format="flat", orientation="landscape")
# 2. Upload a full-bleed cover image (front of card)
cover = client.custom_cards.upload_image(
url="https://example.com/cover.jpg",
image_type="cover",
)
# 3. Upload a logo (appears on the writing side)
logo = client.custom_cards.upload_image(
url="https://example.com/logo.png",
image_type="logo",
)
# Or upload from a local file
logo = client.custom_cards.upload_image(
file_path="/path/to/logo.png",
image_type="logo",
)
# 4. Check image quality (optional)
check = client.custom_cards.check_image(image_id=logo.id)
# 5. Create the custom card
card = client.custom_cards.create(
name="My Custom Card",
dimension_id=dims[0].id, # card dimension
cover_id=cover.id, # front cover image
header_logo_id=logo.id, # logo on writing side
header_logo_size_percent=80,
)
# 6. Use the new card to send orders
client.orders.send(
card_id=str(card.card_id),
font="hwDavid",
message="Hello from our custom card!",
recipient={...},
)
Custom cards support text and logos in multiple zones:
| Zone | Logo field | Text field | Font field |
|---|---|---|---|
| Header (top of writing side) | header_logo_id |
header_text |
header_font_id |
| Main (center, folded cards) | main_logo_id |
main_text |
main_font_id |
| Footer (bottom of writing side) | footer_logo_id |
footer_text |
footer_font_id |
| Back | back_logo_id |
back_text |
back_font_id |
| Front cover | cover_id |
— | — |
| Back cover | back_cover_id |
— | — |
Font IDs for text zones come from client.fonts.list_for_customizer() (printed/typeset fonts), which are different from the handwriting fonts used in client.fonts.list().
Manage Custom Images
# List all uploaded images
images = client.custom_cards.list_images()
for img in images:
print(img.id, img.image_type, img.image_url)
# Filter by type
covers = client.custom_cards.list_images(image_type="cover")
logos = client.custom_cards.list_images(image_type="logo")
# Get details of a custom card
card = client.custom_cards.get(card_id=456)
# Delete an image
client.custom_cards.delete_image(image_id=123)
# Delete a custom card
client.custom_cards.delete(card_id=456)
Browse Cards and Fonts
# Card templates
cards = client.cards.list()
card = client.cards.get("12345")
categories = client.cards.categories()
# Handwriting fonts (for orders)
fonts = client.fonts.list()
for font in fonts:
print(f"{font.id}: {font.label}")
# Customizer fonts (for custom card text zones)
customizer_fonts = client.fonts.list_for_customizer()
Gift Cards and Inserts
# List gift cards with their denominations (price points)
gift_cards = client.gift_cards.list()
for gc in gift_cards:
print(f"{gc.title}: {len(gc.denominations)} denominations")
for d in gc.denominations:
print(f" ${d.nominal} (price: ${d.price})")
# Include a gift card denomination in an order
client.orders.send(
card_id="12345",
font="hwDavid",
message="Enjoy!",
denomination_id=gc.denominations[0].id,
recipient={...},
)
# List inserts (optionally include historical/discontinued)
inserts = client.inserts.list()
all_inserts = client.inserts.list(include_historical=True)
# Include an insert in an order
client.orders.send(
card_id="12345",
font="hwDavid",
message="Hello!",
insert_id=inserts[0].id,
recipient={...},
)
QR Codes
Create QR codes and attach them to custom cards.
from handwrytten import QRCodeLocation
# Create a QR code
qr = client.qr_codes.create(name="Website Link", url="https://example.com")
# List existing QR codes
qr_codes = client.qr_codes.list()
# Browse available frames (decorative borders around the QR code)
frames = client.qr_codes.frames()
# Attach a QR code to a custom card
card = client.custom_cards.create(
name="Card with QR",
dimension_id=dims[0].id,
cover_id=cover.id,
qr_code_id=int(qr.id),
qr_code_location=QRCodeLocation.FOOTER, # HEADER, FOOTER, or MAIN
qr_code_size_percent=30,
qr_code_align="right",
)
# Delete a QR code
client.qr_codes.delete(qr_code_id=int(qr.id))
Address Book
Save and manage recipient and sender addresses, then use their IDs when sending orders.
# Save a sender (return address)
sender_id = client.address_book.add_sender(
first_name="David",
last_name="Wachs",
street1="100 S Mill Ave",
city="Tempe",
state="AZ",
zip="85281",
)
# Save a recipient
recipient_id = client.address_book.add_recipient(
first_name="Jane",
last_name="Doe",
street1="123 Main St",
city="Phoenix",
state="AZ",
zip="85001",
)
# Send using saved IDs
client.orders.send(
card_id="12345",
font="hwDavid",
message="Hello!",
sender=sender_id,
recipient=recipient_id,
)
# Update a recipient
client.address_book.update_recipient(
address_id=recipient_id,
street1="456 New St",
city="Scottsdale",
)
# List saved addresses
senders = client.address_book.list_senders()
recipients = client.address_book.list_recipients()
for r in recipients:
print(r.id, r) # e.g. "123 Jane Doe, 456 New St, Scottsdale, AZ 85001"
# Delete addresses
client.address_book.delete_recipient(address_id=recipient_id)
client.address_book.delete_sender(address_id=sender_id)
# Batch delete
client.address_book.delete_recipient(address_ids=[1, 2, 3])
# Countries and states
countries = client.address_book.countries()
states = client.address_book.states("US")
Stamp Options & Delivery Confirmation
Pick a postal stamp option (e.g. First Class vs. Presorted) for US orders, and request delivery confirmation or CASS-only address validation.
from handwrytten import DeliveryConfirmation
# List available stamp options
options = client.shipping.stamp_options()
for opt in options:
print(opt.id, opt.name, opt.price)
# Use one on an order (US mail only — ignored for international)
client.orders.send(
card_id="12345",
font="hwDavid",
message="Hello!",
recipient={...},
stamp_option_id=options[0].id,
delivery_confirmation=DeliveryConfirmation.CONFIRMATION, # 0/1/2 or bool
)
delivery_confirmation accepts an int:
0(DeliveryConfirmation.NONE) — no confirmation1(DeliveryConfirmation.CONFIRMATION, aliasDELIVERY_CONFIRMATION) — delivery confirmation2(DeliveryConfirmation.CASS_ONLY, aliasCASS_VALIDATION) — CASS address validation only
Booleans remain backward compatible: False → 0, True → 1.
Signatures
List the user's saved handwriting signatures for use in orders.
signatures = client.auth.list_signatures()
for sig in signatures:
print(f" [{sig.id}] preview={sig.preview}")
Two-Step Basket Workflow
For finer control, use client.basket directly instead of client.orders.send():
# Step 1: Add order(s) to the basket
client.basket.add_order(
card_id="12345",
font="hwDavid",
addresses=[{
"firstName": "Jane",
"lastName": "Doe",
"street1": "123 Main St",
"city": "Phoenix",
"state": "AZ",
"zip": "85001",
"message": "Hello!",
}],
)
# Step 2: Submit the basket
result = client.basket.send()
# Inspect the basket before sending
basket = client.basket.list() # all items with totals
item = client.basket.get_item(9517) # single item by basket_id
n = client.basket.count() # number of items
# Remove a specific item or clear everything
client.basket.remove(basket_id=9517)
client.basket.clear()
# List previously submitted baskets
past = client.orders.list_past_baskets(page=1)
Error Handling
from handwrytten import (
HandwryttenError,
AuthenticationError,
BadRequestError,
RateLimitError,
)
try:
result = client.orders.send(...)
except AuthenticationError:
print("Check your API key")
except BadRequestError as e:
print(f"Invalid request: {e.message}")
print(f"Details: {e.response_body}")
except RateLimitError as e:
print(f"Rate limited — retry after {e.retry_after}s")
except HandwryttenError as e:
print(f"API error: {e}")
API Resources
| Resource | Methods |
|---|---|
client.auth |
get_user(), login(), list_signatures() |
client.cards |
list(), get(id), categories() |
client.custom_cards |
dimensions(), upload_image(), check_image(), list_images(), delete_image(), create(), get(), delete() |
client.fonts |
list(), list_for_customizer() |
client.gift_cards |
list() |
client.inserts |
list(include_historical) |
client.qr_codes |
list(), create(), delete(), frames() |
client.address_book |
list_recipients(), add_recipient(), update_recipient(), delete_recipient(), list_senders(), add_sender(), delete_sender(), countries(), states(country) |
client.shipping |
stamp_options() |
client.orders |
send(), get(id), list(), list_past_baskets() |
client.basket |
add_order(), send(), remove(basket_id), clear(), list(), get_item(basket_id), count() |
client.prospecting |
calculate_targets(zip, radius) |
Configuration
client = Handwrytten(
api_key="your_key",
timeout=60, # seconds
max_retries=5, # automatic retries with exponential backoff
)
Full Example
See examples/example.py for a complete working demo that exercises every resource: listing cards/fonts, sending single and bulk orders, uploading custom images, creating custom cards, and cleanup.
Requirements
- Python 3.10+
requests
License
MIT
Metadata
Release files for handwrytten 1.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| handwrytten-1.5.0.tar.gz | 62.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| handwrytten-1.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 105.4 kB
Release files / handwrytten-1.5.0.tar.gz
| Download URL | handwrytten-1.5.0.tar.gz |
|---|---|
| Size | 62.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
18fabb4fa22db9d68c44d05194fdac8e726efa71ce71aaf99fdb4dff83ec2227
|
|
BLAKE2b-256 checksum How to use checksums |
a1290a5aed53f6f7fe6668dfa5252da341d59090d5f911942c0010a8196aba91
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Release files / handwrytten-1.5.0-py3-none-any.whl
| Download URL | handwrytten-1.5.0-py3-none-any.whl |
|---|---|
| Size | 43.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
29431ead0a248557121b4d7d83656ca485d985349f445c61febb83b8dfdd0dc9
|
|
BLAKE2b-256 checksum How to use checksums |
8a4ffa8ca64eb6f173c2387bd720bab33d8b6264ac19e4d4e5b748a83e3ba8c5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|