{
  "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
          }
        ]
      ]
    }
  }
}