Manage Etsy shops programmatically via the internal API and Playwright browser automation. No official API keys needed - uses cookie-based authentication from Chrome DevTools.
10-20x faster than Playwright-only approaches - the internal API handles most operations in under 1 second.
Etsy's web interface uses an internal REST API at /api/v3/ajax/shop/{shopId}/listings. This tool authenticates using cookies exported from your browser and a CSRF nonce extracted from the dashboard page.
Two modes:
- API mode (httpx): Fast path for all CRUD operations (~0.5-1s per operation)
- Browser mode (Playwright): Fallback for complex form filling and order scraping
All operations automatically try API first and fall back to Playwright if needed.
- Full listing CRUD (create, read, update, delete)
- Batch update multiple listings at once
- Image upload via multipart API
- Order monitoring (Playwright)
- Multi-shop support
- Cookie session management with import CLI
- Anti-detection (stealth JS, human-like delays)
- Rate limiting (configurable)
# API mode only (recommended - covers most use cases)
pip install etsy-browser
# With Playwright browser fallback
pip install 'etsy-browser[browser]'
playwright install chromiumcp config.example.toml config.tomlEdit config.toml with your shop details:
[defaults]
shop = "my_shop"
[shops.my_shop]
name = "my-etsy-shop-name"The name is your Etsy shop URL name (e.g., if your shop is at etsy.com/shop/CoolMugs, the name is CoolMugs).
- Log into your Etsy shop in Chrome
- Open DevTools (F12) > Application > Cookies >
https://www.etsy.com - Right-click > "Copy all as JSON" (or use the Cookie-Editor extension)
- Import into etsy-browser:
etsy-browser import-cookies my_shop
# Paste the JSON, then Ctrl+Detsy-browser check-sessions
# + my_shop: ACTIVE# Check all sessions
etsy-browser check-sessions
# List configured shops
etsy-browser list-shops
# Test API access
etsy-browser api-test my_shop
# Test with a specific listing
etsy-browser api-test my_shop 1889058316
# Refresh cookies (hit dashboard to keep sessions alive)
etsy-browser refresh-cookiesfrom etsy_browser import load_config, EtsyAPI
# Load config from config.toml
config = load_config()
# Create API client for a shop
api = EtsyAPI(config.get_shop("my_shop"))
# Get all active listings
result = api.get_listings(state="active")
print(f"Found {result['count']} active listings")
# Get a specific listing (127 fields)
listing = api.get_listing("1889058316")
print(listing["listing"]["title"])
# Create a draft listing
result = api.create_listing(
title="Mountain Coffee Mug Gift For Him Nature Ceramic Cup",
description="Beautiful mountain scene on premium ceramic...",
price=14.99,
tags=["coffee mug", "gift for him", "mountain mug"],
taxonomy_id=6048, # Mugs
state="draft",
)
print(f"Created: {result['listing_id']}")
# Update a listing
api.update_listing("1889058316", price=16.99, quantity=50)
# Upload an image
api.upload_image("1889058316", "/path/to/image.jpg", rank=1)
# Batch update
api.batch_update(
["1889058316", "1889058317", "1889058318"],
quantity=99,
)
# Get shop sections
sections = api.get_sections()
# Delete a listing (permanent!)
api.delete_listing("1889058316")
# Deactivate (reversible)
api.deactivate_listing("1889058316")
# Always close when done
api.close()For quick scripts, use the module-level functions:
from etsy_browser import (
load_config, get_listing, get_listings, create_listing,
update_listing, delete_listing, batch_update, list_shops,
)
load_config()
# These auto-select API vs browser
listings = get_listings(state="active")
listing = get_listing(listing_id="1889058316")
update_listing(listing_id="1889058316", price=16.99)from etsy_browser import add_shop, load_config, EtsyAPI, EtsyShopConfig
# Option 1: add_shop() for quick setup
add_shop("my_shop", "my-etsy-shop-name")
# Option 2: environment variables
# ETSY_SHOPS="my_shop=my-etsy-shop-name"
# ETSY_DEFAULT_SHOP="my_shop"
# ETSY_COOKIES_DIR="~/.etsy-browser/cookies"
# Then use normally
config = EtsyShopConfig.from_alias("my_shop")
api = EtsyAPI(config)| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v3/ajax/shop/{shopId}/listings |
List all listings (paginated, 1000/page) |
| GET | /api/v3/ajax/shop/{shopId}/listings/{id} |
Get single listing (127 fields) |
| POST | /api/v3/ajax/shop/{shopId}/listings |
Create listing |
| PATCH | /api/v3/ajax/shop/{shopId}/listings/{id} |
Update listing fields |
| DELETE | /api/v3/ajax/shop/{shopId}/listings/{id} |
Delete listing (204) |
| POST | /api/v3/ajax/shop/{shopId}/listings/images |
Upload image (multipart) |
| GET | /api/v3/ajax/shop/{shopId}/sections |
Get shop sections |
| GET | /api/v3/ajax/shop/{shopId}/listings/{id}/inventory |
Get SKUs/pricing |
| Value | Meaning |
|---|---|
| 0 | Active (is_available=true) |
| 1 | Inactive |
| 3 | Draft |
| 4 | Removed |
| 5 | Expired |
- Cookies: Injected from Chrome DevTools JSON export
- CSRF:
x-csrf-tokenheader from<meta name="csrf_nonce">tag on dashboard (cached 10 min) - Shop ID: Extracted from dashboard page JSON
- Price format: API returns
{"amount": 1499, "divisor": 100}for $14.99. Send"14.99"(string) in create/update. - Tags: Max 13 tags, max 20 characters each.
- Title: Max 140 characters.
- Images: The
imagesfield in listing lists is URL strings. Thelisting_imagesfield has dicts withimage_id. - Pagination: API returns max 1000 listings per page. The library handles pagination automatically.
- Cookie expiry: Cookies expire after ~30 days of inactivity. Use
etsy-browser refresh-cookiesperiodically (or set up a cron job). - Rate limits: Self-imposed: max 1 listing creation per 60s, 10 per hour. Configurable in config.toml.
To prevent cookie expiry, hit the dashboard periodically:
# Manual
etsy-browser refresh-cookies
# Cron job (every 4 hours)
0 */4 * * * /usr/local/bin/etsy-browser refresh-cookies >> ~/logs/etsy-refresh.log 2>&1
# systemd timer (Linux)
# See examples/etsy-cookie-refresh.service and .timer[paths]
cookies_dir = "~/.etsy-browser/cookies"
profiles_dir = "~/.etsy-browser/profiles"
screenshots_dir = "/tmp/etsy_screenshots"
[rate_limits]
max_creates_per_hour = 10
min_create_interval_sec = 60
[defaults]
shop = "my_shop"
[shops.my_shop]
name = "my-etsy-shop-name"
[shops.second_shop]
name = "another-shop-name"| Variable | Description |
|---|---|
ETSY_SHOPS |
Comma-separated alias=name pairs |
ETSY_DEFAULT_SHOP |
Default shop alias |
ETSY_COOKIES_DIR |
Cookie directory path |
ETSY_PROFILES_DIR |
Browser profiles directory path |
ETSY_BROWSER_CONFIG |
Path to config.toml |
Environment variables override config.toml values.
This tool uses Etsy's internal API endpoints which are not officially documented or supported. Use at your own risk. Etsy may change these endpoints at any time. This project is not affiliated with Etsy.
MIT