Chapter 5: Managing Listings

The Heart of Your Digital Products Business

Listings are the core of any Etsy business. For digital products, listing management through the API enables powerful capabilities: bulk creation, automated updates, dynamic pricing, and efficient inventory management. This chapter covers everything you need to programmatically manage your digital product listings.

Understanding Listing Structure

A digital product listing contains several key components:

# Example listing structure for a digital product
listing_structure = {
    "listing_id": 123456789,
    "title": "Digital Planner 2026 | GoodNotes Planner | iPad Planner",
    "description": "Complete digital planner for your productivity needs...",
    "price": {"amount": 999, "divisor": 100, "currency_code": "USD"},
    "quantity": 999,  # High for digital (essentially unlimited)
    "state": "active",  # active, inactive, draft, expired
    "is_digital": True,
    "taxonomy_id": 1234,  # Category
    "tags": ["digital planner", "goodnotes", "ipad planner"],
    "materials": [],
    "shipping_profile_id": None,  # Not needed for digital
    "shop_section_id": 12345,
    "who_made": "i_did",
    "when_made": "2020_2026",
    "is_supply": False,
    "processing_min": None,
    "processing_max": None,
}

Listing Operations Module

# etsy_client/listings.py
"""Listing management operations for Etsy API."""

from typing import Dict, Any, Optional, List
from enum import Enum
from dataclasses import dataclass
from .client import EtsyClient
from .exceptions import ValidationError


class ListingState(Enum):
    """Possible states for a listing."""
    ACTIVE = "active"
    INACTIVE = "inactive"
    DRAFT = "draft"
    EXPIRED = "expired"
    SOLD_OUT = "sold_out"


class WhoMade(Enum):
    """Who made the item."""
    I_DID = "i_did"
    COLLECTIVE = "collective"
    SOMEONE_ELSE = "someone_else"


class WhenMade(Enum):
    """When the item was made."""
    MADE_TO_ORDER = "made_to_order"
    Y_2020_2026 = "2020_2026"
    Y_2010_2019 = "2010_2019"
    Y_2000_2009 = "2000_2009"
    BEFORE_2000 = "before_2000"


