Digital products often come in variations:
This chapter covers managing these variations through the API.
Etsy uses a specific structure for variations:
Listing
├── Property 1: "Size" (Letter, A4, A5)
├── Property 2: "Format" (PDF, PNG)
└── Offerings (combinations with prices)
├── Letter + PDF = $4.99
├── Letter + PNG = $4.99
├── A4 + PDF = $4.99
└── ...
Each combination is called an offering with its own price and SKU.
For digital products, you typically have unlimited stock. But there are scenarios where you might want limited “inventory”:
For most digital products:
def set_unlimited_inventory(client, shop_id, listing_id):
"""Set high quantity for digital product."""
client.put(
f"/application/shops/{shop_id}/listings/{listing_id}",
data={"quantity": 999}
)
First, define the properties and their options:
VARIATION_PROPERTIES = {
"size": {
"property_id": 100, # Etsy property IDs vary
"options": ["Letter", "A4", "A5"]
},
"format": {
"property_id": 200,
"options": ["PDF", "PNG"]
}
}
def create_listing_with_variations(client, shop_id, listing_data, variations):
"""Create a listing with product variations."""
# First create the base listing
listing = client.post(
f"/application/shops/{shop_id}/listings",
data=listing_data
)
listing_id = listing["listing_id"]
# Then add variations via inventory endpoint
inventory_data = build_inventory_payload(variations)
client.put(
f"/application/listings/{listing_id}/inventory",
data=inventory_data
)
return listing_id
The inventory structure is complex. See code/variations.py for complete implementation.
def build_inventory_payload(variations, base_price):
"""Build the inventory payload for Etsy API."""
products = []
# Generate all combinations
from itertools import product as cartesian
option_lists = [v["options"] for v in variations.values()]
for combo in cartesian(*option_lists):
products.append({
"sku": "-".join(combo),
"property_values": [
{"property_id": pid, "value": val}
for pid, val in zip(variations.keys(), combo)
],
"offerings": [{
"price": base_price,
"quantity": 999,
"is_enabled": True
}]
})
return {"products": products}
def get_listing_inventory(client, listing_id):
"""Get inventory and variations for a listing."""
return client.get(f"/application/listings/{listing_id}/inventory")
def get_variation_options(inventory):
"""Extract variation options from inventory data."""
options = {}
for product in inventory.get("products", []):
for prop_value in product.get("property_values", []):
prop_name = prop_value.get("property_name")
value = prop_value.get("values", [None])[0]
if prop_name not in options:
options[prop_name] = set()
options[prop_name].add(value)
return {k: list(v) for k, v in options.items()}
def update_variation_price(client, listing_id, sku, new_price):
"""Update price for a specific variation (by SKU)."""
inventory = get_listing_inventory(client, listing_id)
for product in inventory["products"]:
if product.get("sku") == sku:
product["offerings"][0]["price"] = new_price
client.put(
f"/application/listings/{listing_id}/inventory",
data=inventory
)
Adding a new option (e.g., new size) requires rebuilding the inventory:
def add_variation_option(client, listing_id, property_name, new_option, base_price):
"""Add a new option to an existing variation."""
inventory = get_listing_inventory(client, listing_id)
# Get existing products as template
template = inventory["products"][0] if inventory["products"] else None
if not template:
return None
# Create new product entry for new option
new_product = {
"sku": f"{new_option}-variant",
"property_values": [
{"property_id": pv["property_id"], "value": new_option}
if pv["property_name"] == property_name
else pv
for pv in template["property_values"]
],
"offerings": [{
"price": base_price,
"quantity": 999,
"is_enabled": True
}]
}
inventory["products"].append(new_product)
client.put(
f"/application/listings/{listing_id}/inventory",
data=inventory
)
FORMAT_VARIATIONS = {
"format": {
"options": ["PDF", "PNG", "JPG", "SVG"],
"prices": {"PDF": 499, "PNG": 499, "JPG": 399, "SVG": 599}
}
}
SIZE_VARIATIONS = {
"size": {
"options": ["Letter (8.5x11)", "A4", "A5", "5x7"],
"prices": {"Letter (8.5x11)": 499, "A4": 499, "A5": 399, "5x7": 299}
}
}
BUNDLE_VARIATIONS = {
"bundle": {
"options": ["Single", "Pack of 3", "Pack of 10", "Full Collection"],
"prices": {"Single": 499, "Pack of 3": 999, "Pack of 10": 2499, "Full Collection": 4999}
}
}
A key challenge: Etsy attaches files to the listing, not to individual variations. All customers get all files regardless of which variation they purchase.
Option 1: Include all files
Upload all formats/sizes. Customers choose what they need.
Files: planner-letter.pdf, planner-a4.pdf, planner-a5.pdf
Option 2: Separate listings
Create separate listings for each major variation.
Listing 1: Planner - Letter Size
Listing 2: Planner - A4 Size
Option 3: ZIP bundles
Create ZIP files for each variation combo.
Upload: letter-pdf.zip, a4-pdf.zip, letter-png.zip, etc.
def bulk_update_variation_prices(client, shop_id, price_multiplier):
"""Apply price change to all variations across all listings."""
listings = get_all_listings(client, shop_id)
for listing in listings:
inventory = get_listing_inventory(client, listing["listing_id"])
for product in inventory.get("products", []):
for offering in product.get("offerings", []):
old_price = offering["price"]
offering["price"] = int(old_price * price_multiplier)
client.put(
f"/application/listings/{listing['listing_id']}/inventory",
data=inventory
)
def toggle_variation(client, listing_id, sku, enabled):
"""Enable or disable a specific variation."""
inventory = get_listing_inventory(client, listing_id)
for product in inventory["products"]:
if product.get("sku") == sku:
product["offerings"][0]["is_enabled"] = enabled
client.put(
f"/application/listings/{listing_id}/inventory",
data=inventory
)
SKUs help you track variations:
def generate_sku(product_name, variation_combo):
"""Generate consistent SKU from product and variations."""
base = product_name.lower().replace(" ", "-")[:10]
var_code = "-".join(v[:3].upper() for v in variation_combo)
return f"{base}-{var_code}"
# Example: "planner-LET-PDF" for Letter size PDF
With listings, files, and variations managed, the next chapter covers extracting analytics and insights from your sales data.
| ← Previous: Orders & Fulfillment | Next: Analytics → |