TL;DR
The Etsy API lets you build tools that interact with Etsy’s marketplace using OAuth 2.0 and REST endpoints for shops, listings, orders, inventory, and webhooks. Etsy enforces limits of 10 requests per second per app, so production integrations need token refresh, request queuing, retries, and monitoring.
Introduction
Etsy processes more than $13 billion in annual gross merchandise sales across 230+ countries. If you are building e-commerce tools, inventory systems, or analytics platforms, an Etsy API integration can automate listing synchronization, order processing, and inventory updates.
This guide shows how to implement:
- OAuth 2.0 authorization and token refresh
- Shop, listing, inventory, and order requests
- Rate-limit handling and retries
- Webhook verification and asynchronous processing
- Production monitoring and deployment checks
💡 Apidog can help test Etsy endpoints, validate OAuth flows, inspect webhook payloads, mock responses, and share API test scenarios with your team.
What Is the Etsy API?
Etsy provides a RESTful API for accessing marketplace data and managing seller operations. Common API capabilities include:
- Shop and profile retrieval
- Listing creation, updates, and inventory management
- Order processing and fulfillment tracking
- Customer and transaction data access
- Shipping profiles and tax calculations
- Image and media upload management
Key features
| Feature | Description |
|---|---|
| RESTful design | Standard HTTP methods with JSON responses |
| OAuth 2.0 | Secure authentication with access-token refresh |
| Webhooks | Real-time notifications for order and listing events |
| Rate limiting | 10 requests per second per app, with burst allowance |
| Sandbox support | Development testing without live data |
API architecture
Etsy uses a versioned REST API:
https://openapi.etsy.com/v3/application/
Version 3 is the current API standard. New integrations should use V3.
API versions
| 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 |
If you maintain a V2 integration, plan a migration to V3. Etsy has announced V2 retirement for late 2026.
Getting Started: Authentication Setup
1. Create an Etsy developer account
- Visit the Etsy Developer Portal.
- Sign in with an Etsy account.
- Open Your Apps in the developer dashboard.
- Select Create a new app.
2. Register your application
Provide the following during registration:
- App name: Displayed to users during authorization.
- App description: Explain what the application does.
- Redirect URI: The HTTPS callback URL Etsy redirects users to after authorization.
- Environment: Start in development mode while testing.
After registration, Etsy provides:
- Key String: Public API identifier.
- Shared Secret: Private API credential.
Store credentials in environment variables, not source code:
# .env
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"
3. Understand the OAuth 2.0 flow
The authorization flow is:
- A user clicks Connect with Etsy.
- Your app redirects the user to Etsy.
- The user signs in and grants requested scopes.
- Etsy redirects to your callback with an authorization code.
- Your server exchanges the code for tokens.
- Your app uses the access token for Etsy API calls.
- Your app refreshes tokens before expiry.
Access tokens expire after approximately one hour, so token refresh should be automatic.
4. Generate an authorization URL
Generate a unique state value for every authorization attempt and persist it in the user session. This protects the callback route against CSRF attacks.
const crypto = require('crypto');
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,
response_type: 'code'
});
return `${baseUrl}?${params.toString()}`;
};
const state = crypto.randomBytes(16).toString('hex');
const authUrl = generateAuthUrl(
process.env.ETSY_KEY_STRING,
'https://your-app.com/callback',
state
);
console.log(`Redirect user to: ${authUrl}`);
Required OAuth scopes
Request only the scopes required by your application.
| Scope | Description | Example use case |
|---|---|---|
listings_r |
Read listings | Display products or sync inventory |
listings_w |
Write listings | Create or update products |
orders_r |
Read orders | Fulfillment workflows |
orders_w |
Write orders | Update status or add tracking |
shops_r |
Read shop information | Shop profile and analytics |
transactions_r |
Read transactions | Financial reporting |
email |
Access buyer email | Order communication |
5. Exchange the authorization code for tokens
Your callback handler should:
- Validate
state. - Exchange the one-time authorization code.
- Store token data against the authorized user.
- Redirect the user back to the application.
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
})
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status}`);
}
const data = await response.json();
return {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_in: data.expires_in,
user_id: data.user_id,
scope: data.scope
};
};
Example Express callback route:
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
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');
}
});
6. Refresh access tokens automatically
Refresh a token before it expires rather than waiting for an authentication failure.
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
})
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
const data = await response.json();
return {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_in: data.expires_in
};
};
const ensureValidToken = async (userId) => {
const user = await db.users.findById(userId);
// Refresh when fewer than five minutes remain.
if (user.etsy_token_expires < Date.now() + 300_000) {
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;
};
Always save the new refresh token returned by a successful refresh request.
7. Create an authenticated request helper
Centralizing API calls gives you one place to set headers, parse errors, and add later retry or logging behavior.
const makeEtsyRequest = async (endpoint, options = {}) => {
const { userId, headers = {}, ...fetchOptions } = options;
const accessToken = await ensureValidToken(userId);
const response = await fetch(
`https://openapi.etsy.com/v3/application${endpoint}`,
{
...fetchOptions,
headers: {
Authorization: `Bearer ${accessToken}`,
'x-api-key': process.env.ETSY_KEY_STRING,
Accept: 'application/json',
'Content-Type': 'application/json',
...headers
}
}
);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
const message = error.message || `HTTP ${response.status}`;
const apiError = new Error(`Etsy API error: ${message}`);
apiError.status = response.status;
apiError.headers = response.headers;
throw apiError;
}
return {
data: await response.json(),
headers: response.headers
};
};
Shop Management Endpoints
Retrieve shop information
Use the shop endpoint to fetch shop details, policies, and settings.
const getShopInfo = async (shopId, userId) => {
const { data } = await makeEtsyRequest(`/shops/${shopId}`, {
method: 'GET',
userId
});
return data;
};
const shop = await getShopInfo(12345678, currentUser.id);
console.log(`Shop: ${shop.title}`);
console.log(`Currency: ${shop.currency_code}`);
console.log(`Listings count: ${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,
"vacation_message": null,
"sale_message": "Thank you for supporting small businesses!",
"digital_sale_message": null,
"created_timestamp": 1609459200,
"updated_timestamp": 1710950400,
"num_listings_active": 127,
"num_listings_sold": 1543,
"gaussian_alphas": {
"overall": 4.8,
"last_30_days": 4.9
},
"vacation_autoreply": null,
"url": "https://www.etsy.com/shop/MyHandmadeShop",
"image_url_760x100": "https://i.etsystatic.com/.../banner_760x100.jpg"
}
Retrieve shop sections
Sections help organize listings in a shop.
const getShopSections = async (shopId, userId) => {
const { data } = await makeEtsyRequest(`/shops/${shopId}/sections`, {
method: 'GET',
userId
});
return data;
};
Example response:
{
"count": 5,
"results": [
{
"shop_section_id": 12345,
"title": "Necklaces",
"rank": 1,
"num_listings": 23
},
{
"shop_section_id": 12346,
"title": "Earrings",
"rank": 2,
"num_listings": 45
}
]
}
Listing Management
Create a listing
Validate listing data in your application before sending it to Etsy. For example, cap tags at 13 and ensure the price uses the expected string format.
const createListing = async (shopId, listingData, userId) => {
const payload = {
title: listingData.title,
description: listingData.description,
price: listingData.price.toString(),
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 { data } = await makeEtsyRequest(`/shops/${shopId}/listings`, {
method: 'POST',
userId,
body: JSON.stringify(payload)
});
return data;
};
Example:
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'
},
currentUser.id
);
Upload listing images
Images are uploaded separately after listing creation.
const fs = require('fs');
const uploadListingImage = async (
listingId,
imagePath,
imagePosition,
userId
) => {
const imageBuffer = fs.readFileSync(imagePath);
const payload = {
image: imageBuffer.toString('base64'),
listing_image_id: null,
position: imagePosition,
is_watermarked: false,
alt_text: 'Handcrafted sterling silver moon phase necklace'
};
const { data } = await makeEtsyRequest(`/listings/${listingId}/images`, {
method: 'POST',
userId,
body: JSON.stringify(payload)
});
return data;
};
Upload multiple images sequentially:
const uploadListingImages = async (listingId, imagePaths, userId) => {
const results = [];
for (let index = 0; index < imagePaths.length; index += 1) {
const image = await uploadListingImage(
listingId,
imagePaths[index],
index + 1,
userId
);
results.push(image);
}
return results;
};
Update listing inventory
Use the inventory endpoint to update quantities for existing listing products.
const updateListingInventory = async (shopId, listingId, inventory, userId) => {
const payload = {
products: inventory.products.map((product) => ({
sku: product.sku,
quantity: product.quantity
})),
is_over_selling: inventory.isOverSelling || false,
on_property: inventory.onProperty || []
};
const { data } = await makeEtsyRequest(
`/shops/${shopId}/listings/${listingId}/inventory`,
{
method: 'PUT',
userId,
body: JSON.stringify(payload)
}
);
return data;
};
Example:
await updateListingInventory(
12345678,
987654321,
{
products: [
{ sku: 'MOON-NECKLACE-001', quantity: 10 },
{ sku: 'MOON-NECKLACE-002', quantity: 5 }
],
isOverSelling: false
},
currentUser.id
);
Retrieve listings
Paginate through listings with limit and offset. Etsy allows up to 100 results per request.
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);
}
const { data } = await makeEtsyRequest(
`/shops/${shopId}/listings?${params.toString()}`,
{
method: 'GET',
userId: options.userId
}
);
return data;
};
const getListing = async (listingId, userId) => {
const { data } = await makeEtsyRequest(`/listings/${listingId}`, {
method: 'GET',
userId
});
return data;
};
Delete a listing
const deleteListing = async (listingId, userId) => {
const { data } = await makeEtsyRequest(`/listings/${listingId}`, {
method: 'DELETE',
userId
});
return data;
};
Order Management
Retrieve orders
Filter orders by status or modification timestamp to avoid repeatedly processing the full history.
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);
}
if (options.minLastModified) {
params.append('min_last_modified', options.minLastModified);
}
const { data } = await makeEtsyRequest(
`/shops/${shopId}/orders?${params.toString()}`,
{
method: 'GET',
userId: options.userId
}
);
return data;
};
const getOrder = async (shopId, orderId, userId) => {
const { data } = await makeEtsyRequest(
`/shops/${shopId}/orders/${orderId}`,
{
method: 'GET',
userId
}
);
return data;
};
Order response structure
{
"order_id": 1234567890,
"user_id": 98765432,
"shop_id": 12345678,
"buyer_user_id": 11223344,
"creation_timestamp": 1710864000,
"last_modified_timestamp": 1710950400,
"completed_timestamp": 1710950400,
"state": "complete",
"user_id_fob": null,
"is_guest": false,
"name": "Jane Doe",
"email": "jane.doe@email.com",
"buyer_phone_number": "+1-555-0123",
"total_price": "89.99",
"total_shipping_cost": "5.95",
"total_tax": "7.65",
"grand_total": "103.59",
"currency_code": "USD",
"payment_method": "credit_card",
"shipping_address": {
"name": "Jane Doe",
"address_line1": "123 Main Street",
"address_line2": "Apt 4B",
"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"
}
]
}
Update order status and tracking
const updateOrderStatus = async (shopId, orderId, trackingData, userId) => {
const payload = {
carrier_id: trackingData.carrierId,
tracking_code: trackingData.trackingCode,
should_send_bcc_to_buyer: trackingData.notifyBuyer ?? true
};
const { data } = await makeEtsyRequest(
`/shops/${shopId}/orders/${orderId}/shipping`,
{
method: 'POST',
userId,
body: JSON.stringify(payload)
}
);
return data;
};
Example:
await updateOrderStatus(
12345678,
1234567890,
{
carrierId: 'usps',
trackingCode: '9400111899223456789012',
notifyBuyer: true
},
currentUser.id
);
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
Etsy rate limits
Etsy enforces limits to protect API stability:
- Standard limit: 10 requests per second per app
- Burst allowance: Up to 50 requests in a single second for short bursts
- Hourly quota: 10,000 calls per hour per app
Requests that exceed limits receive 429 Too Many Requests.
Read rate-limit headers
Etsy includes quota headers in responses:
| Header | Description |
|---|---|
x-etsy-quota-remaining |
Remaining calls in the current hour |
x-etsy-quota-reset |
Unix timestamp when the hourly quota resets |
x-etsy-limit-remaining |
Remaining calls in the current second |
x-etsy-limit-reset |
Unix timestamp when the per-second limit resets |
Log these values with request metadata so you can detect quota issues before users see failures.
Retry with exponential backoff
Use retries only for transient failures such as 429 responses. Do not retry validation failures or invalid authorization requests.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const makeRateLimitedRequest = async (
endpoint,
options = {},
maxRetries = 3
) => {
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
try {
const result = await makeEtsyRequest(endpoint, options);
const remaining = result.headers.get('x-etsy-quota-remaining');
const resetTime = result.headers.get('x-etsy-quota-reset');
if (Number(remaining) < 100) {
console.warn(
`Low Etsy quota remaining: ${remaining}; reset: ${resetTime}`
);
}
return result.data;
} catch (error) {
const isRateLimited = error.status === 429;
if (!isRateLimited || attempt === maxRetries) {
throw error;
}
const delay = 2 ** attempt * 1000;
console.warn(`Rate limited. Retrying in ${delay}ms.`);
await sleep(delay);
}
}
};
Queue requests proactively
A local queue can keep a process below the 10-requests-per-second limit.
class RateLimiter {
constructor(requestsPerSecond = 9) {
this.queue = [];
this.interval = 1000 / requestsPerSecond;
this.processing = false;
}
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 {
resolve(await requestFn());
} catch (error) {
reject(error);
}
if (this.queue.length > 0) {
await new Promise((resolve) => setTimeout(resolve, this.interval));
}
}
this.processing = false;
}
}
const etsyRateLimiter = new RateLimiter(9);
const listings = await etsyRateLimiter.add(() =>
makeEtsyRequest('/shops/12345/listings', {
method: 'GET',
userId: currentUser.id
})
);
For multi-instance deployments, use a shared rate-limiting strategy rather than maintaining independent in-memory queues per server.
Webhook Integration
Configure webhooks
To configure a webhook:
- Open Your Apps in the Etsy developer dashboard.
- Select the application.
- Click Add Webhook.
- Enter an HTTPS endpoint URL.
- Select the events to receive.
Available webhook events
| Event type | Trigger | Use case |
|---|---|---|
v3/shops/{shop_id}/orders/create |
New order placed | Start fulfillment |
v3/shops/{shop_id}/orders/update |
Order status changed | Synchronize order status |
v3/shops/{shop_id}/listings/create |
Listing created | Update external inventory |
v3/shops/{shop_id}/listings/update |
Listing changed | Sync product data |
v3/shops/{shop_id}/listings/delete |
Listing removed | Remove external records |
Implement a webhook endpoint
Webhook handlers should:
- Read the raw request body.
- Verify the signature before parsing the event.
- Return
200 OKwithin five seconds. - Move longer processing into a queue or worker.
- Make handlers idempotent because duplicate delivery is possible.
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;
const isValid = verifyWebhookSignature(
payload,
signature,
process.env.ETSY_WEBHOOK_SECRET
);
if (!isValid) {
console.error('Invalid Etsy webhook signature');
return res.status(401).send('Unauthorized');
}
const event = JSON.parse(payload.toString());
// Enqueue event processing in production instead of doing long-running
// work before responding.
await webhookQueue.add({
eventId: event.id,
type: event.type,
data: event.data
});
return res.status(200).send('OK');
}
);
function verifyWebhookSignature(payload, signature, secret) {
if (!signature) {
return false;
}
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
Process queued events by type:
const processEtsyWebhook = async (event) => {
switch (event.type) {
case 'v3/shops/*/orders/create':
return handleNewOrder(event.data);
case 'v3/shops/*/orders/update':
return handleOrderUpdate(event.data);
case 'v3/shops/*/listings/create':
return handleListingCreated(event.data);
case 'v3/shops/*/listings/update':
return handleListingUpdated(event.data);
case 'v3/shops/*/listings/delete':
return handleListingDeleted(event.data);
default:
console.log('Unhandled Etsy event type:', event.type);
}
};
Webhook best practices
- Verify signatures to prevent spoofed requests.
-
Return
200 OKquickly because Etsy retries non-200 responses within five seconds. - Process asynchronously using a queue or worker.
- Implement idempotency using an event ID or a deterministic event key.
- Log every event with timestamps and processing status.
- Store failures for retry and operational review.
Troubleshooting Common Issues
OAuth token exchange fails
Symptoms: 401 or 403 responses during authentication.
Log the response body during development:
const error = await response.json();
console.error('OAuth error:', error);
Check the following:
- The redirect URI matches exactly, including
https://and trailing slash behavior. -
client_idandclient_secretare correct. - The authorization code has not expired or already been used.
- The app environment is correct. Development apps can only access test accounts.
Rate limit exceeded
Symptoms: 429 Too Many Requests.
Actions:
- Queue requests to stay below 10 requests per second.
- Retry rate-limited calls with exponential backoff.
- Batch or consolidate reads where possible.
- Monitor quota headers and throttle before reaching the limit.
- Avoid polling when a webhook can provide the event.
Listing creation returns validation errors
Symptoms: 400 Bad Request with validation messages.
Common causes:
- Invalid
category_id - Numeric price instead of the required string value
- More than 13 tags
- Missing required fields such as
title,description,price,quantity,who_made, orwhen_made
Validate input before making the API call:
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;
};
const errors = validateListing(listingInput);
if (errors.length > 0) {
throw new Error(errors.join('; '));
}
Webhooks are not arriving
Symptoms: Orders are processed on Etsy but your endpoint receives no events.
Check:
- Webhook delivery logs in the developer dashboard.
- The endpoint responds with
200 OKwithin five seconds. - HTTPS and SSL certificate configuration.
- Firewall and network rules.
- Signature verification logic.
- The event subscription configured for the application.
You can also test endpoint reachability manually:
curl -X POST https://your-app.com/webhooks/etsy \
-H "Content-Type: application/json" \
-d '{"type":"test"}'
Images fail to upload
Symptoms: Listing creation succeeds but image upload requests fail.
Verify:
- The image is JPEG, PNG, or GIF.
- Each image is no larger than 20 MB.
- Base64 encoding is correct.
- The listing exists before uploading images.
- Images are uploaded sequentially rather than in parallel.
Production Deployment Checklist
Before going live, verify the following:
- [ ] Switch the application from development mode to production mode.
- [ ] Update redirect URIs to production URLs.
- [ ] Store tokens in an encrypted database.
- [ ] Refresh access tokens automatically.
- [ ] Implement request queuing and retry logic.
- [ ] Configure webhook endpoints with HTTPS.
- [ ] Verify webhook signatures.
- [ ] Add structured logs for API calls and webhook events.
- [ ] Monitor quota usage and
429responses. - [ ] Create a runbook for token, quota, and webhook failures.
- [ ] Test with multiple shop accounts.
- [ ] Document the OAuth onboarding flow for users.
Monitoring and alerting
Track API calls, quota usage, token refreshes, and webhook outcomes.
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
}
};
const failureRate =
metrics.apiCalls.total === 0
? 0
: metrics.apiCalls.failed / metrics.apiCalls.total;
if (failureRate > 0.05) {
sendAlert('Etsy API failure rate above 5%');
}
if (metrics.quotaUsage.current < 500) {
sendAlert('Etsy API quota below 500 calls remaining');
}
Real-World Use Cases
Multi-channel inventory synchronization
A home decor seller can synchronize inventory across Etsy, Shopify, and Amazon.
Implementation flow:
- An Etsy
orders/createwebhook arrives. - The central inventory service decrements the SKU quantity.
- Background workers update Etsy, Shopify, and Amazon.
- The integration records the request and result in an audit log.
This reduces manual inventory work and helps prevent overselling.
Automated order fulfillment
A print-on-demand workflow can automate order routing:
- Receive an
orders/createwebhook. - Fetch or validate order details.
- Send production data to the fulfillment provider.
- Receive a tracking number.
- Update Etsy shipping information with the tracking number.
- Record the fulfillment status for support and retries.
This pattern can route orders to production without manual data entry.
Analytics dashboard
An OAuth-based analytics tool can aggregate data across multiple Etsy shops.
Useful data includes:
- Shop listing and sales metrics
- Order history and trends
- Listing performance
- Customer review data
Store normalized data in your own reporting database so dashboard queries do not consume Etsy API quota unnecessarily.
Conclusion
A production-ready Etsy integration needs more than API requests. Build the OAuth flow securely, refresh tokens before expiry, queue requests below rate limits, verify webhooks, and monitor failures.
Key takeaways:
- Use Etsy API V3 and OAuth 2.0 for new integrations.
- Store access and refresh tokens securely.
- Refresh tokens automatically before the one-hour access-token expiry.
- Stay below 10 requests per second and monitor hourly quota.
- Use webhooks for near-real-time order and inventory workflows.
- Implement retries, idempotency, logs, and alerts before production deployment.
- Use Apidog to test Etsy requests, OAuth flows, and webhook payloads during development.
FAQ
What is the Etsy API used for?
The Etsy API lets developers build applications that interact with Etsy’s marketplace. Common use cases include multi-channel inventory management, automated fulfillment, analytics dashboards, listing tools, and customer relationship systems.
How do I get an Etsy API key?
Create an account in the Etsy Developer Portal, open Your Apps, and select Create a new app. Etsy provides a Key String and Shared Secret after registration. Store both securely in environment variables or a secrets manager.
Is the Etsy API free to use?
Yes, the Etsy API is free for developers. Rate limits apply: 10 requests per second and 10,000 calls per hour per app. Higher limits require Etsy approval for specific use cases.
What authentication does Etsy API use?
Etsy uses OAuth 2.0. Users authorize the app through Etsy’s authorization page, and your app receives an access token and refresh token. Access tokens expire after one hour.
How do I handle Etsy API rate limits?
Queue requests to remain below 10 requests per second, monitor x-etsy-quota-remaining, and retry 429 responses with exponential backoff. Use webhooks where possible instead of frequent polling.
Can I test Etsy API without a live shop?
Yes. Development mode apps can connect to test shops for integration testing. Use a test Etsy account to authorize the development application without affecting live shop data.
How do webhooks work with Etsy API?
Etsy webhooks send POST requests to your HTTPS endpoint when configured events occur, such as new orders or listing updates. Verify request signatures, respond with 200 OK within five seconds, and process events asynchronously.
What happens when an Etsy OAuth token expires?
Access tokens expire after one hour. Use the refresh token to obtain a new access token before expiry. Implement token refresh in your API middleware so requests do not fail during normal operation.
Can I upload listing images via the API?
Yes. Upload images in a separate API request after creating the listing. Images are base64-encoded and can be JPEG, PNG, or GIF files up to 20 MB each.
How do I migrate from Etsy API V2 to V3?
V3 uses OAuth 2.0 instead of OAuth 1.0a and uses different endpoint paths. Update authentication, migrate endpoint paths from /v2/ to /v3/application/, and test all workflows before V2 retirement in late 2026.
Top comments (0)