@dataclass
class ListingData:
    """Data class for creating/updating listings."""
    title: str
    description: str
    price: float
    quantity: int = 999
    taxonomy_id: int = None
    tags: List[str] = None
    materials: List[str] = None
    shop_section_id: int = None
    who_made: str = "i_did"
    when_made: str = "2020_2026"
    is_supply: bool = False
    is_digital: bool = True
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert to API-compatible dictionary."""
        data = {
            "title": self.title,
            "description": self.description,
            "price": self.price,
            "quantity": self.quantity,
            "who_made": self.who_made,
            "when_made": self.when_made,
            "is_supply": self.is_supply,
        }
        
        if self.taxonomy_id:
            data["taxonomy_id"] = self.taxonomy_id
        if self.tags:
            data["tags"] = self.tags[:13]  # Max 13 tags
        if self.materials:
            data["materials"] = self.materials[:13]  # Max 13 materials
        if self.shop_section_id:
            data["shop_section_id"] = self.shop_section_id
        
        return data


class ListingOperations:
    """Operations for managing Etsy listings."""
    
    def __init__(self, client: EtsyClient):
        self.client = client
    
    # ==================== READ OPERATIONS ====================
    
    def get_listing(self, listing_id: int) -> Dict[str, Any]:
        """
        Get a single listing by ID.
        
        Args:
            listing_id: The listing ID
            
        Returns:
            Listing data
        """
        return self.client.get(f"/application/listings/{listing_id}")
    
    def get_listings_by_shop(
        self,
        shop_id: int,
        state: str = "active",
        limit: int = 100,
        offset: int = 0
    ) -> Dict[str, Any]:
        """
        Get all listings for a shop.
        
        Args:
            shop_id: The shop ID
            state: Filter by state (active, inactive, draft, expired)
            limit: Maximum results per page (max 100)
            offset: Pagination offset
            
        Returns:
            Dictionary with 'results' list and pagination info
        """
        return self.client.get(
            f"/application/shops/{shop_id}/listings",
            params={
                "state": state,
                "limit": limit,
                "offset": offset
            }
        )
    
    def get_all_listings(
        self,
        shop_id: int,
        state: str = "active"
    ) -> List[Dict[str, Any]]:
        """
        Get ALL listings for a shop, handling pagination.
        
        Args:
            shop_id: The shop ID
            state: Filter by state
            
        Returns:
            List of all listings
        """
        all_listings = []
        offset = 0
        limit = 100
        
        while True:
            response = self.get_listings_by_shop(
                shop_id, state, limit, offset
            )
            
            listings = response.get('results', [])
            all_listings.extend(listings)
            
            if len(listings) < limit:
                break
            
            offset += limit
        
        return all_listings
    
    def search_listings(
        self,
        shop_id: int,
        keywords: str,
        limit: int = 25
    ) -> Dict[str, Any]:
        """
        Search listings by keywords.
        
        Args:
            shop_id: The shop ID
            keywords: Search keywords
            limit: Maximum results
            
        Returns:
            Search results
        """
        return self.client.get(
            f"/application/shops/{shop_id}/listings",
            params={
                "keywords": keywords,
                "limit": limit
            }
        )
    
    # ==================== CREATE OPERATIONS ====================
    
    def create_listing(
        self,
        shop_id: int,
        listing_data: ListingData,
        state: str = "draft"
    ) -> Dict[str, Any]:
        """
        Create a new listing.
        
        Args:
            shop_id: The shop ID
            listing_data: ListingData object with listing details
            state: Initial state (draft recommended for review)
            
        Returns:
            Created listing data
        """
        data = listing_data.to_dict()
        data["state"] = state
        
        # Validate required fields for digital products
        if not data.get("taxonomy_id"):
            raise ValidationError("taxonomy_id is required")
        
        return self.client.post(
            f"/application/shops/{shop_id}/listings",
            json=data
        )
    
    def create_digital_listing(
        self,
        shop_id: int,
        title: str,
        description: str,
        price: float,
        taxonomy_id: int,
        tags: List[str] = None,
        section_id: int = None,
        state: str = "draft"
    ) -> Dict[str, Any]:
        """
        Simplified method to create a digital product listing.
        
        Args:
            shop_id: The shop ID
            title: Listing title (max 140 chars)
            description: Listing description
            price: Price in shop currency
            taxonomy_id: Category ID
            tags: List of tags (max 13)
            section_id: Shop section ID
            state: Initial state
            
        Returns:
            Created listing data
        """
        listing = ListingData(
            title=title[:140],
            description=description,
            price=price,
            taxonomy_id=taxonomy_id,
            tags=tags,
            shop_section_id=section_id,
            is_digital=True
        )
        
        return self.create_listing(shop_id, listing, state)
    
    # ==================== UPDATE OPERATIONS ====================
    
    def update_listing(
        self,
        listing_id: int,
        **kwargs
    ) -> Dict[str, Any]:
        """
        Update a listing.
        
        Args:
            listing_id: The listing ID
            **kwargs: Fields to update
            
        Returns:
            Updated listing data
        """
        # Filter out None values
        data = {k: v for k, v in kwargs.items() if v is not None}
        
        if not data:
            raise ValidationError("No fields to update")
        
        return self.client.patch(
            f"/application/listings/{listing_id}",
            json=data
        )
    
    def update_price(
        self,
        listing_id: int,
        new_price: float
    ) -> Dict[str, Any]:
        """
        Update only the price of a listing.
        
        Args:
            listing_id: The listing ID
            new_price: New price value
            
        Returns:
            Updated listing data
        """
        return self.update_listing(listing_id, price=new_price)
    
    def update_quantity(
        self,
        listing_id: int,
        quantity: int
    ) -> Dict[str, Any]:
        """
        Update the quantity of a listing.
        
        Args:
            listing_id: The listing ID
            quantity: New quantity
            
        Returns:
            Updated listing data
        """
        return self.update_listing(listing_id, quantity=quantity)
    
    def activate_listing(self, listing_id: int) -> Dict[str, Any]:
        """Activate a draft or inactive listing."""
        return self.update_listing(listing_id, state="active")
    
    def deactivate_listing(self, listing_id: int) -> Dict[str, Any]:
        """Deactivate an active listing."""
        return self.update_listing(listing_id, state="inactive")
    
    # ==================== DELETE OPERATIONS ====================
    
    def delete_listing(self, listing_id: int) -> Dict[str, Any]:
        """
        Delete a listing.
        
        Args:
            listing_id: The listing ID
            
        Returns:
            Deletion confirmation
        """
        return self.client.delete(f"/application/listings/{listing_id}")
    
    # ==================== BULK OPERATIONS ====================
    
    def bulk_update_prices(
        self,
        listings: List[Dict[str, Any]],
        price_modifier: float
    ) -> List[Dict[str, Any]]:
        """
        Update prices for multiple listings.
        
        Args:
            listings: List of listings (must have listing_id and price)
            price_modifier: Multiplier for prices (e.g., 0.8 for 20% off)
            
        Returns:
            List of update results
        """
        results = []
        
        for listing in listings:
            listing_id = listing['listing_id']
            current_price = listing['price']['amount'] / listing['price']['divisor']
            new_price = round(current_price * price_modifier, 2)
            
            try:
                result = self.update_price(listing_id, new_price)
                results.append({
                    'listing_id': listing_id,
                    'success': True,
                    'old_price': current_price,
                    'new_price': new_price
                })
            except Exception as e:
                results.append({
                    'listing_id': listing_id,
                    'success': False,
                    'error': str(e)
                })
        
        return results
    
    def bulk_update_tags(
        self,
        listing_ids: List[int],
        tags: List[str]
    ) -> List[Dict[str, Any]]:
        """
        Update tags for multiple listings.
        
        Args:
            listing_ids: List of listing IDs
            tags: Tags to apply to all listings
            
        Returns:
            List of update results
        """
        results = []
        
        for listing_id in listing_ids:
            try:
                result = self.update_listing(listing_id, tags=tags[:13])
                results.append({
                    'listing_id': listing_id,
                    'success': True
                })
            except Exception as e:
                results.append({
                    'listing_id': listing_id,
                    'success': False,
                    'error': str(e)
                })
        
        return results

Finding Taxonomy IDs

Etsy uses taxonomy IDs to categorize listings. Here’s how to find the right one:

# etsy_client/taxonomy.py
"""Taxonomy (category) operations for Etsy API."""

from typing import Dict, Any, List, Optional
from .client import EtsyClient


class TaxonomyOperations:
    """Operations for working with Etsy taxonomies/categories."""
    
    # Common digital product taxonomy IDs
    COMMON_DIGITAL_TAXONOMIES = {
        "Digital Prints": 66,
        "Digital Planners": 1063,
        "SVG Files": 2078,
        "Clip Art": 68,
        "Digital Paper": 69,
        "Graphic Design Resources": 2079,
        "Social Media Templates": 2080,
        "Resume Templates": 2081,
        "Invitations & Announcements": 67,
        "Patterns & How To": 70,
    }
    
    def __init__(self, client: EtsyClient):
        self.client = client
    
    def get_buyer_taxonomy(self) -> Dict[str, Any]:
        """
        Get the complete buyer taxonomy tree.
        
        Returns:
            Taxonomy tree structure
        """
        return self.client.get("/application/buyer-taxonomy/nodes")
    
    def get_seller_taxonomy(self) -> Dict[str, Any]:
        """
        Get the seller taxonomy for listing creation.
        
        Returns:
            Seller taxonomy tree
        """
        return self.client.get("/application/seller-taxonomy/nodes")
    
    def search_taxonomy(
        self,
        query: str,
        taxonomy_data: Optional[Dict] = None
    ) -> List[Dict[str, Any]]:
        """
        Search for taxonomy nodes by name.
        
        Args:
            query: Search query
            taxonomy_data: Pre-fetched taxonomy (optional)
            
        Returns:
            List of matching taxonomy nodes
        """
        if taxonomy_data is None:
            taxonomy_data = self.get_seller_taxonomy()
        
        results = []
        query_lower = query.lower()
        
        def search_node(node: Dict, path: List[str] = None):
            path = path or []
            current_path = path + [node['name']]
            
            if query_lower in node['name'].lower():
                results.append({
                    'id': node['id'],
                    'name': node['name'],
                    'path': ' > '.join(current_path),
                    'level': len(current_path)
                })
            
            for child in node.get('children', []):
                search_node(child, current_path)
        
        for node in taxonomy_data.get('results', []):
            search_node(node)
        
        return results
    
    def get_taxonomy_properties(
        self,
        taxonomy_id: int
    ) -> Dict[str, Any]:
        """
        Get properties available for a taxonomy.
        
        Args:
            taxonomy_id: The taxonomy ID
            
        Returns:
            Available properties for the category
        """
        return self.client.get(
            f"/application/seller-taxonomy/nodes/{taxonomy_id}/properties"
        )


def find_digital_taxonomy(client: EtsyClient, product_type: str) -> int:
    """
    Helper to find the best taxonomy ID for a digital product type.
    
    Args:
        client: EtsyClient instance
        product_type: Description of the product type
        
    Returns:
        Best matching taxonomy ID
    """
    taxonomy_ops = TaxonomyOperations(client)
    
    # Check common taxonomies first
    product_lower = product_type.lower()
    for name, tax_id in taxonomy_ops.COMMON_DIGITAL_TAXONOMIES.items():
        if any(word in product_lower for word in name.lower().split()):
            return tax_id
    
    # Search taxonomy tree
    results = taxonomy_ops.search_taxonomy(product_type)
    
    if results:
        # Return the most specific (deepest) match
        results.sort(key=lambda x: x['level'], reverse=True)
        return results[0]['id']
    
    # Default to Digital Prints
    return 66


# Example usage
def explore_taxonomies(client: EtsyClient):
    """Print available digital product taxonomies."""
    
    tax_ops = TaxonomyOperations(client)
    
    print("Searching for digital product categories...")
    
    search_terms = [
        "digital",
        "printable",
        "template",
        "svg",
        "download"
    ]
    
    all_results = []
    for term in search_terms:
        results = tax_ops.search_taxonomy(term)
        all_results.extend(results)
    
    # Deduplicate
    seen = set()
    unique = []
    for r in all_results:
        if r['id'] not in seen:
            seen.add(r['id'])
            unique.append(r)
    
    print(f"\nFound {len(unique)} relevant categories:\n")
    
    for r in sorted(unique, key=lambda x: x['path']):
        print(f"ID: {r['id']:6} | {r['path']}")

Creating Digital Product Listings

Here’s a complete example of creating a digital product listing:

# scripts/create_listing.py
"""Create a digital product listing."""

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent))

from config import Config
from etsy_client import EtsyClient
from etsy_client.listings import ListingOperations, ListingData
from etsy_client.users import get_my_shop_id


def create_digital_planner_listing():
    """Example: Create a digital planner listing."""
    
    client = EtsyClient(
        api_key=Config.ETSY_API_KEY,
        access_token=Config.ETSY_ACCESS_TOKEN
    )
    
    shop_id = get_my_shop_id(client)
    listing_ops = ListingOperations(client)
    
    # Prepare listing data
    listing = ListingData(
        title="2026 Digital Planner | GoodNotes Planner | iPad Planner | Dated Daily Weekly Monthly",
        description="""📱 THE ULTIMATE DIGITAL PLANNER FOR 2026 📱

