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.3.2.tar.gz (50.8 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.3.2-py3-none-any.whl (39.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: handwrytten-1.3.2.tar.gz
  • Upload date:
  • Size: 50.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for handwrytten-1.3.2.tar.gz
Algorithm Hash digest
SHA256 d8dbfbf958765620cdcc41eaef822dba936dab69801b40d752b5f87f610c62f7
MD5 7174e4dddaefd9bc35ed5d751ab00d5a
BLAKE2b-256 166c2a568a25cc66aff32f784f478571976ff6a24e7b19ae9da1c41aa62c0d38

See more details on using hashes here.

File details

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

File metadata

  • Download URL: handwrytten-1.3.2-py3-none-any.whl
  • Upload date:
  • Size: 39.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for handwrytten-1.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9719dc9d42cfcbb69962cdd204ab8345e43f1d413f2d039ec09a9045abbef481
MD5 aa22a84d3e03b774a70363ae0fcf0db4
BLAKE2b-256 941e21a933024212bc7b2a84b187fc5671411d8e8b3e4ee99d5bbdd448369bd8

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