A properly configured development environment eliminates friction and reduces errors. This chapter guides you through setting up everything you need to build Etsy API applications in Python.
Before accessing the API, you need to register as an Etsy developer:
Navigate to https://www.etsy.com/developers and sign in with your Etsy account.
For OAuth 2.0 authentication, you need to specify:
http://localhost:3000/callbackAfter creation, you’ll receive:
Important: Never commit these credentials to version control. We’ll configure secure storage shortly.
Ensure you have Python 3.8 or later:
# Check your Python version
python3 --version
# On Ubuntu/Debian
sudo apt update
sudo apt install python3 python3-pip python3-venv
# On macOS with Homebrew
brew install python3
# On Windows, download from python.org
Always use virtual environments to isolate project dependencies:
# Create project directory
mkdir etsy-api-project
cd etsy-api-project
# Create virtual environment
python3 -m venv venv
# Activate the environment
# On Linux/macOS:
source venv/bin/activate
# On Windows:
.\venv\Scripts\activate
# Verify activation (should show venv path)
which python
Create a requirements.txt file with essential dependencies:
# HTTP requests
requests>=2.28.0
httpx>=0.24.0
# Environment and configuration
python-dotenv>=1.0.0
# Data handling
pandas>=2.0.0
# Authentication
authlib>=1.2.0
# Async support
aiohttp>=3.8.0
# Development tools
pytest>=7.0.0
black>=23.0.0
flake8>=6.0.0
# Optional: for building dashboards
plotly>=5.0.0
dash>=2.0.0
Install the dependencies:
pip install -r requirements.txt
Never hardcode API credentials. Use environment variables:
# .env file (add to .gitignore!)
ETSY_API_KEY=your_api_key_here
ETSY_SHARED_SECRET=your_shared_secret_here
ETSY_SHOP_ID=your_shop_id
ETSY_ACCESS_TOKEN=your_access_token
ETSY_REFRESH_TOKEN=your_refresh_token
# .gitignore
.env
*.pyc
__pycache__/
venv/
.vscode/
*.log
tokens.json
# config.py
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
class Config:
"""Application configuration from environment variables."""
ETSY_API_KEY = os.getenv('ETSY_API_KEY')
ETSY_SHARED_SECRET = os.getenv('ETSY_SHARED_SECRET')
ETSY_SHOP_ID = os.getenv('ETSY_SHOP_ID')
ETSY_ACCESS_TOKEN = os.getenv('ETSY_ACCESS_TOKEN')
ETSY_REFRESH_TOKEN = os.getenv('ETSY_REFRESH_TOKEN')
# API settings
BASE_URL = "https://openapi.etsy.com/v3"
OAUTH_URL = "https://www.etsy.com/oauth"
@classmethod
def validate(cls):
"""Ensure required configuration is present."""
required = ['ETSY_API_KEY']
missing = [key for key in required if not getattr(cls, key)]
if missing:
raise ValueError(f"Missing required configuration: {', '.join(missing)}")
return True
# Validate on import
if __name__ == "__main__":
Config.validate()
print("Configuration loaded successfully!")
Organize your project for maintainability:
etsy-api-project/
├── .env # Environment variables (not in git)
├── .gitignore
├── requirements.txt
├── config.py # Configuration management
├── etsy_client/
│ ├── __init__.py
│ ├── client.py # Main API client
│ ├── auth.py # Authentication handling
│ ├── listings.py # Listing operations
│ ├── orders.py # Order operations
│ ├── files.py # Digital file operations
│ └── exceptions.py # Custom exceptions
├── scripts/
│ ├── authorize.py # OAuth authorization script
│ ├── sync_listings.py # Listing sync utilities
│ └── export_analytics.py # Analytics export
├── tests/
│ ├── __init__.py
│ ├── test_client.py
│ └── test_listings.py
└── data/
└── .gitkeep # For storing exported data
# Create directories
mkdir -p etsy_client scripts tests data
# Create __init__.py files
touch etsy_client/__init__.py tests/__init__.py
# Create placeholder for data directory
touch data/.gitkeep
Let’s create the foundation for all API interactions:
# etsy_client/exceptions.py
"""Custom exceptions for Etsy API client."""
class EtsyAPIError(Exception):
"""Base exception for Etsy API errors."""
def __init__(self, message, status_code=None, response=None):
self.message = message
self.status_code = status_code
self.response = response
super().__init__(self.message)
class AuthenticationError(EtsyAPIError):
"""Raised when authentication fails."""
pass
class RateLimitError(EtsyAPIError):
"""Raised when rate limit is exceeded."""
def __init__(self, message, retry_after=60, **kwargs):
self.retry_after = retry_after
super().__init__(message, **kwargs)
class NotFoundError(EtsyAPIError):
"""Raised when a resource is not found."""
pass
class ValidationError(EtsyAPIError):
"""Raised when request validation fails."""
pass
# etsy_client/client.py
"""Core Etsy API client implementation."""
import time
import logging
from typing import Optional, Dict, Any
import requests
from .exceptions import (
EtsyAPIError,
AuthenticationError,
RateLimitError,
NotFoundError,
ValidationError
)
logger = logging.getLogger(__name__)
class EtsyClient:
"""
Main client for interacting with the Etsy API v3.
Usage:
client = EtsyClient(api_key="your_key", access_token="your_token")
shop = client.get_shop(shop_id)
"""
BASE_URL = "https://openapi.etsy.com/v3"
def __init__(
self,
api_key: str,
access_token: Optional[str] = None,
shop_id: Optional[str] = None,
timeout: int = 30,
max_retries: int = 3,
dry_run: bool = False
):
"""
Initialize the Etsy API client.
Args:
api_key: Your Etsy API key (keystring)
access_token: OAuth access token for authenticated requests
shop_id: Default shop ID for operations
timeout: Request timeout in seconds
max_retries: Maximum retry attempts for failed requests
dry_run: If True, log requests without executing writes
"""
self.api_key = api_key
self.access_token = access_token
self.shop_id = shop_id
self.timeout = timeout
self.max_retries = max_retries
self.dry_run = dry_run
self._session = requests.Session()
self._setup_session()
def _setup_session(self):
"""Configure the requests session with default headers."""
self._session.headers.update({
"x-api-key": self.api_key,
"Content-Type": "application/json",
"Accept": "application/json"
})
if self.access_token:
self._session.headers["Authorization"] = f"Bearer {self.access_token}"
def set_access_token(self, access_token: str):
"""Update the access token for authenticated requests."""
self.access_token = access_token
self._session.headers["Authorization"] = f"Bearer {access_token}"
def _build_url(self, endpoint: str) -> str:
"""Build the full URL for an API endpoint."""
return f"{self.BASE_URL}{endpoint}"
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
"""
Handle API response and raise appropriate exceptions.
Args:
response: The requests Response object
Returns:
Parsed JSON response data
Raises:
EtsyAPIError: For various API errors
"""
# Log rate limit info
remaining = response.headers.get('X-RateLimit-Remaining')
if remaining:
logger.debug(f"Rate limit remaining: {remaining}")
# Handle different status codes
if response.status_code == 200:
return response.json() if response.content else {}
elif response.status_code == 201:
return response.json() if response.content else {"created": True}
elif response.status_code == 204:
return {"deleted": True}
elif response.status_code == 401:
raise AuthenticationError(
"Authentication failed. Check your API key and access token.",
status_code=401,
response=response
)
elif response.status_code == 403:
raise AuthenticationError(
"Access forbidden. Check your OAuth scopes.",
status_code=403,
response=response
)
elif response.status_code == 404:
raise NotFoundError(
f"Resource not found: {response.url}",
status_code=404,
response=response
)
elif response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
raise RateLimitError(
f"Rate limit exceeded. Retry after {retry_after} seconds.",
retry_after=retry_after,
status_code=429,
response=response
)
elif response.status_code == 400:
error_data = response.json() if response.content else {}
raise ValidationError(
f"Validation error: {error_data.get('error', 'Unknown error')}",
status_code=400,
response=response
)
else:
error_data = response.json() if response.content else {}
raise EtsyAPIError(
f"API error: {error_data.get('error', 'Unknown error')}",
status_code=response.status_code,
response=response
)
def _request(
self,
method: str,
endpoint: str,
params: Optional[Dict] = None,
json: Optional[Dict] = None,
files: Optional[Dict] = None,
**kwargs
) -> Dict[str, Any]:
"""
Make an API request with retry logic.
Args:
method: HTTP method (GET, POST, PUT, DELETE)
endpoint: API endpoint path
params: Query parameters
json: JSON body data
files: Files to upload
**kwargs: Additional requests arguments
Returns:
Parsed JSON response
"""
url = self._build_url(endpoint)
# Dry run for write operations
if self.dry_run and method in ('POST', 'PUT', 'PATCH', 'DELETE'):
logger.info(f"[DRY RUN] {method} {url}")
logger.info(f"[DRY RUN] Params: {params}")
logger.info(f"[DRY RUN] Body: {json}")
return {"dry_run": True, "method": method, "url": url}
last_exception = None
for attempt in range(self.max_retries):
try:
logger.debug(f"{method} {url}")
# Handle file uploads differently
headers = None
if files:
headers = {k: v for k, v in self._session.headers.items()
if k != 'Content-Type'}
response = self._session.request(
method=method,
url=url,
params=params,
json=json,
files=files,
headers=headers,
timeout=self.timeout,
**kwargs
)
return self._handle_response(response)
except RateLimitError as e:
logger.warning(f"Rate limited, waiting {e.retry_after}s (attempt {attempt + 1})")
time.sleep(e.retry_after)
last_exception = e
except requests.exceptions.Timeout:
logger.warning(f"Request timeout (attempt {attempt + 1})")
time.sleep(2 ** attempt) # Exponential backoff
last_exception = EtsyAPIError("Request timeout")
except requests.exceptions.ConnectionError:
logger.warning(f"Connection error (attempt {attempt + 1})")
time.sleep(2 ** attempt)
last_exception = EtsyAPIError("Connection error")
raise last_exception or EtsyAPIError("Max retries exceeded")
# Convenience methods
def get(self, endpoint: str, **kwargs) -> Dict[str, Any]:
"""Make a GET request."""
return self._request('GET', endpoint, **kwargs)
def post(self, endpoint: str, **kwargs) -> Dict[str, Any]:
"""Make a POST request."""
return self._request('POST', endpoint, **kwargs)
def put(self, endpoint: str, **kwargs) -> Dict[str, Any]:
"""Make a PUT request."""
return self._request('PUT', endpoint, **kwargs)
def patch(self, endpoint: str, **kwargs) -> Dict[str, Any]:
"""Make a PATCH request."""
return self._request('PATCH', endpoint, **kwargs)
def delete(self, endpoint: str, **kwargs) -> Dict[str, Any]:
"""Make a DELETE request."""
return self._request('DELETE', endpoint, **kwargs)
# Basic resource methods
def get_me(self) -> Dict[str, Any]:
"""Get the currently authenticated user."""
return self.get("/application/users/me")
def get_shop(self, shop_id: Optional[str] = None) -> Dict[str, Any]:
"""Get shop information."""
shop_id = shop_id or self.shop_id
if not shop_id:
raise ValueError("shop_id is required")
return self.get(f"/application/shops/{shop_id}")
def ping(self) -> bool:
"""Test API connectivity."""
try:
self.get("/application/openapi-ping")
return True
except EtsyAPIError:
return False
# etsy_client/__init__.py
"""Etsy API Client package."""
from .client import EtsyClient
from .exceptions import (
EtsyAPIError,
AuthenticationError,
RateLimitError,
NotFoundError,
ValidationError
)
__all__ = [
'EtsyClient',
'EtsyAPIError',
'AuthenticationError',
'RateLimitError',
'NotFoundError',
'ValidationError'
]
__version__ = '1.0.0'
Create a simple test script:
# scripts/test_setup.py
"""Test script to verify the development environment setup."""
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from config import Config
from etsy_client import EtsyClient
def test_configuration():
"""Test that configuration loads correctly."""
print("Testing configuration...")
try:
Config.validate()
print("✓ Configuration loaded successfully")
print(f" API Key: {Config.ETSY_API_KEY[:8]}...")
return True
except ValueError as e:
print(f"✗ Configuration error: {e}")
return False
def test_api_connection():
"""Test basic API connectivity."""
print("\nTesting API connection...")
client = EtsyClient(
api_key=Config.ETSY_API_KEY,
access_token=Config.ETSY_ACCESS_TOKEN
)
if client.ping():
print("✓ API connection successful")
return True
else:
print("✗ API connection failed")
return False
def test_authenticated_request():
"""Test authenticated API access."""
print("\nTesting authenticated access...")
if not Config.ETSY_ACCESS_TOKEN:
print("⚠ No access token configured, skipping")
return True
client = EtsyClient(
api_key=Config.ETSY_API_KEY,
access_token=Config.ETSY_ACCESS_TOKEN
)
try:
user = client.get_me()
print(f"✓ Authenticated as user: {user.get('user_id')}")
return True
except Exception as e:
print(f"✗ Authentication failed: {e}")
return False
def main():
"""Run all setup tests."""
print("=" * 50)
print("Etsy API Development Environment Test")
print("=" * 50)
results = [
test_configuration(),
test_api_connection(),
test_authenticated_request()
]
print("\n" + "=" * 50)
if all(results):
print("All tests passed! Environment is ready.")
else:
print("Some tests failed. Please check the errors above.")
print("=" * 50)
if __name__ == "__main__":
main()
Run the test:
python scripts/test_setup.py
Create .vscode/settings.json:
{
"python.defaultInterpreterPath": "${workspaceFolder}/venv/bin/python",
"python.linting.enabled": true,
"python.linting.flake8Enabled": true,
"python.formatting.provider": "black",
"editor.formatOnSave": true,
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": ["tests"],
"python.envFile": "${workspaceFolder}/.env"
}
Create .vscode/launch.json for debugging:
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env"
},
{
"name": "Python: Test Setup",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/scripts/test_setup.py",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env"
}
]
}
Set up proper logging for debugging:
# logging_config.py
import logging
import sys
from pathlib import Path
def setup_logging(
level: str = "INFO",
log_file: str = None,
format_string: str = None
):
"""
Configure logging for the application.
Args:
level: Logging level (DEBUG, INFO, WARNING, ERROR)
log_file: Optional file path for log output
format_string: Custom log format
"""
if format_string is None:
format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# Create handlers
handlers = [logging.StreamHandler(sys.stdout)]
if log_file:
log_path = Path(log_file)
log_path.parent.mkdir(parents=True, exist_ok=True)
handlers.append(logging.FileHandler(log_file))
# Configure root logger
logging.basicConfig(
level=getattr(logging, level.upper()),
format=format_string,
handlers=handlers
)
# Reduce noise from third-party libraries
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
# Usage in scripts:
# from logging_config import setup_logging
# setup_logging(level="DEBUG", log_file="logs/etsy_api.log")
Before proceeding to authentication, verify:
With your development environment configured, you’re ready to implement OAuth authentication. The next chapter walks through the complete OAuth 2.0 flow with PKCE, generating access tokens that unlock the full power of the Etsy API.
| ← Chapter 1: Understanding the Etsy API Ecosystem | Table of Contents | Chapter 3: Authentication and Authorization → |