Transform your planning with this comprehensive digital planner designed for GoodNotes, Notability, and other PDF annotation apps.

✨ WHAT'S INCLUDED:
• Dated pages for all of 2026
• Monthly overview spreads
• Weekly horizontal layouts
• Daily planning pages
• Goal setting worksheets
• Habit trackers
• Notes sections
• Hyperlinked tabs for easy navigation

📋 FEATURES:
• Optimized for iPad and tablets
• Hyperlinked navigation
• Clickable tabs
• Clean, minimal design
• Portrait orientation
• Compatible with Apple Pencil & stylus

💻 COMPATIBLE WITH:
• GoodNotes 5 & 6
• Notability
• Noteshelf
• PDF Expert
• Any PDF annotation app

📥 INSTANT DOWNLOAD:
You'll receive your files immediately after purchase. No physical product will be shipped.

❓ QUESTIONS?
Message me anytime! I typically respond within 24 hours.

Thank you for visiting my shop! ❤️
""",
        price=9.99,
        quantity=999,
        taxonomy_id=1063,  # Digital Planners category
        tags=[
            "digital planner",
            "goodnotes planner",
            "ipad planner",
            "2026 planner",
            "digital planner 2026",
            "notability planner",
            "daily planner",
            "weekly planner",
            "monthly planner",
            "tablet planner",
            "instant download",
            "pdf planner",
            "hyperlinked planner"
        ],
        who_made="i_did",
        when_made="2020_2026",
        is_digital=True
    )
    
    # Create as draft first
    print("Creating listing as draft...")
    
    result = listing_ops.create_listing(
        shop_id=shop_id,
        listing_data=listing,
        state="draft"
    )
    
    listing_id = result['listing_id']
    print(f"✓ Listing created with ID: {listing_id}")
    print(f"  Title: {result['title']}")
    print(f"  State: {result['state']}")
    print(f"  Price: ${result['price']['amount'] / result['price']['divisor']:.2f}")
    
    print("\nNext steps:")
    print("1. Add listing images via the API or Etsy interface")
    print("2. Upload digital files for download")
    print("3. Activate the listing when ready")
    
    return result


if __name__ == "__main__":
    create_digital_planner_listing()

Bulk Listing Management

# scripts/bulk_operations.py
"""Bulk listing operations for digital products."""

import sys
import csv
from pathlib import Path
from typing import List, Dict

sys.path.insert(0, str(Path(__file__).parent.parent))

from config import Config
from etsy_client import EtsyClient
from etsy_client.listings import ListingOperations
from etsy_client.users import get_my_shop_id


class BulkListingManager:
    """Manage bulk listing operations."""
    
    def __init__(self, client: EtsyClient, shop_id: int):
        self.client = client
        self.shop_id = shop_id
        self.listing_ops = ListingOperations(client)
    
    def export_listings_to_csv(
        self,
        output_file: str,
        state: str = "active"
    ):
        """
        Export all listings to CSV for review/editing.
        
        Args:
            output_file: Path to output CSV file
            state: Filter by listing state
        """
        listings = self.listing_ops.get_all_listings(self.shop_id, state)
        
        if not listings:
            print("No listings found")
            return
        
        # Prepare CSV data
        fieldnames = [
            'listing_id', 'title', 'price', 'quantity',
            'views', 'favorites', 'tags', 'state'
        ]
        
        with open(output_file, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            
            for listing in listings:
                price = listing['price']['amount'] / listing['price']['divisor']
                
                writer.writerow({
                    'listing_id': listing['listing_id'],
                    'title': listing['title'],
                    'price': price,
                    'quantity': listing['quantity'],
                    'views': listing.get('views', 0),
                    'favorites': listing.get('num_favorers', 0),
                    'tags': ', '.join(listing.get('tags', [])),
                    'state': listing['state']
                })
        
        print(f"✓ Exported {len(listings)} listings to {output_file}")
    
    def apply_sale(
        self,
        discount_percent: float,
        min_price: float = 0.20
    ) -> Dict[str, int]:
        """
        Apply a sale discount to all active listings.
        
        Args:
            discount_percent: Discount percentage (e.g., 20 for 20% off)
            min_price: Minimum price after discount
            
        Returns:
            Summary of updates
        """
        listings = self.listing_ops.get_all_listings(self.shop_id, "active")
        
        multiplier = 1 - (discount_percent / 100)
        updated = 0
        errors = 0
        
        print(f"Applying {discount_percent}% discount to {len(listings)} listings...")
        
        for listing in listings:
            listing_id = listing['listing_id']
            current_price = listing['price']['amount'] / listing['price']['divisor']
            new_price = max(round(current_price * multiplier, 2), min_price)
            
            try:
                self.listing_ops.update_price(listing_id, new_price)
                print(f"  {listing_id}: ${current_price:.2f} → ${new_price:.2f}")
                updated += 1
            except Exception as e:
                print(f"  {listing_id}: Error - {e}")
                errors += 1
        
        return {"updated": updated, "errors": errors}
    
    def end_sale(self, original_prices: Dict[int, float]) -> Dict[str, int]:
        """
        Restore original prices after a sale.
        
        Args:
            original_prices: Dict mapping listing_id to original price
            
        Returns:
            Summary of updates
        """
        updated = 0
        errors = 0
        
        print(f"Restoring original prices for {len(original_prices)} listings...")
        
        for listing_id, original_price in original_prices.items():
            try:
                self.listing_ops.update_price(listing_id, original_price)
                print(f"  {listing_id}: restored to ${original_price:.2f}")
                updated += 1
            except Exception as e:
                print(f"  {listing_id}: Error - {e}")
                errors += 1
        
        return {"updated": updated, "errors": errors}
    
    def deactivate_low_performers(
        self,
        min_views: int = 100,
        min_favorites: int = 5,
        days_active: int = 90
    ) -> List[int]:
        """
        Deactivate listings with poor performance.
        
        Args:
            min_views: Minimum views threshold
            min_favorites: Minimum favorites threshold
            days_active: Days since creation to consider
            
        Returns:
            List of deactivated listing IDs
        """
        import time
        
        listings = self.listing_ops.get_all_listings(self.shop_id, "active")
        cutoff_time = time.time() - (days_active * 24 * 60 * 60)
        
        deactivated = []
        
        for listing in listings:
            created = listing.get('created_timestamp', time.time())
            
            # Skip recent listings
            if created > cutoff_time:
                continue
            
            views = listing.get('views', 0)
            favorites = listing.get('num_favorers', 0)
            
            if views < min_views and favorites < min_favorites:
                try:
                    self.listing_ops.deactivate_listing(listing['listing_id'])
                    deactivated.append(listing['listing_id'])
                    print(f"  Deactivated: {listing['title'][:50]}...")
                except Exception as e:
                    print(f"  Error deactivating {listing['listing_id']}: {e}")
        
        print(f"\n✓ Deactivated {len(deactivated)} low-performing listings")
        return deactivated
    
    def refresh_stale_listings(
        self,
        days_without_update: int = 30
    ) -> List[int]:
        """
        Touch listings that haven't been updated recently.
        
        Updating listings can improve their search ranking.
        
        Args:
            days_without_update: Days since last update threshold
            
        Returns:
            List of refreshed listing IDs
        """
        import time
        
        listings = self.listing_ops.get_all_listings(self.shop_id, "active")
        cutoff_time = time.time() - (days_without_update * 24 * 60 * 60)
        
        refreshed = []
        
        for listing in listings:
            updated = listing.get('updated_timestamp', listing.get('created_timestamp', 0))
            
            if updated < cutoff_time:
                try:
                    # Touch by updating quantity (no actual change)
                    self.listing_ops.update_quantity(
                        listing['listing_id'],
                        listing['quantity']
                    )
                    refreshed.append(listing['listing_id'])
                except Exception as e:
                    print(f"  Error refreshing {listing['listing_id']}: {e}")
        
        print(f"✓ Refreshed {len(refreshed)} stale listings")
        return refreshed


def main():
    """Demo bulk operations."""
    client = EtsyClient(
        api_key=Config.ETSY_API_KEY,
        access_token=Config.ETSY_ACCESS_TOKEN
    )
    
    shop_id = get_my_shop_id(client)
    manager = BulkListingManager(client, shop_id)
    
    # Export current listings
    manager.export_listings_to_csv("data/listings_export.csv")


if __name__ == "__main__":
    main()

Listing Image Management

# etsy_client/images.py
"""Listing image management."""

from typing import Dict, Any, List, BinaryIO
from pathlib import Path
from .client import EtsyClient


class ImageOperations:
    """Operations for managing listing images."""
    
    # Etsy image requirements
    MAX_IMAGES = 10
    MIN_SIZE = 2000  # pixels on longest side
    MAX_FILE_SIZE = 10 * 1024 * 1024  # 10MB
    ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif']
    
    def __init__(self, client: EtsyClient):
        self.client = client
    
    def get_listing_images(
        self,
        listing_id: int
    ) -> Dict[str, Any]:
        """
        Get all images for a listing.
        
        Args:
            listing_id: The listing ID
            
        Returns:
            List of image data
        """
        return self.client.get(f"/application/listings/{listing_id}/images")
    
    def upload_listing_image(
        self,
        shop_id: int,
        listing_id: int,
        image_path: str,
        rank: int = 1,
        overwrite: bool = False,
        is_watermarked: bool = False,
        alt_text: str = ""
    ) -> Dict[str, Any]:
        """
        Upload an image to a listing.
        
        Args:
            shop_id: The shop ID
            listing_id: The listing ID
            image_path: Path to the image file
            rank: Image position (1-10)
            overwrite: Replace existing image at rank
            is_watermarked: Whether image has watermark
            alt_text: Alt text for accessibility
            
        Returns:
            Uploaded image data
        """
        path = Path(image_path)
        
        if not path.exists():
            raise FileNotFoundError(f"Image not found: {image_path}")
        
        # Check file size
        if path.stat().st_size > self.MAX_FILE_SIZE:
            raise ValueError(f"Image too large. Max size: {self.MAX_FILE_SIZE / 1024 / 1024}MB")
        
        with open(path, 'rb') as f:
            files = {
                'image': (path.name, f, 'image/jpeg')
            }
            
            data = {
                'rank': rank,
                'overwrite': overwrite,
                'is_watermarked': is_watermarked,
            }
            
            if alt_text:
                data['alt_text'] = alt_text
            
            return self.client.post(
                f"/application/shops/{shop_id}/listings/{listing_id}/images",
                files=files,
                data=data
            )
    
    def delete_listing_image(
        self,
        listing_id: int,
        image_id: int
    ) -> Dict[str, Any]:
        """
        Delete an image from a listing.
        
        Args:
            listing_id: The listing ID
            image_id: The image ID
            
        Returns:
            Deletion confirmation
        """
        return self.client.delete(
            f"/application/listings/{listing_id}/images/{image_id}"
        )
    
    def reorder_listing_images(
        self,
        shop_id: int,
        listing_id: int,
        image_ids: List[int]
    ) -> Dict[str, Any]:
        """
        Reorder images on a listing.
        
        Args:
            shop_id: The shop ID
            listing_id: The listing ID
            image_ids: Image IDs in desired order
            
        Returns:
            Updated images
        """
        # Update rank for each image
        for rank, image_id in enumerate(image_ids, 1):
            # Note: This might require multiple API calls
            # Etsy's API may have a batch endpoint
            pass
        
        return self.get_listing_images(listing_id)
    
    def upload_multiple_images(
        self,
        shop_id: int,
        listing_id: int,
        image_paths: List[str]
    ) -> List[Dict[str, Any]]:
        """
        Upload multiple images to a listing.
        
        Args:
            shop_id: The shop ID
            listing_id: The listing ID
            image_paths: List of image file paths
            
        Returns:
            List of upload results
        """
        if len(image_paths) > self.MAX_IMAGES:
            raise ValueError(f"Too many images. Max: {self.MAX_IMAGES}")
        
        results = []
        
        for rank, path in enumerate(image_paths, 1):
            try:
                result = self.upload_listing_image(
                    shop_id, listing_id, path, rank=rank
                )
                results.append({
                    'path': path,
                    'success': True,
                    'image_id': result.get('listing_image_id')
                })
            except Exception as e:
                results.append({
                    'path': path,
                    'success': False,
                    'error': str(e)
                })
        
        return results

Key Takeaways

  1. Start with drafts: Create listings as drafts, then activate after review
  2. Use taxonomy correctly: Find the right category ID for better visibility
  3. Maximize tags: Use all 13 tags with relevant keywords
  4. Bulk operations: Build tools for managing many listings efficiently
  5. Handle images properly: Follow Etsy’s image requirements

Moving Forward

With listing management in place, you’re ready to tackle the unique aspect of digital products: file management and digital downloads. The next chapter covers uploading and managing the actual files customers will download.


← Chapter 4: Working with Shops and Users Table of Contents Chapter 6: Digital Downloads and File Management →