This workflow corresponds to n8n.io template #16449 — we link there as the canonical source.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Audit a competitor shop on Etsy",
"nodes": [
{
"id": "shop-manual",
"name": "Run audit manually",
"type": "n8n-nodes-base.manualTrigger",
"position": [
0,
320
],
"parameters": {},
"typeVersion": 1
},
{
"id": "shop-schedule",
"name": "Audit every day",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
0,
520
],
"parameters": {
"rule": {
"interval": [
{
"field": "days",
"daysInterval": 1
}
]
}
},
"typeVersion": 1.2
},
{
"id": "shop-config",
"name": "Configuration (EDIT ME)",
"type": "n8n-nodes-base.set",
"position": [
280,
420
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "shop-config-1",
"name": "shop_name",
"type": "string",
"value": "ENTER_ETSY_SHOP_NAME"
},
{
"id": "shop-config-2",
"name": "listing_limit",
"type": "number",
"value": 100
}
]
},
"includeOtherFields": false
},
"typeVersion": 3.4
},
{
"id": "shop-lookup-build",
"name": "Build Etsy shop lookup",
"type": "n8n-nodes-base.code",
"position": [
560,
420
],
"parameters": {
"jsCode": "// Validates the shop name and builds the public shop lookup URL.\nconst config = $input.first().json;\nconst shopName = String(config.shop_name || '').trim();\nif (!shopName || shopName === 'ENTER_ETSY_SHOP_NAME') {\n throw new Error('Enter an Etsy shop name in Configuration (EDIT ME).');\n}\nconst listingLimit = Math.min(100, Math.max(1, Math.round(Number(config.listing_limit || 100))));\nreturn [{ json: {\n shop_name: shopName,\n listing_limit: listingLimit,\n lookup_url: `https://api.etsy.com/v3/application/shops?shop_name=${encodeURIComponent(shopName)}`\n} }];"
},
"typeVersion": 2
},
{
"id": "shop-fetch",
"name": "Find public Etsy shop",
"type": "n8n-nodes-base.httpRequest",
"position": [
840,
420
],
"parameters": {
"url": "={{ $json.lookup_url }}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"typeVersion": 4.3
},
{
"id": "shop-listings-build",
"name": "Build active listings request",
"type": "n8n-nodes-base.code",
"position": [
1120,
420
],
"parameters": {
"jsCode": "// Resolves the lookup result to a numeric shop ID and builds the listings URL.\nconst response = $input.first().json;\nconst config = $('Build Etsy shop lookup').first().json;\nconst shop = Array.isArray(response.results) ? response.results[0] : null;\nif (!shop?.shop_id) {\n throw new Error(`Etsy shop not found: ${config.shop_name}`);\n}\nreturn [{ json: {\n shop,\n shop_id: String(shop.shop_id),\n listing_limit: config.listing_limit,\n listings_url: `https://api.etsy.com/v3/application/shops/${shop.shop_id}/listings/active?limit=${config.listing_limit}&sort_on=created&sort_order=down&includes=Images`\n} }];"
},
"typeVersion": 2
},
{
"id": "shop-listings-fetch",
"name": "Fetch active shop listings",
"type": "n8n-nodes-base.httpRequest",
"position": [
1400,
420
],
"parameters": {
"url": "={{ $json.listings_url }}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"typeVersion": 4.3
},
{
"id": "shop-analyze",
"name": "Analyze shop and compare catalog",
"type": "n8n-nodes-base.code",
"position": [
1680,
420
],
"parameters": {
"jsCode": "// Creates a shop audit and compares the active catalog with the previous production run.\nconst response = $input.first().json;\nconst request = $('Build active listings request').first().json;\nconst shop = request.shop;\nconst listings = Array.isArray(response.results) ? response.results : [];\nconst staticData = $getWorkflowStaticData('global');\nconst previous = staticData.etsyShopAuditSnapshot || {};\nconst hasPreviousSnapshot = Object.keys(previous).length > 0;\nconst current = {};\nconst prices = [];\nconst taxonomyCounts = {};\nlet personalizedCount = 0;\n\nconst moneyValue = (money) => {\n const amount = Number(money?.amount);\n const divisor = Number(money?.divisor || 100);\n return Number.isFinite(amount) && divisor > 0 ? amount / divisor : null;\n};\n\nfor (const listing of listings) {\n const id = String(listing.listing_id);\n const price = moneyValue(listing.price);\n if (price !== null) prices.push(price);\n if (listing.is_personalizable) personalizedCount += 1;\n const taxonomy = String(listing.taxonomy_id || '');\n if (taxonomy) taxonomyCounts[taxonomy] = (taxonomyCounts[taxonomy] || 0) + 1;\n current[id] = {\n listing_id: id,\n title: listing.title || '',\n price,\n currency: listing?.price?.currency_code || '',\n quantity: listing.quantity ?? null,\n is_personalizable: Boolean(listing.is_personalizable),\n taxonomy_id: listing.taxonomy_id || null,\n created_timestamp: listing.created_timestamp || null,\n updated_timestamp: listing.updated_timestamp || null,\n url: listing.url || `https://www.etsy.com/listing/${id}`,\n image_url: listing.images?.[0]?.url_570xN || ''\n };\n}\n\nconst newListings = hasPreviousSnapshot\n ? Object.keys(current).filter((id) => !previous[id]).map((id) => current[id])\n : [];\nconst removedListings = hasPreviousSnapshot\n ? Object.keys(previous).filter((id) => !current[id]).map((id) => previous[id])\n : [];\nstaticData.etsyShopAuditSnapshot = current;\n\nprices.sort((a, b) => a - b);\nconst average = prices.length ? prices.reduce((sum, value) => sum + value, 0) / prices.length : null;\nconst median = prices.length === 0 ? null : prices.length % 2\n ? prices[(prices.length - 1) / 2]\n : (prices[prices.length / 2 - 1] + prices[prices.length / 2]) / 2;\nconst topTaxonomies = Object.entries(taxonomyCounts)\n .sort((a, b) => b[1] - a[1]).slice(0, 10)\n .map(([taxonomy_id, occurrences]) => ({ taxonomy_id, occurrences }));\n\nreturn [{ json: {\n status: hasPreviousSnapshot ? 'comparison_complete' : 'baseline_created',\n checked_at: new Date().toISOString(),\n shop: {\n shop_id: String(shop.shop_id), shop_name: shop.shop_name, title: shop.title || '',\n url: shop.url || '', currency_code: shop.currency_code || '', is_vacation: Boolean(shop.is_vacation),\n listing_active_count_reported_by_etsy: Number(shop.listing_active_count || response.count || 0),\n num_favorers: Number(shop.num_favorers || 0), accepts_custom_requests: Boolean(shop.accepts_custom_requests)\n },\n analyzed_count: listings.length,\n sample_note: `Catalog metrics and change detection use the newest ${listings.length} active listings returned by Etsy. Increase the limit up to 100 if needed.`,\n price_statistics: {\n minimum: prices.length ? Math.min(...prices) : null,\n maximum: prices.length ? Math.max(...prices) : null,\n average: average === null ? null : Math.round(average * 100) / 100,\n median: median === null ? null : Math.round(median * 100) / 100\n },\n personalized_share_percent: listings.length ? Math.round((personalizedCount / listings.length) * 10000) / 100 : 0,\n top_taxonomy_ids: topTaxonomies,\n new_listings: newListings,\n removed_listings: removedListings,\n total_catalog_changes: newListings.length + removedListings.length,\n listings: Object.values(current)\n} }];"
},
"typeVersion": 2
},
{
"id": "shop-if",
"name": "Catalog changes found?",
"type": "n8n-nodes-base.if",
"position": [
1960,
420
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "catalog-changes",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.total_catalog_changes }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.2
},
{
"id": "shop-alert",
"name": "Catalog changes detected",
"type": "n8n-nodes-base.set",
"position": [
2240,
320
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "shop-alert-1",
"name": "result_message",
"type": "string",
"value": "=Detected {{ $json.new_listings.length }} new and {{ $json.removed_listings.length }} removed listing(s) in {{ $json.shop.shop_name }}."
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "shop-report",
"name": "Shop audit report",
"type": "n8n-nodes-base.set",
"position": [
2240,
520
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "shop-report-1",
"name": "result_message",
"type": "string",
"value": "={{ $json.status === 'baseline_created' ? 'Shop baseline saved. Future production runs will compare the catalog.' : 'Shop audit complete. No catalog changes were detected in the monitored sample.' }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "shop-overview",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-40,
-440
],
"parameters": {
"color": 1,
"width": 680,
"height": 660,
"content": "## Audit an Etsy Competitor Shop\n\nCreate a repeatable audit of a public Etsy shop and detect catalog changes without OAuth, scraping, or a database. The workflow resolves a shop name to its numeric ID, fetches its newest active listings, summarizes the catalog, and stores a snapshot in n8n.\n\n### How it works\n- A manual or daily schedule trigger starts the audit.\n- **Configuration (EDIT ME)** accepts the public Etsy shop name and a listing sample size.\n- The workflow retrieves the public shop profile and up to 100 newest active listings.\n- It calculates observed price statistics, personalization usage, and dominant taxonomy IDs.\n- The first production run creates a baseline. Later runs report newly added and removed listings within the monitored sample.\n\n### Setup\n1. Create an Etsy developer app and copy its keystring and shared secret.\n2. Create a Header Auth credential in both Etsy HTTP nodes. Set the header name to `x-api-key` and its value to `KEYSTRING:SHARED_SECRET`.\n3. Enter the exact shop name in **Configuration (EDIT ME)**.\n4. Activate the workflow to run daily and retain snapshots between production executions.\n\n### Customization tips\nChange the schedule, increase the sample to 100, or connect **Catalog changes detected** to Slack, email, Telegram, or Google Sheets. Snapshot comparisons cover the returned sample, not listings beyond the configured limit.\n"
},
"typeVersion": 1
},
{
"id": "shop-section-1",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-60,
240
],
"parameters": {
"color": 7,
"width": 1620,
"height": 440,
"content": "## 1. Resolve and fetch the shop\nEnter an exact Etsy shop name, resolve its public profile, and retrieve up to 100 newest active listings.\n"
},
"typeVersion": 1
},
{
"id": "shop-section-2",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1640,
240
],
"parameters": {
"color": 7,
"width": 920,
"height": 440,
"content": "## 2. Audit and compare the catalog\nSummarize observed prices, personalization, and taxonomies, then detect new or removed listings against the previous run.\n"
},
"typeVersion": 1
}
],
"settings": {
"executionOrder": "v1"
},
"connections": {
"Audit every day": {
"main": [
[
{
"node": "Configuration (EDIT ME)",
"type": "main",
"index": 0
}
]
]
},
"Run audit manually": {
"main": [
[
{
"node": "Configuration (EDIT ME)",
"type": "main",
"index": 0
}
]
]
},
"Find public Etsy shop": {
"main": [
[
{
"node": "Build active listings request",
"type": "main",
"index": 0
}
]
]
},
"Build Etsy shop lookup": {
"main": [
[
{
"node": "Find public Etsy shop",
"type": "main",
"index": 0
}
]
]
},
"Catalog changes found?": {
"main": [
[
{
"node": "Catalog changes detected",
"type": "main",
"index": 0
}
],
[
{
"node": "Shop audit report",
"type": "main",
"index": 0
}
]
]
},
"Configuration (EDIT ME)": {
"main": [
[
{
"node": "Build Etsy shop lookup",
"type": "main",
"index": 0
}
]
]
},
"Fetch active shop listings": {
"main": [
[
{
"node": "Analyze shop and compare catalog",
"type": "main",
"index": 0
}
]
]
},
"Build active listings request": {
"main": [
[
{
"node": "Fetch active shop listings",
"type": "main",
"index": 0
}
]
]
},
"Analyze shop and compare catalog": {
"main": [
[
{
"node": "Catalog changes found?",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs manually or daily to audit a public Etsy shop via the Etsy API, summarize active listing metrics, and store a snapshot in n8n to detect newly added or removed listings between runs. Runs on a manual trigger or on a daily schedule. Reads the target Etsy shop…
Source: https://n8n.io/workflows/16449/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow manually processes car photos from Google Drive, sends each image plus a studio backdrop and logo to the Google Gemini Image API for background replacement and license-plate branding, an
3D Listing — covers (platforms split). Uses googleDrive, httpRequest. Event-driven trigger; 12 nodes.
This workflow runs on a schedule (or manually) to fetch public Etsy listing details in a single batch request, stores a baseline snapshot in n8n, and alerts you when listing prices change beyond a con
3D Listing — covers (all formats). Uses googleDrive, httpRequest. Event-driven trigger; 10 nodes.
This n8n workflow automatically tracks hotel room prices, detects price drops, and sends real-time email alerts with savings calculations. It continuously monitors multiple hotels and room types to he