Skip to main content

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],
)

recipient must be either all saved-address IDs or all full addresses (Recipient / dict) — not a mix. Saved IDs use the top-level message / wishes; full addresses carry their own per-row values. The API handles the two modes differently, so the SDK raises ValueError if 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 confirmation
  • 1 (DeliveryConfirmation.CONFIRMATION, alias DELIVERY_CONFIRMATION) — delivery confirmation
  • 2 (DeliveryConfirmation.CASS_ONLY, alias CASS_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)

Source distribution for handwrytten 1.5.0
File Size Uploaded
handwrytten-1.5.0.tar.gz 62.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for handwrytten 1.5.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

1.5.0 This release

2 release files

1.3.2

2 release files

1.3.1

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page