Getting Started
Introduction
JeterDev Tools is a managed bridge over the Etsy API v3. Send requests with your API key and we handle Etsy authentication, rate limiting, and errors — you just consume the JSON.
Public endpoints work with your API key alone. Private endpoints (create listings, manage orders, upload images) require connecting your Etsy shop via OAuth from the dashboard.
Base URL
https://jeterdev.tools/api/v1Quick start
curl "https://jeterdev.tools/api/v1/listings/search?query=handmade+art&limit=10" \ -H "x-api-key: jt_YOUR_KEY_HERE"
Getting Started
Authentication
All requests require an API key via the x-api-key header. Generate your key from the Dashboard.
curl "https://jeterdev.tools/api/v1/listings/search?query=art" \ -H "x-api-key: jt_a3f4b5c6d7e8f9..."
Key format
jt_a3f4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6
Check your key
curl "https://jeterdev.tools/api/usage" \ -H "x-api-key: jt_YOUR_KEY"
{"plan":"pro","dailyLimit":50000,"used":142,"remaining":49858,"resetsAt":"2026-05-09T00:00:00.000Z"}Getting Started
Rate Limits
Two independent limits: per-second and daily. Daily limits reset at midnight UTC.
Response headers
X-RateLimit-Limit-Day: 50000 X-RateLimit-Remaining-Day: 49858 X-RateLimit-Reset-Day: 1746835200 X-RateLimit-Limit-Second: 10 X-RateLimit-Remaining-Second:9 X-Plan: pro
When rate limited — 429
{
"error": {
"code": "RATE_LIMIT_DAILY",
"status": 429,
"message": "Daily request limit reached (50000/day).",
"hint": "Limit resets at midnight UTC.",
"docs": "https://jeterdev.tools/docs#rate-limits"
}
}Getting Started
Store Connection
Endpoints that access your own shop data require a connected Etsy store. Each user authenticates independently — your data is completely isolated from other users. Store connection uses OAuth 2.0 with PKCE and must be done through the dashboard.
Access token
1 hour
Auto-refreshed transparently. You never notice this.
Refresh token
90 days
Only when this expires do you need to reconnect. Dashboard shows a red indicator.
✓ API key only
⚡ Requires store connection
How to connect
Log in to JeterDev Tools
Go to jeterdev.tools/dashboard
Click "Connect Etsy Shop"
You will be redirected to Etsy to authorize your account
Review and authorize permissions
Etsy shows the list of permissions being requested
You are connected
Your shop name appears in the dashboard. Private endpoints are now available.
Error if not connected
{
"error": {
"code": "STORE_NOT_CONNECTED",
"status": 403,
"message": "Shop 61004439 is not connected.",
"hint": "Connect the shop at jeterdev.tools/dashboard."
}
}Error if token expired
When Etsy revokes a refresh token (user revoked access, token aged out, etc.), the bridge marks the shop connection_expired: true in Firestore and all subsequent calls for that shop return this error immediately without retrying the upstream.
{
"error": {
"code": "STORE_TOKEN_EXPIRED",
"status": 403,
"message": "Shop 61004439 OAuth token has expired and could not be refreshed. Re-link the shop to restore access.",
"connection_expired": true,
"hint": "Re-connect the shop at jeterdev.tools/dashboard."
}
}How to detect proactively
Poll GET /api/v1/stores and check each store entry for connection_expired: true. When set, prompt your user to re-link before they attempt a publish — rather than letting the publish fail.
const stores = await getStores();
for (const store of stores) {
if (store.connection_expired) {
// surface reconnect CTA to your user
promptReconnect(store.shopId, store.shopName);
}
}OAuth scopes requested
Getting Started
Errors
All errors follow a consistent shape with a machine-readable code, HTTP status, message, and actionable hint.
{
"error": {
"code": "ENDPOINT_NOT_IN_PLAN",
"status": 403,
"message": "Endpoint 'listings/create' is not available on the Starter plan.",
"hint": "Upgrade your plan at jeterdev.tools/pricing.",
"docs": "https://jeterdev.tools/docs#rate-limits"
}
}Listing Builder
Overview
Create Etsy listings with a single atomic API call. Pass your full payload — title, images, variations, category attributes, personalization — and the bridge handles all Etsy API orchestration internally.
Saves payload internally. Zero Etsy calls. Returns instantly.
Creates listing on Etsy as a draft. Uploads images and sets inventory. Returns listing_id.
Same as publish + activates on Etsy. Costs $0.20 USD per listing per shop.
Recommended flow
GET /stores
Confirm connected shops and get their shop_ids
POST /stores/{shopId}/sync
Get shipping profiles, return policies, processing profiles, and sections in one call
GET /categories/{id}/listing-schema
Discover required attributes and variation properties for your category
POST /listings/create
Submit with state="draft" first, then re-submit with state="publish" or "active"
GET /listings/create/{job_id}
Poll for results — available for 24 hours
Listing Builder
Uploads
Upload images, videos, or digital files before a listing exists. Uses a presigned URL flow — the file goes directly to storage, bypassing the 4.5MB function limit. Supports up to 100MB per file. Files cached 24 hours.
3-step flow
POST /uploads/presign
Get upload_id + a signed Firebase Storage URL
PUT file → upload_url
PUT directly to Firebase Storage — no size limit, file never touches Vercel
POST /uploads/confirm
Confirm upload and get your jt-upload:// URL
Step 1 — POST /api/v1/uploads/presign
curl -X POST "https://jeterdev.tools/api/v1/uploads/presign" -H "x-api-key: jt_YOUR_KEY" -H "Content-Type: application/json" -d '{"filename":"product-photo.jpg","content_type":"image/jpeg","size":15728640,"type":"image"}'{
"upload_id": "jt_a1b2c3d4...",
"upload_url": "https://storage.googleapis.com/...?Signature=...",
"method": "PUT",
"content_type": "image/jpeg",
"expires_in": "30min",
"note": "PUT your file binary to upload_url with Content-Type header"
}Step 2 — PUT file directly to upload_url (Firebase Storage)
The file goes directly to Firebase Storage — never through Vercel. No size limit.
# curl — empty response body on success (HTTP 200)
curl -X PUT "https://storage.googleapis.com/...?Signature=..." -H "Content-Type: image/jpeg" --data-binary "@/path/to/photo.jpg"
# fetch (browser / Node.js)
await fetch(upload_url, {
method: "PUT",
headers: { "Content-Type": "image/jpeg" },
body: fileBlob, // File, Buffer, or ReadableStream — up to 5GB
});
// Empty response body = successStep 3 — POST /api/v1/uploads/confirm
curl -X POST "https://jeterdev.tools/api/v1/uploads/confirm" -H "x-api-key: jt_YOUR_KEY" -H "Content-Type: application/json" -d '{"upload_id":"jt_a1b2c3d4..."}'{
"url": "jt-upload://jt_a1b2c3d4...",
"upload_id": "jt_a1b2c3d4...",
"filename": "product-photo.jpg",
"size": 15728640,
"content_type": "image/jpeg",
"type": "image",
"expires_in": "24h"
}Full example — Node.js
const fs = require("fs");
const JT_KEY = "jt_YOUR_KEY";
const BASE = "https://jeterdev.tools/api/v1";
async function uploadImage(filePath, filename) {
const size = fs.statSync(filePath).size;
// 1. Presign
const { upload_id, upload_url } = await fetch(`${BASE}/uploads/presign`, {
method: "POST",
headers: { "x-api-key": JT_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ filename, content_type: "image/jpeg", size }),
}).then(r => r.json());
// 2. PUT directly to Firebase Storage
await fetch(upload_url, {
method: "PUT",
headers: { "Content-Type": "image/jpeg" },
body: fs.readFileSync(filePath),
});
// Empty response = success
// 3. Confirm
const { url } = await fetch(`${BASE}/uploads/confirm`, {
method: "POST",
headers: { "x-api-key": JT_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ upload_id }),
}).then(r => r.json());
return url; // "jt-upload://jt_..."
}
// Use in listings/create
const imageUrl = await uploadImage("./ai-product-15mb.jpg", "product.jpg");
await fetch(`${BASE}/listings/create`, {
method: "POST",
headers: { "x-api-key": JT_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
state: "draft",
shops: [{ shop_id: 61004439, shipping_profile_id: 289094606827, return_policy_id: 1396555302092 }],
listing: { title: "AI Product", description: "...", taxonomy_id: 482, price: 29.99, quantity: 10, who_made: "i_did", when_made: "2020_2026" },
images: [{ url: imageUrl, rank: 1 }],
}),
});Use jt-upload:// in POST /listings/create
{
"state": "publish",
"shops": [...],
"listing": { "title": "AI Generated Mug", ... },
"images": [
{ "url": "jt-upload://jt_a1b2c3d4...", "rank": 1 },
{ "url": "jt-upload://jt_b2c3d4e5...", "rank": 2 }
]
}Supported formats and limits
Listing Builder
Create Listing
POST /api/v1/listings/create
Single atomic call to create a listing across one or more shops.
Request body
{
"state": "draft",
"shops": [{
"shop_id": 61004439,
"shipping_profile_id": 289094606827,
"return_policy_id": 1396555302092,
"processing_profile_id": 1456101932490,
"shop_section_id": 55308357,
"price": 34.99
}],
"listing": {
"title": "Vintage Band Tee", "description": "Premium cotton...",
"listing_type": "physical", "taxonomy_id": 482,
"price": 29.99, "quantity": 100,
"who_made": "i_did", "when_made": "2020_2026",
"tags": ["vintage tee", "band shirt"],
"processing_min": 1, "processing_max": 3
},
"images": [
{ "url": "https://example.com/black-tee.jpg", "rank": 1 },
{ "url": "https://example.com/white-tee.jpg", "rank": 2 }
],
"personalization": {
"enabled": true, "is_required": false,
"instructions": "Enter name (max 15 chars)", "max_chars": 15
},
"category_attributes": {
"200": { "value_ids": [1], "values": ["Black"] },
"325502675244": { "value_ids": [2668], "values": ["Short sleeve"] }
},
"variations": {
"properties": [
{ "property_id": 200, "name": "Color", "values": ["Black","White"] },
{ "property_id": 62809790533, "name": "Size", "scale_id": 17, "values": ["S","M","L"] }
],
"offerings": [
{ "color": "Black", "size": "S", "price": 29.99, "quantity": 20, "sku": "BLK-S" },
{ "color": "Black", "size": "L", "price": 34.99, "quantity": 15, "sku": "BLK-L" },
{ "color": "White", "size": "M", "price": 29.99, "quantity": 25, "sku": "WHT-M" }
],
"variation_images": { "property": "color", "mapping": { "Black": 0, "White": 1 } }
}
}Draft response
{
"listing_pk": 1234567,
"job_id": "jt_aBcDeFgH...",
"status": "draft",
"dashboard_url": "/dashboard#drafts/jt_..."
}Publish/Active response
{
"job_id": "jt_aBcDeFgH...",
"listing_pk": 1234567,
"status": "completed",
"shops_count": 1,
"poll_url": "/api/v1/listings/create/jt_...",
"results": [{
"shop_id": "61004439",
"status": "ok",
"listing_id": 4501295417,
"listing_url":"https://www.etsy.com/listing/4501295417",
"currency_code": "USD", "price": 29.99,
"images": [{"listing_image_id":5523110099001,"url_fullxfull":"...","rank":1}],
"videos": [], "activated": false,
"activation_cost_usd": 0, "warnings": []
}]
}processing_profile_id resolution
shops[0].processing_profile_id must be a real Etsy readiness state ID obtained from GET /stores/{shopId}/processing-profiles/live. The create endpoint resolves it in this order:
Breaking change from previous behavior
The previous implementation silently fell back to a hardcoded ID table when no active listings were found, producing IDs like 3 that are not valid for most shops. This is now a hard error. Always pass a real ID from /processing-profiles/live.
Validation error (400)
{
"error": {
"code": "VALIDATION_FAILED", "status": 400,
"message": "Request validation failed.",
"fields": {
"listing.title": "Title is required",
"shops[0].shipping_profile_id": "Required for physical listings"
}
}
}Shop not connected (403)
{
"error": {
"code": "STORE_NOT_OWNED", "status": 403,
"message": "The following shop IDs are not connected: 61004439",
"hint": "Connect shops at jeterdev.tools/dashboard."
}
}Listing Builder
Job Poll
GET /api/v1/listings/create/{job_id}
Retrieve a listing creation job result. Jobs are retained for 24 hours — useful for retry logic or restoring queue state after a page reload.
curl "https://jeterdev.tools/api/v1/listings/create/jt_aBcDeFgHiJkL..." \ -H "x-api-key: jt_YOUR_KEY"
Response — same shape as create
{ "job_id": "jt_...", "listing_pk": 1234567, "status": "completed", "results": [...] }Job expired or not found
{ "error": { "code": "JOB_NOT_FOUND", "status": 404, "message": "Job 'jt_...' not found. Jobs expire after 24 hours." } }Notifications
Pushover Sale Notifications
Get instant push notifications on your phone every time a sale comes in. Uses Pushover — $5 one-time purchase. Works across all your connected shops automatically.
https://www.jeterdev.tools/api/webhooks/etsy) is already registered. Just connect Pushover and sales flow through automatically for all shops.Setup — 2 steps
Get Pushover credentials
Buy the app at pushover.net ($5 one-time). Copy your User Key. Create an Application and copy the App Token.
Connect to JeterDev Tools
POST /notifications/pushover with your keys. A test notification fires immediately on your phone.
POST /api/v1/notifications/pushover
curl -X POST "https://jeterdev.tools/api/v1/notifications/pushover" \
-H "x-api-key: jt_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_key": "YOUR_PUSHOVER_USER_KEY",
"app_token": "YOUR_PUSHOVER_APP_TOKEN"
}'{ "success": true, "message": "Pushover connected. A test notification was sent to your device." }What the notification looks like
💰 New sale — BambiCraftCo
Order #3456789012
$31.99 USD · 2 items · New York
View order on Etsy →
GET — check status
curl "https://jeterdev.tools/api/v1/notifications/pushover" -H "x-api-key: jt_YOUR_KEY"
{ "configured": true, "app_token": "abcd..." }DELETE — disconnect
curl -X DELETE "https://jeterdev.tools/api/v1/notifications/pushover" -H "x-api-key: jt_YOUR_KEY"
Stores
List Stores
GET /api/v1/stores
Returns all Etsy shops connected to your API key with their shop_id, connection status, and token validity.
curl "https://jeterdev.tools/api/v1/stores" -H "x-api-key: jt_YOUR_KEY"
{
"plan": "Pro",
"remaining": 4892,
"stores": [
{
"shopId": "61004439",
"shopName": "MyCeramicsShop",
"etsyUserId": "288401054",
"connectedAt": "2026-05-09T12:00:00.000Z",
"token_valid": true,
"connection_expired": false
},
{
"shopId": "72005550",
"shopName": "MyVintageShop",
"etsyUserId": "301882914",
"connectedAt": "2026-05-10T08:30:00.000Z",
"token_valid": false,
"connection_expired": true
}
]
}⚠ Detecting expired connections
When connection_expired is true, the OAuth refresh token has failed — auto-refresh will not recover the shop. Show a reconnect CTA to your user. Attempting any authenticated call for that shop returns STORE_TOKEN_EXPIRED (403) until the shop is re-linked.
Stores
Sync
POST /api/v1/stores/{shopId}/sync
One-shot full shop data refresh. Returns shipping profiles, return policies, processing profiles, shop sections, and production partners in a single response.
curl -X POST "https://jeterdev.tools/api/v1/stores/61004439/sync" -H "x-api-key: jt_YOUR_KEY"
{
"shop_id": "61004439", "synced_at": "2026-05-09T14:22:00.000Z",
"shipping_profiles": [{ "shipping_profile_id": 289094606827, "title": "US Standard" }],
"return_policies": [{ "return_policy_id": 1396555302092, "accepts_returns": true }],
"processing_profiles": [
{
"readiness_state_id": 1456101932490,
"readiness_state": "made_to_order",
"min_processing_days": 1,
"max_processing_days": 3,
"processing_days_display_label": "1-3 days",
"shop_id": 61004439
}
],
"shop_sections": [{ "shop_section_id": 55308357, "title": "Mugs & Cups" }],
"production_partners": [] // separate from processing_profiles
}Stores
Live Profiles
Four endpoints that always hit Etsy fresh — never cached. Use them individually when you only need one data type. For all four at once use POST /stores/{shopId}/sync.
All shipping profiles for the shop. Use shipping_profile_id when creating listings.
curl "https://jeterdev.tools/api/v1/stores/61004439/shipping-profiles/live" -H "x-api-key: jt_YOUR_KEY"
Shop return policies. Use return_policy_id when creating listings.
curl "https://jeterdev.tools/api/v1/stores/61004439/return-policies/live" -H "x-api-key: jt_YOUR_KEY"
Processing time profiles configured for the shop. Fetched directly from GET /v3/application/shops/{shop_id}/readiness-state-definitions — the official Etsy endpoint. Results exist independently of active listings.
Key field
Use readiness_state_id as shops[0].processing_profile_id when publishing listings. This is the real Etsy ID — not a derived value.
curl "https://jeterdev.tools/api/v1/stores/61004439/processing-profiles/live" -H "x-api-key: jt_YOUR_KEY"
{
"shop_id": "61004439",
"fetched_at": "2026-05-20T18:00:00.000Z",
"count": 2,
"source": "readiness_state_definitions",
"note": "readiness_state_id is the real Etsy ID. Pass it as shops[0].processing_profile_id when creating listings.",
"processing_profiles": [
{
"readiness_state_id": 1456101932490,
"readiness_state": "made_to_order",
"min_processing_days": 1,
"max_processing_days": 3,
"processing_days_display_label": "1-3 days",
"shop_id": 61004439
},
{
"readiness_state_id": 1456101932491,
"readiness_state": "ready_to_ship",
"min_processing_days": 0,
"max_processing_days": 0,
"processing_days_display_label": "Ready to ship",
"shop_id": 61004439
}
]
}Shop sections (categories). Use shop_section_id when creating listings.
curl "https://jeterdev.tools/api/v1/stores/61004439/shop-sections/live" -H "x-api-key: jt_YOUR_KEY"
Etsy Marketplace
search
Full-text search across Etsy's active marketplace — 35M+ listings.
Etsy Marketplace
listings
Read, create, update, and delete Etsy listings. Write endpoints require Pro plan and store connection.
Etsy Marketplace
shops
Shop info, listings, sections, reviews, orders, and transactions.
Etsy Marketplace
store-mgmt
Manage individual orders, shop sections, and store-level operations.
Etsy Marketplace
images
Listing images, digital files, and videos. Upload/delete require Pro plan and store connection.
Etsy Marketplace
properties
Listing-level and category-level variation properties and attributes.
Etsy Marketplace
shipping
Manage shipping profiles, destinations, and expedited options.
Etsy Marketplace
categories
Etsy seller taxonomy. Cache /categories/list — it rarely changes.
Etsy Marketplace
users
Etsy user profiles and saved addresses.
Etsy Marketplace
policies
Full CRUD for all shop policies — general, privacy, refund, shipping, and payment.
