Orders and Fulfillment

Overview

For digital products, “fulfillment” is automatic—Etsy delivers the files immediately after purchase. But you still need to:

This chapter covers accessing and managing order data via the API.

Understanding Etsy’s Order Structure

Etsy uses a hierarchy:

Receipt (Order)
└── Transactions (Line Items)
    ├── Transaction 1: Product A
    └── Transaction 2: Product B

Retrieving Orders

Get Recent Orders

def get_recent_orders(client, shop_id, limit=25):
    """Get the most recent orders."""
    response = client.get(
        f"/application/shops/{shop_id}/receipts",
        params={"limit": limit}
    )
    return response["results"]

Get Orders in Date Range

from datetime import datetime, timedelta

def get_orders_in_range(client, shop_id, start_date, end_date):
    """Get orders within a specific date range."""
    # Convert to Unix timestamps
    min_created = int(start_date.timestamp())
    max_created = int(end_date.timestamp())
    
    orders = []
    offset = 0
    
    while True:
        response = client.get(
            f"/application/shops/{shop_id}/receipts",
            params={
                "min_created": min_created,
                "max_created": max_created,
                "limit": 100,
                "offset": offset
            }
        )
        orders.extend(response["results"])
        
        if len(response["results"]) < 100:
            break
        offset += 100
    
    return orders

Get a Specific Order

def get_order(client, shop_id, receipt_id):
    """Get details for a specific order."""
    return client.get(f"/application/shops/{shop_id}/receipts/{receipt_id}")

The Receipt Object

A receipt contains comprehensive order information:

{
    "receipt_id": 1234567890,
    "receipt_type": 0,
    "status": "paid",
    "buyer_user_id": 98765432,
    "buyer_email": "customer@email.com",
    "name": "Customer Name",
    "total_price": {"amount": 1498, "divisor": 100, "currency_code": "USD"},
    "subtotal": {"amount": 1400, "divisor": 100, "currency_code": "USD"},
    "total_tax_cost": {"amount": 98, "divisor": 100, "currency_code": "USD"},
    "create_timestamp": 1699900000,
    "is_paid": true,
    "transactions": [...]
}

Working with Transactions

Each transaction represents one item purchased:

def get_order_items(client, shop_id, receipt_id):
    """Get individual items from an order."""
    order = get_order(client, shop_id, receipt_id)
    
    items = []
    for transaction in order.get("transactions", []):
        items.append({
            "transaction_id": transaction["transaction_id"],
            "listing_id": transaction["listing_id"],
            "title": transaction["title"],
            "quantity": transaction["quantity"],
            "price": transaction["price"]["amount"] / transaction["price"]["divisor"]
        })
    
    return items

Digital Product Specifics

Auto-Fulfillment

Digital products are automatically marked as fulfilled when payment completes. You don’t need to “ship” them:

def is_digital_order(order):
    """Check if an order contains only digital items."""
    for transaction in order.get("transactions", []):
        if not transaction.get("is_digital", False):
            return False
    return True

Download Status

Unfortunately, the API doesn’t expose whether customers have downloaded their files. This is a limitation you’ll need to work around (e.g., ask in messages if they need help).

Order Filtering

Filter by Product Type

def get_orders_by_listing(client, shop_id, listing_id):
    """Get all orders for a specific listing."""
    all_orders = get_recent_orders(client, shop_id, limit=100)
    
    matching = []
    for order in all_orders:
        for transaction in order.get("transactions", []):
            if transaction["listing_id"] == listing_id:
                matching.append(order)
                break
    
    return matching

Filter by Status

def get_orders_by_status(client, shop_id, status):
    """Get orders with a specific status."""
    response = client.get(
        f"/application/shops/{shop_id}/receipts",
        params={"was_paid": status == "paid"}
    )
    return response["results"]

Practical Order Reports

Daily Sales Summary

See code/orders.py for complete implementation.

