Skip to main content

Official Python SDK for the Handwrytten API — send real handwritten notes at scale

Project description

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
)

# Mix saved IDs and inline addresses in a bulk send
result = client.orders.send(
    card_id="12345",
    font="hwDavid",
    message="Hello!",
    sender=98765,
    recipient=[
        67890,  # saved address ID
        {"firstName": "Jane", "lastName": "Doe",
         "street1": "123 Main St", "city": "Phoenix",
         "state": "AZ", "zip": "85001"},
    ],
)

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")

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.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.8+
  • requests

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

handwrytten-1.2.1.tar.gz (50.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

handwrytten-1.2.1-py3-none-any.whl (39.6 kB view details)

Uploaded Python 3

File details

Details for the file handwrytten-1.2.1.tar.gz.

File metadata

  • Download URL: handwrytten-1.2.1.tar.gz
  • Upload date:
  • Size: 50.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for handwrytten-1.2.1.tar.gz
Algorithm Hash digest
SHA256 7d682d2474280af77544f21786d9f3d485f7d7eb5b79716d39750853f24272c2
MD5 0cb1c3046550ee8416a7ecfeee1ce464
BLAKE2b-256 42123e74895c578b8ed3c46826f2a9d8f27be0fd82b890952db4c496066a623e

See more details on using hashes here.

File details

Details for the file handwrytten-1.2.1-py3-none-any.whl.

File metadata

  • Download URL: handwrytten-1.2.1-py3-none-any.whl
  • Upload date:
  • Size: 39.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for handwrytten-1.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 aca1c9f227e5634a461008d997f524747e434f62d7a62b2f26ba5ba99857b0d7
MD5 d1d1d7a39f765645c114b0a131757b94
BLAKE2b-256 a5516d92c991663109a171d841265f3ea04c34bfa565f3c6aa29c7c9cb492df4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page