{
  "version": "1.0.0",
  "exported_at": "2026-06-03T12:20:00.000Z",
  "project": {
    "name": "Vinted Advanced Scraper by URL",
    "description": "Best-effort Vinted catalog scraper by URL. Starts from a Vinted catalog/search URL and attempts to collect all available listing pages by calling Vinted same-origin catalog/detail API endpoints in the browser. If the API is blocked, it scrolls the visible catalog page and falls back to DOM card extraction. Exports product data similar to the Octoparse template: title, price, buyer protection, brand, size, condition, material, color, added date, shipping, photos, seller, rating, reviews, and address. Edit the Navigate block URL for another Vinted catalog/search URL. Limitations: Vinted may require a valid browser session, cookies, CAPTCHA, login, or may rate-limit/block anonymous API access; when blocked, the CSV will contain a diagnostic row.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "set-window-size-1",
      "block_type": "process",
      "title": "Set Window Size",
      "description": "Set browser window dimensions",
      "position_x": 120,
      "position_y": 240,
      "config": {
        "width": 1920,
        "height": 1080
      }
    },
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 480,
      "position_y": 240,
      "config": {
        "url": "https://www.vinted.fr/catalog?search_text=chaussure&time=1753329391",
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 840,
      "position_y": 240,
      "config": {
        "timeout": 45
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1200,
      "position_y": 240,
      "config": {
        "duration": 4
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1560,
      "position_y": 240,
      "config": {
        "jsCode": "return (async () => {\n  const rootId = 'uscraper-vinted-results';\n  const old = document.getElementById(rootId);\n  if (old) old.remove();\n\n  const root = document.createElement('div');\n  root.id = rootId;\n  root.style.display = 'none';\n  root.setAttribute('data-source-url', location.href);\n  root.setAttribute('data-api-error', '');\n  document.body.appendChild(root);\n\n  const fields = ['titre','prix','prix_protection','protection_acheteurs','marque','taille','etat','matiere','couleur','ajoute','envoi','photo_1','photo_2','photo_3','vendeur','rating','evaluation','adresse','item_url','item_id','scrape_status'];\n  const sleep = ms => new Promise(r => setTimeout(r, ms));\n  const clean = v => String(v === null || v === undefined ? '' : v).replace(/\\s+/g, ' ').trim();\n  const abs = u => { try { return u ? new URL(u, location.origin).href : ''; } catch (e) { return u || ''; } };\n  const txt = (el, sel) => { const n = sel ? el.querySelector(sel) : el; return n ? clean(n.textContent || '') : ''; };\n  const attr = (el, sel, name) => { const n = sel ? el.querySelector(sel) : el; return n ? clean(n.getAttribute(name) || '') : ''; };\n  const addRow = rec => {\n    const row = document.createElement('div');\n    row.className = 'uscraper-vinted-row';\n    fields.forEach(f => row.setAttribute('data-' + f, clean(rec[f])));\n    root.appendChild(row);\n  };\n  const money = v => {\n    if (v === null || v === undefined) return '';\n    if (typeof v === 'string' || typeof v === 'number') return String(v);\n    if (v.amount !== undefined) return [v.amount, v.currency_code || v.currency || ''].filter(Boolean).join(' ');\n    if (v.value !== undefined) return [v.value, v.currency_code || v.currency || ''].filter(Boolean).join(' ');\n    if (v.price !== undefined) return money(v.price);\n    return '';\n  };\n  const titleOf = v => {\n    if (!v) return '';\n    if (Array.isArray(v)) return v.map(titleOf).filter(Boolean).join(', ');\n    if (typeof v === 'string') return v;\n    return v.title || v.name || v.code || '';\n  };\n  const photoOf = p => {\n    if (!p) return '';\n    if (typeof p === 'string') return p;\n    if (p.full_size_url) return p.full_size_url;\n    if (p.url) return p.url;\n    if (p.image_url) return p.image_url;\n    if (p.dominant_color_url) return p.dominant_color_url;\n    if (p.high_resolution && p.high_resolution.url) return p.high_resolution.url;\n    if (Array.isArray(p.thumbnails) && p.thumbnails.length) return p.thumbnails[p.thumbnails.length - 1].url || '';\n    return '';\n  };\n  async function fetchJson(u) {\n    const r = await fetch(u, {\n      credentials: 'include',\n      headers: {\n        'accept': 'application/json, text/plain, */*',\n        'x-requested-with': 'XMLHttpRequest'\n      }\n    });\n    const t = await r.text();\n    if (!r.ok) throw new Error('HTTP ' + r.status + ' from ' + u + ': ' + t.slice(0, 160));\n    try { return JSON.parse(t); } catch (e) { throw new Error('Non-JSON response from ' + u + ': ' + t.slice(0, 160)); }\n  }\n\n  const errors = [];\n  const records = [];\n  const items = [];\n  const seen = new Set();\n  const source = new URL(location.href);\n  const searchText = source.searchParams.get('search_text') || source.searchParams.get('q') || '';\n\n  async function collectApiItems() {\n    const maxPages = 60;\n    const maxItems = 10000;\n    for (let page = 1; page <= maxPages && items.length < maxItems; page++) {\n      const candidates = [];\n      const api1 = new URL('/api/v2/catalog/items', location.origin);\n      source.searchParams.forEach((v, k) => {\n        if (!['page', 'per_page', 'time'].includes(k)) api1.searchParams.append(k, v);\n      });\n      if (searchText && !api1.searchParams.has('search_text')) api1.searchParams.set('search_text', searchText);\n      api1.searchParams.set('page', String(page));\n      api1.searchParams.set('per_page', '96');\n      api1.searchParams.set('order', api1.searchParams.get('order') || 'newest_first');\n      candidates.push(api1.href);\n\n      const api2 = new URL('/api/v2/catalog/items', location.origin);\n      if (searchText) api2.searchParams.set('search_text', searchText);\n      api2.searchParams.set('page', String(page));\n      api2.searchParams.set('per_page', '96');\n      api2.searchParams.set('order', 'newest_first');\n      candidates.push(api2.href);\n\n      let data = null;\n      let lastErr = '';\n      for (const u of candidates) {\n        try {\n          data = await fetchJson(u);\n          break;\n        } catch (e) {\n          lastErr = String(e && e.message ? e.message : e);\n        }\n      }\n      if (!data) {\n        errors.push('catalog page ' + page + ': ' + lastErr);\n        break;\n      }\n      const pageItems = data.items || data.catalog_items || data.results || data.data || [];\n      if (!Array.isArray(pageItems) || !pageItems.length) break;\n      let added = 0;\n      for (const it of pageItems) {\n        const id = it.id || it.item_id || (it.item && it.item.id);\n        const key = String(id || it.url || JSON.stringify(it).slice(0, 80));\n        if (!seen.has(key)) {\n          seen.add(key);\n          items.push(it.item || it);\n          added++;\n          if (items.length >= maxItems) break;\n        }\n      }\n      const p = data.pagination || data.meta || {};\n      if (!added) break;\n      if (p.total_pages && page >= Number(p.total_pages)) break;\n      if (p.next_page === null || p.next_page === false) break;\n      await sleep(300);\n    }\n  }\n\n  try {\n    await collectApiItems();\n  } catch (e) {\n    errors.push('api collection failed: ' + String(e && e.message ? e.message : e));\n  }\n\n  if (items.length) {\n    for (const it of items) {\n      let d = it;\n      const id = it.id || it.item_id;\n      if (id) {\n        const detailUrls = ['/api/v2/items/' + id, '/api/v2/items/' + id + '?localize=false'];\n        for (const du of detailUrls) {\n          try {\n            const detail = await fetchJson(du);\n            d = detail.item || detail.data || detail || it;\n            break;\n          } catch (e) {}\n        }\n        await sleep(120);\n      }\n      const user = d.user || it.user || d.seller || {};\n      const photos = (d.photos || d.images || it.photos || it.images || (d.photo ? [d.photo] : [])).map(photoOf).filter(Boolean);\n      const city = d.city || d.location || user.city || '';\n      const country = d.country_title || d.country || user.country_title || user.country || '';\n      const fee = d.service_fee || d.buyer_fee || d.buyer_protection_fee || d.protection_fee || '';\n      const itemUrl = abs(d.url || it.url || (id ? '/items/' + id : ''));\n      records.push({\n        titre: d.title || it.title || '',\n        prix: money(d.price || it.price),\n        prix_protection: money(d.total_item_price || d.price_with_buyer_fee || d.price_with_fees || d.total_price),\n        protection_acheteurs: fee || d.price_with_buyer_fee || d.total_item_price ? 'Inclut la Protection acheteurs' : '',\n        marque: d.brand_title || titleOf(d.brand) || it.brand_title || '',\n        taille: d.size_title || titleOf(d.size) || it.size_title || '',\n        etat: d.status || d.status_title || d.condition || it.status || '',\n        matiere: titleOf(d.materials || d.material),\n        couleur: titleOf(d.colors || d.color),\n        ajoute: d.created_at || d.created_at_ts || it.created_at || '',\n        envoi: money(d.shipping_price || d.shipment_price || d.min_shipping_price || d.shipping_fee),\n        photo_1: photos[0] || '',\n        photo_2: photos[1] || '',\n        photo_3: photos[2] || '',\n        vendeur: user.login || user.username || user.name || '',\n        rating: user.rating || user.feedback_reputation || user.feedback_rating || '',\n        evaluation: user.feedback_count || user.feedbacks_count || user.feedback_count_positive || '',\n        adresse: [city, country].filter(Boolean).join(', '),\n        item_url: itemUrl,\n        item_id: id || '',\n        scrape_status: 'api_ok'\n      });\n    }\n  }\n\n  if (!records.length) {\n    try {\n      for (let i = 0; i < 8; i++) {\n        window.scrollTo(0, document.body.scrollHeight);\n        await sleep(1200);\n      }\n      const cardSelectors = [\n        '[data-testid*=\"grid-item\"]',\n        '[data-testid*=\"item-box\"]',\n        'div.feed-grid__item',\n        '[class*=\"feed-grid\"] > div',\n        'a[href*=\"/items/\"]'\n      ];\n      const cardSet = new Set();\n      for (const sel of cardSelectors) document.querySelectorAll(sel).forEach(el => cardSet.add(el.closest('div') || el));\n      Array.from(cardSet).forEach(card => {\n        const link = card.querySelector('a[href*=\"/items/\"]') || (card.matches && card.matches('a[href*=\"/items/\"]') ? card : null);\n        const img = card.querySelector('img');\n        const title = txt(card, '[data-testid*=\"title\"]') || txt(card, '[data-testid*=\"item-title\"]') || attr(img, null, 'alt');\n        const price = txt(card, '[data-testid*=\"price\"]') || Array.from(card.querySelectorAll('span,div')).map(n => clean(n.textContent)).find(t => /€|EUR|£|GBP|\\$/.test(t)) || '';\n        const subtitle = txt(card, '[data-testid*=\"subtitle\"]') || '';\n        const url = link ? abs(link.getAttribute('href')) : '';\n        const photo = img ? (img.currentSrc || img.src || '') : '';\n        if (title || price || url || photo) {\n          records.push({\n            titre: title,\n            prix: price,\n            prix_protection: '',\n            protection_acheteurs: '',\n            marque: subtitle,\n            taille: '',\n            etat: '',\n            matiere: '',\n            couleur: '',\n            ajoute: '',\n            envoi: '',\n            photo_1: photo,\n            photo_2: '',\n            photo_3: '',\n            vendeur: '',\n            rating: '',\n            evaluation: '',\n            adresse: '',\n            item_url: url,\n            item_id: (url.match(/items\\/(\\d+)/) || [])[1] || '',\n            scrape_status: 'dom_fallback_partial'\n          });\n        }\n      });\n    } catch (e) {\n      errors.push('dom fallback failed: ' + String(e && e.message ? e.message : e));\n    }\n  }\n\n  if (records.length) {\n    records.forEach(addRow);\n    root.setAttribute('data-row-count', String(records.length));\n  } else {\n    const message = errors.join(' | ') || 'No API items and no visible listing cards were found. Vinted may be blocking automation, requiring cookies/login/CAPTCHA, or the search URL returned no public results.';\n    root.setAttribute('data-api-error', message);\n    addRow({\n      titre: 'NO_RESULTS_OR_BLOCKED',\n      prix: '',\n      prix_protection: '',\n      protection_acheteurs: '',\n      marque: '',\n      taille: '',\n      etat: '',\n      matiere: '',\n      couleur: '',\n      ajoute: '',\n      envoi: '',\n      photo_1: '',\n      photo_2: '',\n      photo_3: '',\n      vendeur: '',\n      rating: '',\n      evaluation: '',\n      adresse: message,\n      item_url: location.href,\n      item_id: '',\n      scrape_status: 'blocked_or_empty'\n    });\n  }\n  return { rows: root.querySelectorAll('.uscraper-vinted-row').length, errors };\n})();",
        "waitForCompletion": true,
        "timeout": 900
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1920,
      "position_y": 240,
      "config": {
        "selector": "#uscraper-vinted-results .uscraper-vinted-row",
        "timeout": 90,
        "visible": false
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2280,
      "position_y": 240,
      "config": {
        "rowSelector": "#uscraper-vinted-results .uscraper-vinted-row",
        "fileName": "vinted_data_scraper_by_url.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "create",
        "columns": [
          {
            "name": "titre",
            "selector": "",
            "attribute": "data-titre"
          },
          {
            "name": "prix",
            "selector": "",
            "attribute": "data-prix"
          },
          {
            "name": "prix_protection",
            "selector": "",
            "attribute": "data-prix_protection"
          },
          {
            "name": "protection_acheteurs",
            "selector": "",
            "attribute": "data-protection_acheteurs"
          },
          {
            "name": "marque",
            "selector": "",
            "attribute": "data-marque"
          },
          {
            "name": "taille",
            "selector": "",
            "attribute": "data-taille"
          },
          {
            "name": "etat",
            "selector": "",
            "attribute": "data-etat"
          },
          {
            "name": "matiere",
            "selector": "",
            "attribute": "data-matiere"
          },
          {
            "name": "couleur",
            "selector": "",
            "attribute": "data-couleur"
          },
          {
            "name": "ajoute",
            "selector": "",
            "attribute": "data-ajoute"
          },
          {
            "name": "envoi",
            "selector": "",
            "attribute": "data-envoi"
          },
          {
            "name": "photo_1",
            "selector": "",
            "attribute": "data-photo_1"
          },
          {
            "name": "photo_2",
            "selector": "",
            "attribute": "data-photo_2"
          },
          {
            "name": "photo_3",
            "selector": "",
            "attribute": "data-photo_3"
          },
          {
            "name": "vendeur",
            "selector": "",
            "attribute": "data-vendeur"
          },
          {
            "name": "rating",
            "selector": "",
            "attribute": "data-rating"
          },
          {
            "name": "evaluation",
            "selector": "",
            "attribute": "data-evaluation"
          },
          {
            "name": "adresse",
            "selector": "",
            "attribute": "data-adresse"
          },
          {
            "name": "item_url",
            "selector": "",
            "attribute": "data-item_url"
          },
          {
            "name": "item_id",
            "selector": "",
            "attribute": "data-item_id"
          },
          {
            "name": "scrape_status",
            "selector": "",
            "attribute": "data-scrape_status"
          }
        ]
      }
    },
    {
      "block_id": "end-1",
      "block_type": "output",
      "title": "End",
      "description": "Terminate execution flow",
      "position_x": 2640,
      "position_y": 240,
      "config": {}
    }
  ],
  "connections": [
    {
      "from_block_id": "set-window-size-1",
      "from_connector_id": "right",
      "to_block_id": "navigate-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "navigate-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-page-load-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-page-load-1",
      "from_connector_id": "right",
      "to_block_id": "sleep-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-1",
      "from_connector_id": "right",
      "to_block_id": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-1",
      "from_connector_id": "right",
      "to_block_id": "structured-export-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-1",
      "from_connector_id": "right",
      "to_block_id": "end-1",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-entry",
      "element_type": "group",
      "title": "Entry & Setup",
      "color": "#4589ff",
      "position_x": 48,
      "position_y": 136,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "set-window-size-1"
        ]
      }
    },
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 408,
      "position_y": 136,
      "width": 1760,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "wait-for-element-1"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1488,
      "position_y": 136,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 2208,
      "position_y": 136,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "group-control",
      "element_type": "group",
      "title": "Control Flow",
      "color": "#8d8d8d",
      "position_x": 2568,
      "position_y": 136,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "end-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort Vinted catalog scraper by URL. Starts from a Vinted catalog/search URL and attempts to collect all available listing pages by calling Vinted same-origin catalog/detail API endpoints in the browser. If the API is blocked, it scrolls the visible catalog page and falls back to DOM card extraction. Exports product data similar to the Octoparse template: title, price, buyer protection, brand, size, condition, material, color, added date, shipping, photos, seller, rating, reviews, and address. Edit the Navigate block URL for another Vinted catalog/search URL. Limitations: Vinted may require a valid browser session, cookies, CAPTCHA, login, or may rate-limit/block anonymous API access; when blocked, the CSV will contain a diagnostic row.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `return (async () => {\n  const rootId = 'uscraper-vinted-results';\n  const old = document.getElementB...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1760,
      "position_y": 220,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-1"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Extracts rows matching `#uscraper-vinted-results .uscraper-vinted-row`. Confirm row count > 0 before running at scale.",
      "color": "#ee5396",
      "position_x": 2480,
      "position_y": 220,
      "width": 340,
      "height": 119,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    }
  ]
}