TL;DR
The Etsy API lets you build applications for Etsy’s marketplace, automating shop, listing, order, and inventory management. It uses OAuth 2.0 authentication, REST endpoints, and enforces a rate limit of 10 calls/sec per app. This guide shows you how to set up authentication, use core endpoints, integrate webhooks, and prepare your integration for production.
Introduction
Etsy handles over $13B in annual gross merchandise sales across 230+ countries. If you’re building e-commerce tools, inventory systems, or analytics dashboards, Etsy API integration is critical.
Manual data entry costs multi-channel sellers 15–20 hours a week. Automating your Etsy integration means syncing listings, processing orders, and updating inventory without manual overhead.
This guide covers end-to-end Etsy API integration: OAuth 2.0 authentication, shop/listing/order management, webhook handling, and error troubleshooting. By the end, you’ll be ready for production with a robust Etsy integration.
💡 Apidog streamlines API integration testing. Use it to test Etsy endpoints, validate OAuth flows, inspect webhook payloads, and debug authentication—all in one workspace. Import API specs, mock responses, and share test scenarios with your team.
What Is the Etsy API?
Etsy provides a RESTful API for marketplace data and seller operations, including:
- Shop/profile info retrieval
- Listing creation, updates, and inventory management
- Order processing and fulfillment tracking
- Customer and transaction data access
- Shipping profiles and tax calculations
- Image/media upload management
Key Features
| Feature | Description |
|---|---|
| RESTful Design | Standard HTTP methods with JSON responses |
| OAuth 2.0 | Secure authentication with token refresh |
| Webhooks | Real-time order/listing event notifications |
| Rate Limiting | 10 requests/sec per app (with burst allowance) |
| Sandbox Support | Dev environment without live data |
API Architecture Overview
Etsy uses versioned REST structure:
https://openapi.etsy.com/v3/application/
Version 3 (v3) is the current standard, with improved OAuth 2.0 support and simpler endpoints.
API Versions Compared
| Version | Status | Authentication | Use Case |
|---|---|---|---|
| V3 | Current | OAuth 2.0 | All new integrations |
| V2 | Deprecated | OAuth 1.0a | Legacy apps only |
| V1 | Retired | N/A | Do not use |
Migrate any V2 integrations to V3 ASAP. V2 will be fully retired by late 2026.
Getting Started: Authentication Setup
Step 1: Create Your Etsy Developer Account
- Go to the Etsy Developer Portal
- Sign in (or create an account)
- Navigate to Your Apps in the dashboard
- Click Create a new app
Step 2: Register Your Application
Fill the app form:
- App Name: Clear, descriptive (users see this during OAuth)
- App Description: What your app does and who uses it
- Redirect URI: HTTPS endpoint Etsy redirects to after auth
- Production/Development: Start with development mode
After submission you get:
- Key String: Public API identifier
- Shared Secret: Private secret (never expose)
Store credentials in environment variables:
# .env file
ETSY_KEY_STRING="your_key_string_here"
ETSY_SHARED_SECRET="your_shared_secret_here"
ETSY_ACCESS_TOKEN="generated_via_oauth"
ETSY_REFRESH_TOKEN="generated_via_oauth"
Step 3: Understand OAuth 2.0 Flow
Etsy uses OAuth 2.0. The flow is:
1. User clicks "Connect with Etsy" in your app
2. Redirect to Etsy authorization URL
3. User logs in and grants permissions
4. Etsy redirects back with authorization code
5. Exchange code for access token
6. Use access token for API calls
7. Refresh token when access token expires (1 hour)
Step 4: Implement OAuth Authorization
Generate the authorization URL in Node.js:
const generateAuthUrl = (clientId, redirectUri, state) => {
const baseUrl = 'https://www.etsy.com/oauth/connect';
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: redirectUri,
scope: 'listings_r listings_w orders_r orders_w shops_r',
state: state, // CSRF protection
response_type: 'code'
});
return `${baseUrl}?${params.toString()}`;
};
// Usage
const authUrl = generateAuthUrl(
process.env.ETSY_KEY_STRING,
'https://your-app.com/callback',
crypto.randomBytes(16).toString('hex')
);
console.log(`Redirect user to: ${authUrl}`);
Required Scopes
Request only what you need:
| Scope | Description | Use Case |
|---|---|---|
listings_r |
Read listings | Display/sync inventory |
listings_w |
Write listings | Create/update products |
orders_r |
Read orders | Order management |
orders_w |
Write orders | Update status |
shops_r |
Read shop info | Shop profile/analytics |
transactions_r |
Read transactions | Financial reporting |
email |
Buyer email | Order comms |
Step 5: Exchange Code for Access Token
Handle the OAuth callback to get tokens:
const exchangeCodeForToken = async (code, redirectUri) => {
const response = await fetch('https://api.etsy.com/v3/public/oauth/token', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.ETSY_KEY_STRING,
client_secret: process.env.ETSY_SHARED_SECRET,
redirect_uri: redirectUri,
code: code
})
});
const data = await response.json();
// Store tokens securely
return {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_in: data.expires_in,
user_id: data.user_id,
scope: data.scope
};
};
// Express callback route
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Verify CSRF state
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state parameter');
}
try {
const tokens = await exchangeCodeForToken(code, 'https://your-app.com/callback');
await db.users.update(req.session.userId, {
etsy_access_token: tokens.access_token,
etsy_refresh_token: tokens.refresh_token,
etsy_token_expires: Date.now() + (tokens.expires_in * 1000),
etsy_user_id: tokens.user_id
});
res.redirect('/dashboard');
} catch (error) {
console.error('Token exchange failed:', error);
res.status(500).send('Authentication failed');
}
});
Step 6: Implement Token Refresh
Tokens expire after 1 hour. Refresh automatically:
const refreshAccessToken = async (refreshToken) => {
const response = await fetch('https://api.etsy.com/v3/public/oauth/token', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.ETSY_KEY_STRING,
client_secret: process.env.ETSY_SHARED_SECRET,
refresh_token: refreshToken
})
});
const data = await response.json();
// Always save the new refresh token!
return {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_in: data.expires_in
};
};
// Middleware to auto-refresh
const ensureValidToken = async (userId) => {
const user = await db.users.findById(userId);
if (user.etsy_token_expires < Date.now() + 300000) {
const newTokens = await refreshAccessToken(user.etsy_refresh_token);
await db.users.update(userId, {
etsy_access_token: newTokens.access_token,
etsy_refresh_token: newTokens.refresh_token,
etsy_token_expires: Date.now() + (newTokens.expires_in * 1000)
});
return newTokens.access_token;
}
return user.etsy_access_token;
};
Step 7: Make Authenticated API Calls
Send your access token with every request:
const makeEtsyRequest = async (endpoint, options = {}) => {
const accessToken = await ensureValidToken(options.userId);
const response = await fetch(`https://openapi.etsy.com/v3/application${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${accessToken}`,
'x-api-key': process.env.ETSY_KEY_STRING,
'Accept': 'application/json',
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Etsy API Error: ${error.message}`);
}
return response.json();
};
Shop Management Endpoints
Retrieving Shop Information
Fetch shop details:
const getShopInfo = async (shopId) => {
const response = await makeEtsyRequest(`/shops/${shopId}`, {
method: 'GET'
});
return response;
};
// Example usage
const shop = await getShopInfo(12345678);
console.log(`Shop: ${shop.title}`);
console.log(`Currency: ${shop.currency_code}`);
console.log(`Listings: ${shop.num_listings_active}`);
Example response:
{
"shop_id": 12345678,
"shop_name": "MyHandmadeShop",
"title": "Handmade Jewelry & Accessories",
"announcement": "Welcome! Free shipping on orders over $50",
"currency_code": "USD",
"is_vacation": false,
"num_listings_active": 127,
"num_listings_sold": 1543,
"url": "https://www.etsy.com/shop/MyHandmadeShop"
}
Retrieving Shop Sections
Organize listings by section:
const getShopSections = async (shopId) => {
const response = await makeEtsyRequest(`/shops/${shopId}/sections`, {
method: 'GET'
});
return response;
};
// Example response:
{
"count": 5,
"results": [
{
"shop_section_id": 12345,
"title": "Necklaces",
"rank": 1,
"num_listings": 23
}
]
}
Listing Management
Creating a New Listing
Create a product listing:
const createListing = async (shopId, listingData) => {
const payload = {
title: listingData.title,
description: listingData.description,
price: listingData.price.toString(), // Must be string
quantity: listingData.quantity,
sku: listingData.sku || [],
tags: listingData.tags.slice(0, 13),
category_id: listingData.categoryId,
shop_section_id: listingData.sectionId,
state: listingData.state || 'active',
who_made: listingData.whoMade,
when_made: listingData.whenMade,
is_supply: listingData.isSupply,
item_weight: listingData.weight || null,
item_weight_unit: listingData.weightUnit || 'g',
item_length: listingData.length || null,
item_width: listingData.width || null,
item_height: listingData.height || null,
item_dimensions_unit: listingData.dimensionsUnit || 'mm',
is_private: listingData.isPrivate || false,
recipient: listingData.recipient || null,
occasion: listingData.occasion || null,
style: listingData.style || []
};
const response = await makeEtsyRequest(`/shops/${shopId}/listings`, {
method: 'POST',
body: JSON.stringify(payload)
});
return response;
};
// Usage
const listing = await createListing(12345678, {
title: 'Sterling Silver Moon Phase Necklace',
description: 'Handcrafted sterling silver necklace featuring moon phases...',
price: 89.99,
quantity: 15,
sku: ['MOON-NECKLACE-001'],
tags: ['moon necklace', 'sterling silver', 'moon phase', 'celestial jewelry'],
categoryId: 10623,
sectionId: 12345,
state: 'active',
whoMade: 'i_did',
whenMade: 'made_to_order',
isSupply: false,
weight: 25,
weightUnit: 'g'
});
Uploading Listing Images
Images are uploaded after listing creation (see Etsy documentation):
const uploadListingImage = async (listingId, imagePath, imagePosition = 1) => {
const fs = require('fs');
const imageBuffer = fs.readFileSync(imagePath);
const base64Image = imageBuffer.toString('base64');
const payload = {
image: base64Image,
listing_image_id: null,
position: imagePosition,
is_watermarked: false,
alt_text: 'Handcrafted sterling silver moon phase necklace'
};
const response = await makeEtsyRequest(`/listings/${listingId}/images`, {
method: 'POST',
body: JSON.stringify(payload)
});
return response;
};
// Upload multiple images
const uploadListingImages = async (listingId, imagePaths) => {
const results = [];
for (let i = 0; i < imagePaths.length; i++) {
const result = await uploadListingImage(listingId, imagePaths[i], i + 1);
results.push(result);
}
return results;
};
Updating Listing Inventory
Update inventory for an existing listing:
const updateListingInventory = async (shopId, listingId, inventory) => {
const payload = {
products: inventory.products.map(product => ({
sku: product.sku,
quantity: product.quantity
})),
is_over_selling: inventory.isOverSelling || false,
on_property: inventory.onProperty || []
};
const response = await makeEtsyRequest(
`/shops/${shopId}/listings/${listingId}/inventory`,
{
method: 'PUT',
body: JSON.stringify(payload)
}
);
return response;
};
// Usage
await updateListingInventory(12345678, 987654321, {
products: [
{ sku: 'MOON-NECKLACE-001', quantity: 10 },
{ sku: 'MOON-NECKLACE-002', quantity: 5 }
],
isOverSelling: false
});
Retrieving Listings
Fetch all listings or filter by status:
const getListings = async (shopId, options = {}) => {
const params = new URLSearchParams({
limit: options.limit || 25,
offset: options.offset || 0
});
if (options.state) {
params.append('state', options.state); // active, inactive, draft, sold_out
}
const response = await makeEtsyRequest(
`/shops/${shopId}/listings?${params.toString()}`,
{ method: 'GET' }
);
return response;
};
// Get single listing
const getListing = async (listingId) => {
const response = await makeEtsyRequest(`/listings/${listingId}`, {
method: 'GET'
});
return response;
};
Deleting a Listing
Remove a listing:
const deleteListing = async (listingId) => {
const response = await makeEtsyRequest(`/listings/${listingId}`, {
method: 'DELETE'
});
return response;
};
Order Management
Retrieving Orders
Fetch orders with filters:
const getOrders = async (shopId, options = {}) => {
const params = new URLSearchParams({
limit: options.limit || 25,
offset: options.offset || 0
});
if (options.status) {
params.append('status', options.status); // open, completed, cancelled
}
if (options.minLastModified) {
params.append('min_last_modified', options.minLastModified);
}
const response = await makeEtsyRequest(
`/shops/${shopId}/orders?${params.toString()}`,
{ method: 'GET' }
);
return response;
};
// Get one order
const getOrder = async (shopId, orderId) => {
const response = await makeEtsyRequest(`/shops/${shopId}/orders/${orderId}`, {
method: 'GET'
});
return response;
};
Order response:
{
"order_id": 1234567890,
"user_id": 98765432,
"shop_id": 12345678,
"state": "complete",
"name": "Jane Doe",
"email": "jane.doe@email.com",
"total_price": "89.99",
"total_shipping_cost": "5.95",
"grand_total": "103.59",
"currency_code": "USD",
"shipping_address": {
"name": "Jane Doe",
"address_line1": "123 Main Street",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "US",
"phone": "+1-555-0123"
},
"listings": [
{
"listing_id": 987654321,
"title": "Sterling Silver Moon Phase Necklace",
"sku": ["MOON-NECKLACE-001"],
"quantity": 1,
"price": "89.99"
}
]
}
Updating Order Status
Mark orders as shipped/add tracking:
const updateOrderStatus = async (shopId, orderId, trackingData) => {
const payload = {
carrier_id: trackingData.carrierId,
tracking_code: trackingData.trackingCode,
should_send_bcc_to_buyer: trackingData.notifyBuyer || true
};
const response = await makeEtsyRequest(
`/shops/${shopId}/orders/${orderId}/shipping`,
{
method: 'POST',
body: JSON.stringify(payload)
}
);
return response;
};
// Usage
await updateOrderStatus(12345678, 1234567890, {
carrierId: 'usps',
trackingCode: '9400111899223456789012',
notifyBuyer: true
});
Common Carrier IDs:
| Carrier | Carrier ID |
|---|---|
| USPS | usps |
| FedEx | fedex |
| UPS | ups |
| DHL | dhl_express |
| Canada Post | canada_post |
| Royal Mail | royal_mail |
| Australia Post | australia_post |
Rate Limiting and Quotas
Understanding Rate Limits
Etsy enforces:
- Standard: 10 requests/sec per app
- Burst: Up to 50 requests in one second (short bursts)
- Quota: 10,000 calls/hour/app
Exceeding limits returns HTTP 429.
Implementing Rate Limit Handling
Use exponential backoff for retries:
const makeRateLimitedRequest = async (endpoint, options = {}, maxRetries = 3) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await makeEtsyRequest(endpoint, options);
// Check rate limit headers
const remaining = response.headers.get('x-etsy-quota-remaining');
const resetTime = response.headers.get('x-etsy-quota-reset');
if (remaining < 100) {
console.warn(`Low quota remaining: ${remaining}, resets at ${resetTime}`);
}
return response;
} catch (error) {
if (error.message.includes('429') && attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000;
console.log(`Rate limited. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
};
Rate limit headers:
| Header | Description |
|---|---|
x-etsy-quota-remaining |
Remaining calls this hour |
x-etsy-quota-reset |
When quota resets (unix time) |
x-etsy-limit-remaining |
Remaining calls this second |
x-etsy-limit-reset |
When per-second resets (unix) |
Always log these headers for monitoring.
Webhook Integration
Configuring Webhooks
- Go to Your Apps in the developer dashboard
- Select your app
- Click Add Webhook
- Enter your HTTPS endpoint
- Select events to subscribe
Available Webhook Events
| Event Type | Trigger | Use Case |
|---|---|---|
v3/shops/{shop_id}/orders/create |
New order placed | Confirmation/fulfillment |
v3/shops/{shop_id}/orders/update |
Order status changed | Status sync |
v3/shops/{shop_id}/listings/create |
New listing created | Inventory sync |
v3/shops/{shop_id}/listings/update |
Listing modified | Product sync |
v3/shops/{shop_id}/listings/delete |
Listing removed | Remove from systems |
Creating Webhook Handler
Example Express handler with signature verification:
const express = require('express');
const crypto = require('crypto');
const app = express();
app.post('/webhooks/etsy', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-etsy-signature'];
const payload = req.body;
// Verify webhook signature
const isValid = verifyWebhookSignature(payload, signature, process.env.ETSY_WEBHOOK_SECRET);
if (!isValid) {
console.error('Invalid webhook signature');
return res.status(401).send('Unauthorized');
}
const event = JSON.parse(payload.toString());
// Route to appropriate handler
switch (event.type) {
case 'v3/shops/*/orders/create':
await handleNewOrder(event.data);
break;
case 'v3/shops/*/orders/update':
await handleOrderUpdate(event.data);
break;
case 'v3/shops/*/listings/create':
await handleListingCreated(event.data);
break;
case 'v3/shops/*/listings/update':
await handleListingUpdated(event.data);
break;
case 'v3/shops/*/listings/delete':
await handleListingDeleted(event.data);
break;
default:
console.log('Unhandled event type:', event.type);
}
// Respond within 5 seconds
res.status(200).send('OK');
});
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
Webhook Best Practices
- Verify signatures — Prevent spoofed webhooks
- Return 200 OK quickly — Etsy retries if not acknowledged in 5s
- Process asynchronously — Queue events for background processing
- Implement idempotency — Handle duplicate deliveries
- Log all events — Timestamped audit trail
Troubleshooting Common Issues
Issue: OAuth Token Exchange Fails
Symptoms: 401/403 during authentication.
Diagnosis:
const error = await response.json();
console.error('OAuth error:', error);
Solutions:
- Redirect URI must match exactly (incl. https://, trailing slash)
- Confirm
client_idandclient_secret - Authorization code not expired (expires after 1 use or 5 mins)
- App in correct mode (prod/dev)
Issue: Rate Limit Exceeded
Symptoms: HTTP 429 responses.
Solutions:
- Implement request queuing + rate limiting
- Use exponential backoff retries
- Batch requests (fetch multiple listings at once)
- Monitor quota headers
Simple rate limiter:
class RateLimiter {
constructor(requestsPerSecond = 9) {
this.queue = [];
this.interval = 1000 / requestsPerSecond;
this.processing = false;
}
async add(requestFn) {
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject });
this.process();
});
}
async process() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
const { requestFn, resolve, reject } = this.queue.shift();
try {
const result = await requestFn();
resolve(result);
} catch (error) {
reject(error);
}
if (this.queue.length > 0) {
await new Promise(r => setTimeout(r, this.interval));
}
}
this.processing = false;
}
}
// Usage
const etsyRateLimiter = new RateLimiter(9);
const result = await etsyRateLimiter.add(() => makeEtsyRequest('/shops/12345/listings'));
Issue: Listing Creation Fails with Validation Errors
Symptoms: 400 Bad Request.
Common Causes:
- Invalid
category_id(use categories API) - Price must be a string
- Max 13 tags per listing
- Required fields missing
Validate before request:
const validateListing = (data) => {
const errors = [];
if (!data.title || data.title.length < 5) {
errors.push('Title must be at least 5 characters');
}
if (typeof data.price !== 'string') {
errors.push('Price must be a string');
}
if (data.tags && data.tags.length > 13) {
errors.push('Maximum 13 tags allowed');
}
if (!['i_did', 'someone_else', 'collective'].includes(data.whoMade)) {
errors.push('Invalid who_made value');
}
return errors;
};
Issue: Webhooks Not Arriving
Symptoms: Webhook endpoint receives nothing.
Diagnosis:
- Check delivery logs in dashboard
- Endpoint returns 200 OK within 5s?
- Test with
curl
Solutions:
- Ensure HTTPS with valid cert
- Whitelist Etsy webhook IPs
- Check signature verification
- Use webhook testing tools
Issue: Images Fail to Upload
Symptoms: Listing created, images error.
Solutions:
- Valid image format (JPEG, PNG, GIF)
- File size <= 20MB
- Correct base64 encoding
- Listing exists before upload
- Upload images sequentially, not in parallel
Production Deployment Checklist
Before go-live, ensure:
- [ ] Switch from development to production app mode
- [ ] Update all redirect URIs to production
- [ ] Secure token storage (encrypted DB)
- [ ] Automatic token refresh logic
- [ ] Set up rate limiting and request queuing
- [ ] Webhook endpoint uses HTTPS
- [ ] Comprehensive error handling
- [ ] API call logging
- [ ] Quota monitoring
- [ ] Runbook for common issues
- [ ] Test with multiple shop accounts
- [ ] Document OAuth flow for onboarding
Monitoring and Alerting
Track these metrics:
const metrics = {
apiCalls: {
total: 0,
successful: 0,
failed: 0,
rateLimited: 0
},
quotaUsage: {
current: 0,
limit: 10000,
resetTime: null
},
oauthTokens: {
active: 0,
expiring_soon: 0,
refresh_failures: 0
},
webhooks: {
received: 0,
processed: 0,
failed: 0
}
};
// Alert on high failure rate
const failureRate = metrics.apiCalls.failed / metrics.apiCalls.total;
if (failureRate > 0.05) {
sendAlert('Etsy API failure rate above 5%');
}
// Alert on low quota
if (metrics.quotaUsage.current < 500) {
sendAlert('Etsy API quota below 500 calls remaining');
}
Real-World Use Cases
Multi-Channel Inventory Sync
Scenario: Home decor seller syncs inventory across Etsy, Shopify, and Amazon.
- Problem: Manual updates led to overselling
- Solution: Central inventory system + Etsy API webhooks
- Result: 0 overselling, 12 hours/week saved
Flow:
- Etsy webhook triggers on order creation
- Central system decrements inventory
- API updates Shopify & Amazon
- Log confirmation
Automated Order Fulfillment
Scenario: Print-on-demand automates processing.
- Problem: 50+ manual orders/day
- Solution: Etsy API + fulfillment provider integration
- Result: Orders auto-routed to production in <5 minutes
Key Points:
- Webhook listens for
orders/create - Send order details to print provider
- Update tracking via Etsy API
- Auto shipping notification to customer
Analytics Dashboard
Scenario: Seller analytics tool for multiple shops.
- Problem: No unified reporting
- Solution: OAuth-based multi-shop aggregation
- Result: Real-time dashboard: sales, traffic, conversions
Data via API:
- Shop stats (listings, sales, revenue)
- Order history/trends
- Listing performance
- Customer reviews
Conclusion
The Etsy API gives you comprehensive access to marketplace features. Key reminders:
- OAuth 2.0 authentication: manage/refresh tokens securely
- Rate limiting (10 req/s, 10K/hr): monitor & queue requests
- Webhooks: real-time order/inventory sync
- Error handling & retries: critical for reliability
- Apidog streamlines API testing and collaboration for Etsy integrations
FAQ Section
What is the Etsy API used for?
The Etsy API lets developers build apps that interact with Etsy’s marketplace. Common uses: inventory management across channels, automated order fulfillment, analytics dashboards, listing tools, and CRM systems.
How do I get an Etsy API key?
Register at the Etsy Developer Portal, go to Your Apps, and click Create a new app. After registration, you get a Key String (public ID) and Shared Secret (private key). Store both in environment variables.
Is the Etsy API free to use?
Yes, the Etsy API is free. Rate limits apply: 10 requests/second and 10,000 calls/hour per app. Higher limits require Etsy approval.
What authentication does Etsy API use?
OAuth 2.0. Users authorize your app via the Etsy auth page; you get an access token (1 hour) and a refresh token (for new tokens).
How do I handle Etsy API rate limits?
Queue requests to stay under 10/sec. Track x-etsy-quota-remaining for hourly usage. Use exponential backoff on HTTP 429 responses.
Can I test Etsy API without a live shop?
Yes—dev mode apps connect to test shops. Create a test Etsy account and use it for OAuth with your dev app.
How do webhooks work with Etsy API?
Etsy webhooks send POSTs to your HTTPS endpoint on events (orders, listings). Set up in the app dashboard, verify signatures, and respond with 200 OK within 5s.
What happens when Etsy OAuth token expires?
Access tokens expire after 1 hour. Use the refresh token to get a new access token before expiry. Automate token refresh in your middleware.
Can I upload listing images via the API?
Yes—upload images as base64-encoded strings in a separate API call after creating the listing. Max 20MB, JPEG/PNG/GIF only.
How do I migrate from Etsy API V2 to V3?
V3 uses OAuth 2.0 (not OAuth 1.0a) and new endpoints. Update your auth flow, change endpoint paths from /v2/ to /v3/application/, and test before V2 retirement (late 2026).
Top comments (0)