def daily_sales_summary(client, shop_id, date=None):
    """Generate a sales summary for a specific day."""
    if date is None:
        date = datetime.now()
    
    start = datetime(date.year, date.month, date.day)
    end = start + timedelta(days=1)
    
    orders = get_orders_in_range(client, shop_id, start, end)
    
    total_revenue = sum(
        o["total_price"]["amount"] / o["total_price"]["divisor"] 
        for o in orders
    )
    
    return {
        "date": date.strftime("%Y-%m-%d"),
        "order_count": len(orders),
        "total_revenue": total_revenue,
        "average_order": total_revenue / len(orders) if orders else 0
    }

Product Performance Report

def product_sales_report(client, shop_id, days=30):
    """See which products sold in the last N days."""
    end_date = datetime.now()
    start_date = end_date - timedelta(days=days)
    
    orders = get_orders_in_range(client, shop_id, start_date, end_date)
    
    product_sales = {}
    for order in orders:
        for transaction in order.get("transactions", []):
            listing_id = transaction["listing_id"]
            if listing_id not in product_sales:
                product_sales[listing_id] = {
                    "title": transaction["title"],
                    "quantity": 0,
                    "revenue": 0
                }
            product_sales[listing_id]["quantity"] += transaction["quantity"]
            product_sales[listing_id]["revenue"] += (
                transaction["price"]["amount"] / transaction["price"]["divisor"]
            )
    
    # Sort by revenue
    return sorted(
        product_sales.values(), 
        key=lambda x: x["revenue"], 
        reverse=True
    )

Handling Customer Issues

Finding Customer Orders

When a customer contacts you, find their orders:

def find_customer_orders(client, shop_id, buyer_email=None, buyer_user_id=None):
    """Find orders from a specific customer."""
    all_orders = get_recent_orders(client, shop_id, limit=100)
    
    matching = []
    for order in all_orders:
        if buyer_email and order.get("buyer_email") == buyer_email:
            matching.append(order)
        elif buyer_user_id and order.get("buyer_user_id") == buyer_user_id:
            matching.append(order)
    
    return matching

Common Customer Issues

For digital products, common issues include:

  1. Can’t find download link: Direct them to Purchases > Download Files
  2. Wrong file format: Check if you offer the format they need
  3. Corrupted file: Verify your uploaded files are intact
  4. Didn’t receive email: Download is on Etsy, not email

Export to CSV

Export orders for accounting or analysis:

import csv
from pathlib import Path

def export_orders_csv(orders, filename="orders.csv"):
    """Export orders to CSV file."""
    fieldnames = ["receipt_id", "date", "customer", "total", "items"]
    
    with open(filename, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        
        for order in orders:
            writer.writerow({
                "receipt_id": order["receipt_id"],
                "date": datetime.fromtimestamp(order["create_timestamp"]).isoformat(),
                "customer": order.get("name", "N/A"),
                "total": order["total_price"]["amount"] / order["total_price"]["divisor"],
                "items": len(order.get("transactions", []))
            })
    
    return Path(filename).absolute()

Order Notifications

While the API doesn’t have push notifications, you can poll for new orders:

import time

def monitor_new_orders(client, shop_id, check_interval=300):
    """Poll for new orders every N seconds."""
    last_check = datetime.now()
    
    while True:
        orders = get_orders_in_range(
            client, shop_id, 
            last_check, 
            datetime.now()
        )
        
        for order in orders:
            print(f"New order: {order['receipt_id']} - ${order['total_price']['amount']/100}")
            # Could trigger email, Slack notification, etc.
        
        last_check = datetime.now()
        time.sleep(check_interval)

For production, consider using Etsy’s webhooks (limited) or a proper job scheduler.

What’s Next

Digital products often come in variations (PDF vs PNG, A4 vs Letter). The next chapter covers managing product variations and inventory.


← Previous: Digital Files Next: Inventory & Variations →

← Back to Table of Contents