This workflow corresponds to n8n.io template #16420 — 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": "Track competitor listing prices on Etsy",
"nodes": [
{
"id": "manual-trigger",
"name": "Run tracker manually",
"type": "n8n-nodes-base.manualTrigger",
"position": [
0,
300
],
"parameters": {},
"typeVersion": 1
},
{
"id": "schedule-trigger",
"name": "Check every 6 hours",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
0,
500
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"typeVersion": 1.2
},
{
"id": "configuration",
"name": "Configuration (EDIT ME)",
"type": "n8n-nodes-base.set",
"position": [
280,
400
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "config-listing-ids",
"name": "listing_ids",
"type": "string",
"value": "PASTE_COMMA_SEPARATED_LISTING_IDS_HERE"
},
{
"id": "config-threshold",
"name": "minimum_change_percent",
"type": "number",
"value": 0
}
]
},
"includeOtherFields": false
},
"typeVersion": 3.4
},
{
"id": "build-request",
"name": "Build Etsy batch request",
"type": "n8n-nodes-base.code",
"position": [
560,
400
],
"parameters": {
"jsCode": "// Validates the user input and builds one public Etsy API URL.\n// Listing IDs are the numeric values found in Etsy listing URLs.\nconst config = $input.first().json;\nconst rawIds = String(config.listing_ids || '');\nconst listingIds = [...new Set(rawIds.match(/\\d+/g) || [])];\n\nif (listingIds.length === 0) {\n throw new Error('Add at least one numeric Etsy listing ID in Configuration (EDIT ME).');\n}\nif (listingIds.length > 100) {\n throw new Error('Etsy accepts up to 100 listing IDs per batch request.');\n}\n\nconst minimumChangePercent = Number(config.minimum_change_percent || 0);\nif (!Number.isFinite(minimumChangePercent) || minimumChangePercent < 0) {\n throw new Error('minimum_change_percent must be zero or a positive number.');\n}\n\nconst encodedIds = encodeURIComponent(listingIds.join(','));\n\nreturn [{\n json: {\n listing_ids: listingIds,\n minimum_change_percent: minimumChangePercent,\n request_url: `https://api.etsy.com/v3/application/listings/batch?listing_ids=${encodedIds}`\n }\n}];"
},
"typeVersion": 2
},
{
"id": "fetch-listings",
"name": "Fetch public Etsy listings",
"type": "n8n-nodes-base.httpRequest",
"position": [
840,
400
],
"parameters": {
"url": "={{ $json.request_url }}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"typeVersion": 4.3
},
{
"id": "compare-prices",
"name": "Compare prices with previous run",
"type": "n8n-nodes-base.code",
"position": [
1120,
400
],
"parameters": {
"jsCode": "// Compares the current Etsy prices with the previous production run.\n// Workflow static data keeps the snapshot inside n8n, so no database is required.\nconst response = $input.first().json;\nconst config = $('Build Etsy batch request').first().json;\nconst listings = Array.isArray(response.results) ? response.results : [];\nconst requestedIds = config.listing_ids.map(String);\nconst threshold = Number(config.minimum_change_percent || 0);\nconst staticData = $getWorkflowStaticData('global');\nconst previous = staticData.etsyPriceTrackerSnapshot || {};\nconst hasPreviousSnapshot = Object.keys(previous).length > 0;\nconst current = {};\nconst changes = [];\n\nconst getPrice = (listing) => {\n const amount = Number(listing?.price?.amount);\n const divisor = Number(listing?.price?.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 = getPrice(listing);\n const record = {\n listing_id: id,\n title: listing.title || '',\n shop_id: listing.shop_id ? String(listing.shop_id) : '',\n price,\n currency: listing?.price?.currency_code || '',\n quantity: listing.quantity ?? null,\n state: listing.state || '',\n url: listing.url || `https://www.etsy.com/listing/${id}`,\n checked_at: new Date().toISOString()\n };\n current[id] = record;\n\n const old = previous[id];\n if (!hasPreviousSnapshot || !old || price === null || old.price === null) continue;\n\n const oldCents = Math.round(Number(old.price) * 100);\n const newCents = Math.round(price * 100);\n if (oldCents === newCents) continue;\n\n const changePercent = oldCents === 0\n ? null\n : Math.round((((newCents - oldCents) / oldCents) * 100) * 100) / 100;\n if (changePercent !== null && Math.abs(changePercent) < threshold) continue;\n\n changes.push({\n type: 'price_changed',\n listing_id: id,\n title: record.title,\n old_price: old.price,\n new_price: price,\n currency: record.currency,\n change_percent: changePercent,\n url: record.url\n });\n}\n\nif (hasPreviousSnapshot) {\n for (const id of requestedIds) {\n if (previous[id] && !current[id]) {\n changes.push({\n type: 'listing_unavailable',\n listing_id: id,\n title: previous[id].title || '',\n old_price: previous[id].price,\n currency: previous[id].currency || '',\n url: previous[id].url || `https://www.etsy.com/listing/${id}`\n });\n }\n }\n}\n\nstaticData.etsyPriceTrackerSnapshot = current;\n\nconst unavailableOnFirstRun = hasPreviousSnapshot\n ? []\n : requestedIds.filter((id) => !current[id]);\n\nreturn [{\n json: {\n status: hasPreviousSnapshot ? 'comparison_complete' : 'baseline_created',\n checked_at: new Date().toISOString(),\n requested_count: requestedIds.length,\n found_count: listings.length,\n total_changes: changes.length,\n changes,\n unavailable_on_first_run: unavailableOnFirstRun,\n current_listings: Object.values(current)\n }\n}];"
},
"typeVersion": 2
},
{
"id": "changes-found",
"name": "Changes found?",
"type": "n8n-nodes-base.if",
"position": [
1400,
400
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "has-price-changes",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.total_changes }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.2
},
{
"id": "price-change-alerts",
"name": "Price change alerts",
"type": "n8n-nodes-base.set",
"position": [
1680,
300
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "alert-message",
"name": "result_message",
"type": "string",
"value": "=Price changes detected for {{ $json.total_changes }} Etsy listing(s). Connect this output to Slack, email, Telegram, or your preferred notification node."
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "no-alerts-summary",
"name": "Baseline or no changes",
"type": "n8n-nodes-base.set",
"position": [
1680,
500
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "summary-message",
"name": "result_message",
"type": "string",
"value": "={{ $json.status === 'baseline_created' ? 'Baseline saved. The next scheduled run will compare prices against it.' : 'No price changes matched your threshold.' }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "overview-sticky",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-40,
-420
],
"parameters": {
"color": 1,
"width": 660,
"height": 640,
"content": "## Track Competitor Listing Prices on Etsy\n\nMonitor the current prices of selected public Etsy listings and detect changes without OAuth, a database, or a third-party scraping service. This workflow uses Etsy's official Open API and stores the previous snapshot in n8n workflow static data.\n\n### How it works\n- A manual or scheduled trigger starts the price check.\n- **Configuration (EDIT ME)** accepts one or more numeric Etsy listing IDs and an optional minimum percentage change.\n- One public batch request fetches the current listing details.\n- The workflow normalizes Etsy Money values, compares them with the previous production run, and reports price changes or unavailable listings.\n- The first production run creates the baseline; later runs perform comparisons.\n\n### Setup\n1. Create an Etsy developer app and copy its keystring and shared secret.\n2. In **Fetch public Etsy listings**, create a Header Auth credential. Set the header name to `x-api-key` and the value to `KEYSTRING:SHARED_SECRET`.\n3. Add competitor listing IDs in **Configuration (EDIT ME)**. You can find each numeric ID in its Etsy URL.\n4. Activate the workflow. The default schedule runs every six hours.\n\n### Customization tips\nChange the schedule, raise the percentage threshold to ignore small changes, or connect **Price change alerts** to Slack, email, Telegram, or Google Sheets.\n"
},
"typeVersion": 1
},
{
"id": "fetch-section-sticky",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-60,
220
],
"parameters": {
"color": 7,
"width": 1080,
"height": 440,
"content": "## 1. Configure and fetch listings\nRun manually or on schedule, enter up to 100 listing IDs, and fetch their current public data from Etsy in one authenticated batch request.\n"
},
"typeVersion": 1
},
{
"id": "compare-section-sticky",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1080,
220
],
"parameters": {
"color": 7,
"width": 900,
"height": 440,
"content": "## 2. Compare and route changes\nSave a baseline inside n8n, compare prices on later production runs, and route meaningful changes to the alert output.\n"
},
"typeVersion": 1
}
],
"settings": {
"executionOrder": "v1"
},
"connections": {
"Changes found?": {
"main": [
[
{
"node": "Price change alerts",
"type": "main",
"index": 0
}
],
[
{
"node": "Baseline or no changes",
"type": "main",
"index": 0
}
]
]
},
"Check every 6 hours": {
"main": [
[
{
"node": "Configuration (EDIT ME)",
"type": "main",
"index": 0
}
]
]
},
"Run tracker manually": {
"main": [
[
{
"node": "Configuration (EDIT ME)",
"type": "main",
"index": 0
}
]
]
},
"Configuration (EDIT ME)": {
"main": [
[
{
"node": "Build Etsy batch request",
"type": "main",
"index": 0
}
]
]
},
"Build Etsy batch request": {
"main": [
[
{
"node": "Fetch public Etsy listings",
"type": "main",
"index": 0
}
]
]
},
"Fetch public Etsy listings": {
"main": [
[
{
"node": "Compare prices with previous run",
"type": "main",
"index": 0
}
]
]
},
"Compare prices with previous run": {
"main": [
[
{
"node": "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 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 configurable percentage threshold. Runs either manually or every 6 hours on a…
Source: https://n8n.io/workflows/16420/ — 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
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.
3D Listing — covers (platforms split). Uses googleDrive, httpRequest. Event-driven trigger; 12 nodes.
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