├── .gitignore ├── .gitattributes ├── github └── vinted-app.jpg ├── package.json ├── api ├── server.js └── routes │ ├── item.js │ └── member.js ├── service ├── searchItems.js ├── getMemberInfo.js ├── getMemberItems.js ├── getMemberRates.js ├── searchMembers.js └── getItemInfo.js ├── LICENSE ├── utils └── formatTimeDescription.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /github/vinted-app.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrBalourd/Vinted-API/HEAD/github/vinted-app.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vintedscrapper", 3 | "version": "1.0.0", 4 | "description": "grab some infos from vinted website!", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node api/server.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "mr.balourd", 11 | "license": "MIT", 12 | "dependencies": { 13 | "body-parser": "^1.20.3", 14 | "express": "^4.21.2", 15 | "node-fetch": "^3.3.2", 16 | "puppeteer": "^24.2.0" 17 | }, 18 | "type": "module" 19 | } 20 | -------------------------------------------------------------------------------- /api/server.js: -------------------------------------------------------------------------------- 1 | // make express server 2 | import express from 'express'; 3 | import bodyParser from 'body-parser'; 4 | import member from './routes/member.js'; 5 | import item from './routes/item.js'; 6 | 7 | 8 | const app = express(); 9 | const port = process.env.PORT || 3000; 10 | 11 | // body parser middleware 12 | app.use(bodyParser.urlencoded({ extended: true })); 13 | app.use(bodyParser.json()); 14 | 15 | 16 | app.use(member); 17 | app.use(item) 18 | 19 | app.listen(port, () => { 20 | console.log(`Server running on port ${port}`); 21 | console.log(`localhost:${port}`) 22 | }); -------------------------------------------------------------------------------- /service/searchItems.js: -------------------------------------------------------------------------------- 1 | import puppeteer from "puppeteer"; 2 | import fetch from "node-fetch"; 3 | export async function searchItems(query) { 4 | const result = []; 5 | const browser = await puppeteer.launch(); 6 | const page = await browser.newPage(); 7 | 8 | const url_request= `https://www.vinted.fr/api/v2/catalog/items?page=1&per_page=96&search_text=${query.text}&catalog_ids=¤cy=${query.currency}&size_ids=&brand_ids=&status_ids=&color_ids=&material_ids=` 9 | await page.goto(url_request); 10 | const data = await page.evaluate(() => { 11 | return fetch('https://www.vinted.fr/api/v2/catalog/items?page=1&per_page=96&search_text=robe&catalog_ids=¤cy=EUR&size_ids=&brand_ids=&status_ids=&color_ids=&material_ids=') 12 | .then(response => response.json()) 13 | .then(data => data.items); 14 | }); 15 | console.log(url_request) 16 | return data; 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mr.Balourd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/routes/item.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { getItemInfo } from "../../service/getItemInfo.js" 3 | import { searchItems } from "../../service/searchItems.js" 4 | const router = express.Router(); 5 | 6 | 7 | router.post('/api/items', async (req, res) => { 8 | const query = req.body; 9 | if (!query || !query.text || !query.currency || !query.order) { 10 | return res.status(400).json({ error: 'Invalid request body' }); 11 | } 12 | 13 | try { 14 | const data = await searchItems(query); 15 | res.json(data); 16 | 17 | } catch (error) { 18 | res.status(500).json({ error: 'Internal server error' + error }); 19 | } 20 | }); 21 | 22 | router.post('/api/item', async (req, res) => { 23 | const query = req.body; 24 | if (!query || !query.item) { 25 | return res.status(400).json({ error: 'Invalid request body' }); 26 | } 27 | 28 | try { 29 | // Fetch search results using the provided query 30 | console.log(query) 31 | 32 | const data = await getItemInfo(query); 33 | 34 | console.log(data) 35 | 36 | res.json(data); 37 | 38 | } catch (error) { 39 | res.status(500).json({ error: 'Internal server error' + error }); 40 | } 41 | }); 42 | export default router; -------------------------------------------------------------------------------- /service/getMemberInfo.js: -------------------------------------------------------------------------------- 1 | import puppeteer from "puppeteer"; 2 | 3 | export async function getMemberInfo(memberDress) { 4 | const memberDressLink = `https://www.vinted.fr/member/${memberDress}` 5 | try { 6 | const browser = await puppeteer.launch({headless: "new"}); 7 | const page = await browser.newPage(); 8 | 9 | await page.setRequestInterception(true); 10 | 11 | page.on('request', (req) => { 12 | if(req.resourceType() === 'stylesheet' || req.resourceType() === 'font'){ 13 | req.abort(); 14 | } 15 | else { 16 | req.continue(); 17 | } 18 | }); 19 | 20 | await page.goto(memberDressLink); 21 | await page.waitForSelector("#onetrust-banner-sdk > div"); 22 | 23 | await page.evaluate(() => { 24 | const acceptButton = document.querySelector('#onetrust-accept-btn-handler'); 25 | if (acceptButton) { 26 | acceptButton.click(); 27 | } 28 | }); 29 | const memberId = memberDress.split("-")[0]; 30 | await page.goto(`https://www.vinted.fr/api/v2/users/${memberId}`) 31 | // select the pre element int obody 32 | const userInfosBased = await page.evaluate(() => {return document.querySelector("pre").textContent}) 33 | const userInfos = JSON.parse(userInfosBased); 34 | await browser.close(); 35 | return userInfos 36 | } catch (e ){ 37 | return e 38 | } 39 | } -------------------------------------------------------------------------------- /utils/formatTimeDescription.js: -------------------------------------------------------------------------------- 1 | export function formatTimeDescription(time, isLogin) { 2 | const now = new Date(); 3 | const timeDifference = now - time; 4 | const secondsDifference = Math.floor(timeDifference / 1000); 5 | const minutesDifference = Math.floor(secondsDifference / 60); 6 | const hoursDifference = Math.floor(minutesDifference / 60); 7 | 8 | if (isLogin) { 9 | if (secondsDifference < 60) { 10 | return `${secondsDifference} second${secondsDifference !== 1 ? 's' : ''} ago`; 11 | } else if (minutesDifference < 60) { 12 | return `${minutesDifference} minute${minutesDifference !== 1 ? 's' : ''} ago`; 13 | } else if (hoursDifference < 24) { 14 | return `${hoursDifference} hour${hoursDifference !== 1 ? 's' : ''} ago`; 15 | } else { 16 | const days = Math.floor(hoursDifference / 24); 17 | return `${days} day${days !== 1 ? 's' : ''} ago`; 18 | } 19 | } else { 20 | if (secondsDifference < 60) { 21 | return `${secondsDifference} second${secondsDifference !== 1 ? 's' : ''} ago`; 22 | } else if (minutesDifference < 60) { 23 | return `${minutesDifference} minute${minutesDifference !== 1 ? 's' : ''} ago`; 24 | } else if (hoursDifference < 24) { 25 | return `${hoursDifference} hour${hoursDifference !== 1 ? 's' : ''} ago`; 26 | } else { 27 | const days = Math.floor(hoursDifference / 24); 28 | return `${days} day${days !== 1 ? 's' : ''} ago`; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /service/getMemberItems.js: -------------------------------------------------------------------------------- 1 | import puppeteer from "puppeteer"; 2 | 3 | export async function getMemberItems (memberDress, page) { 4 | const memberDressLink = `https://www.vinted.fr/member/${memberDress}` 5 | try { 6 | const browser = await puppeteer.launch({headless: "new"}); 7 | const page = await browser.newPage(); 8 | 9 | await page.setRequestInterception(true); 10 | 11 | page.on('request', (req) => { 12 | if(req.resourceType() === 'stylesheet' || req.resourceType() === 'font'){ 13 | req.abort(); 14 | } 15 | else { 16 | req.continue(); 17 | } 18 | }); 19 | 20 | await page.goto(memberDressLink); 21 | await page.waitForSelector("#onetrust-banner-sdk > div"); 22 | 23 | await page.evaluate(() => { 24 | const acceptButton = document.querySelector('#onetrust-accept-btn-handler'); 25 | if (acceptButton) { 26 | acceptButton.click(); 27 | } 28 | }); 29 | const memberId = memberDress.split("-")[0]; 30 | await page.goto(`https://www.vinted.fr/api/v2/users/${memberId}/items?page=${page}&per_page=20&order=relevance`) 31 | // select the pre element int obody 32 | const dressItemsBased = await page.evaluate(() => {return document.querySelector("pre").textContent}) 33 | const dressItems = JSON.parse(dressItemsBased); 34 | await browser.close(); 35 | return dressItems 36 | } catch (e ){ 37 | return e 38 | } 39 | } -------------------------------------------------------------------------------- /service/getMemberRates.js: -------------------------------------------------------------------------------- 1 | import puppeteer from "puppeteer"; 2 | 3 | export async function getMemberRates(memberDress) { 4 | const memberDressLink = `https://www.vinted.fr/member/${memberDress}` 5 | try { 6 | const browser = await puppeteer.launch({headless: "new"}); 7 | const page = await browser.newPage(); 8 | 9 | await page.setRequestInterception(true); 10 | 11 | page.on('request', (req) => { 12 | if(req.resourceType() === 'stylesheet' || req.resourceType() === 'font'){ 13 | req.abort(); 14 | } 15 | else { 16 | req.continue(); 17 | } 18 | }); 19 | 20 | await page.goto(memberDressLink); 21 | await page.waitForSelector("#onetrust-banner-sdk > div"); 22 | 23 | await page.evaluate(() => { 24 | const acceptButton = document.querySelector('#onetrust-accept-btn-handler'); 25 | if (acceptButton) { 26 | acceptButton.click(); 27 | } 28 | }); 29 | const memberId = memberDress.split("-")[0]; 30 | const rateUrl = `https://www.vinted.fr/api/v2/user_feedbacks?user_id=${memberId}&page=1&per_page=250&by=all`; 31 | await page.goto(rateUrl) 32 | // select the pre element int obody 33 | const dressItemsBased = await page.evaluate(() => {return document.querySelector("pre").textContent}) 34 | const dressItems = JSON.parse(dressItemsBased); 35 | await browser.close(); 36 | return dressItems 37 | } catch (e ){ 38 | return e 39 | } 40 | } -------------------------------------------------------------------------------- /service/searchMembers.js: -------------------------------------------------------------------------------- 1 | import puppeteer from "puppeteer"; 2 | 3 | export async function searchMembers(memberName) { 4 | const browser = await puppeteer.launch({headless: "new"}); 5 | const page = await browser.newPage(); 6 | await page.setRequestInterception(true); 7 | 8 | page.on('request', (req) => { 9 | if(req.resourceType() === 'stylesheet' || req.resourceType() === 'font'){ 10 | req.abort(); 11 | } 12 | else { 13 | req.continue(); 14 | } 15 | }); 16 | 17 | await page.goto(`https://www.vinted.fr/member/general/search?page=1&search_text=${memberName}`); 18 | // wait for selector 19 | await page.waitForSelector(".user-grid"); 20 | await page.waitForSelector("#onetrust-banner-sdk > div"); 21 | 22 | await page.evaluate(() => { 23 | const acceptButton = document.querySelector('#onetrust-accept-btn-handler'); 24 | if (acceptButton) { 25 | acceptButton.click(); 26 | } 27 | }); 28 | 29 | 30 | const users = await page.$$(".user-grid > div.user-grid__item > a"); 31 | console.log(`Found ${users.length} users`); 32 | 33 | 34 | const userPromises = users.map(async (user, index) => { 35 | console.log(`Processing user ${index + 1}`); 36 | 37 | 38 | const memberName = await user.evaluate(el => el.querySelector(".web_ui__Cell__content > div > div > div > span").textContent); 39 | const memberId = await user.evaluate(el => el.getAttribute("href").split("/")[2]); 40 | const memberImage = await user.evaluate(el => el.querySelector("div > div > img").getAttribute("src")); 41 | const memberRateCount = await user.evaluate(el => el.querySelector(".web_ui__Cell__content > .web_ui__Cell__body > div > .web_ui__Rating__label > h4").textContent); 42 | 43 | 44 | return { 45 | memberName, 46 | memberId, 47 | memberImage, 48 | memberRateCount 49 | }; 50 | }); 51 | const processedUsers = await Promise.all(userPromises); 52 | await browser.close(); 53 | return processedUsers; 54 | } -------------------------------------------------------------------------------- /api/routes/member.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { searchMembers } from "../../service/searchMembers.js"; 3 | import { getMemberItems } from "../../service/getMemberItems.js"; 4 | import { getMemberInfo } from "../../service/getMemberInfo.js"; 5 | import {getMemberRates} from "../../service/getMemberRates.js"; 6 | const router = express.Router(); 7 | 8 | 9 | router.get("/api/member/search", async (req, res) => { 10 | const search = req.query.query.replace(/'/g, '').replace(/"/g, ''); 11 | if (search === undefined || search === null || search.length === 0 || search === " " || search === ' ') { 12 | res.send({ error: true, message: "Please enter a search term." }); 13 | } else { 14 | const membersResult = await searchMembers(search); 15 | res.send(membersResult); 16 | } 17 | 18 | }); 19 | 20 | 21 | router.get('/api/member/:memberId', async (req, res) => { 22 | const memberId = req.params.memberId; 23 | if (!memberId) { 24 | return res.status(400).json({ error: 'Invalid member ID' }); 25 | } 26 | try { 27 | // Fetch items for the specified member using their ID 28 | const memberInfos = await getMemberInfo(memberId); 29 | res.json(memberInfos); 30 | } catch (error) { 31 | res.status(500).json({ error: 'Internal server error' + error }); 32 | 33 | } 34 | }); 35 | 36 | 37 | 38 | router.get("/api/member/:id/items", async (req, res) => { 39 | let memberId = req.params.id; 40 | let page = req.query.page || 1; 41 | if (!memberId) { 42 | return res.status(400).json({ error: 'Invalid member ID' }); 43 | } 44 | 45 | try { 46 | 47 | const memberItems = await getMemberItems(memberId, page); 48 | res.json(memberItems); 49 | 50 | } catch (error) { 51 | res.status(500).json({ error: 'Internal server error' + error }); 52 | 53 | } 54 | 55 | 56 | }); 57 | 58 | router.get('/api/member/:memberId/rate', async (req, res) => { 59 | const memberId = req.params.memberId; 60 | if (!memberId) { 61 | return res.status(400).json({ error: 'Invalid member ID' }); 62 | } 63 | 64 | try { 65 | const memberRates = await getMemberRates(memberId); 66 | res.json(memberRates); 67 | } catch (error) { 68 | res.status(500).json({ error: 'Internal server error' + error }); 69 | 70 | } 71 | }); 72 | 73 | router.get("/1", (req, res) => { 74 | res.send("1"); 75 | }); 76 | 77 | export default router; -------------------------------------------------------------------------------- /service/getItemInfo.js: -------------------------------------------------------------------------------- 1 | import puppeteer from "puppeteer"; 2 | import { formatTimeDescription } from "../utils/formatTimeDescription.js"; 3 | export async function getItemInfo(itemLink) { 4 | const ItemsData = []; 5 | try { 6 | const browser = await puppeteer.launch({headless: "new"}); 7 | const page = await browser.newPage(); 8 | await page.goto(itemLink.item); 9 | 10 | 11 | // Scrap JSON data 12 | const itemInfoBased = await page.evaluate(() => { return document.querySelector('script[data-component-name="ItemActions"]').textContent }) 13 | const itemInfoParsed = JSON.parse(itemInfoBased) 14 | const itemInfo = itemInfoParsed.item; 15 | 16 | const created_at_ts = new Date(itemInfo.created_at_ts); 17 | const last_logged_on_ts = new Date(itemInfo.user.last_logged_on_ts); 18 | // Calculate the time description for an article 19 | const articleTimeDescription = formatTimeDescription(created_at_ts, false); // Replace with your actual article post time 20 | 21 | // Calculate the time description for a login 22 | const loginTimeDescription = formatTimeDescription(last_logged_on_ts, true); // Replace with your actual login time 23 | ItemsData.push({ 24 | vendor_infos: [ 25 | { 26 | vendor: itemInfo.user.login, 27 | photo: itemInfo.user.photo.full_size_url, 28 | is_suspicious: itemInfo.user.photo.is_suspicious, 29 | id: itemInfo.user.photo.id, 30 | last_logged: loginTimeDescription 31 | } 32 | ], 33 | status: itemInfo.status, 34 | localisation: itemInfo.user.country_title_local, 35 | title: itemInfo.title, 36 | description: itemInfo.description, 37 | package_size: itemInfo.package_size_id, 38 | created_at_ts: articleTimeDescription, 39 | favourite_count: itemInfo.favourite_count, 40 | view_count: itemInfo.view_count, 41 | pricingInfos: [ 42 | { 43 | price: itemInfo.price, 44 | currency: itemInfo.currency, 45 | service_fee: itemInfo.service_fee, 46 | total_item_price: itemInfo.total_item_price, 47 | } 48 | ], 49 | images: itemInfo.photos.map(photo => ({ 50 | isMain: photo.is_main, 51 | url: photo.url 52 | })) 53 | }); 54 | await browser.close(); 55 | return ItemsData; 56 | } catch (error) { 57 | console.log(error); 58 | 59 | return error; 60 | } 61 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vinted API 🛍️ 2 | ![Vinted LOGO](https://github.com/MrBalourd/Vinted-API/blob/main/github/vinted-app.jpg) 3 | 4 | Welcome to the Vinted API! This API allows you to interact with Vinted's data to search for items, retrieve member information, and more. 💼 5 | 6 | ## Planned Functionalities ⚙️ 7 | 8 | In addition to the current endpoints, I'm planning to add the following exciting functionalities to the Vinted API: 9 | 10 | - **Authentication Actions** 🔐: Integrate authentication mechanisms to allow users to log in, ensuring secure access to personalized features. 11 | 12 | - **Write Messages** ✉️: Implement the ability for users to send messages to each other, enhancing communication within the platform. 13 | 14 | - **Add an Item** 🛍️: Enable users to list their items for sale by adding new items to the platform. 15 | 16 | - **Like Item** ❤️: Implement a feature that allows users to "like" or save items they are interested in. 17 | 18 | - **Modify Rapidly an Item** 🚀: Provide a quick and easy way for users to make updates to their listed items. 19 | 20 | - **Push Notifications** 📬: Set up push notifications to alert users when a new article is posted that matches their alerted research criteria. 21 | 22 | These upcoming features will enhance the overall user experience and make the Vinted platform even more dynamic and engaging. 23 | 24 | ## Table of Contents 📚 25 | 26 | - [Introduction](#introduction) 27 | - [Endpoints](#endpoints) 28 | - [Usage](#usage) 29 | - [Installation](#installation) 30 | - [Contributing](#contributing) 31 | - [License](#license) 32 | 33 | ## Introduction 🌟 34 | 35 | The Vinted API is built using Express.js and provides various endpoints to access Vinted's data. It supports searching for items, retrieving member information, fetching member items, and more. 36 | 37 | ## Endpoints 🚀 38 | 39 | ### Search Items 🔍 40 | 41 | Endpoint: `POST /api/items` 42 | 43 | Search for items based on specific criteria like text, currency, and order. 44 | 45 | **Example Request:** 46 | ```bash 47 | curl -X POST https://vinted-api.com/api/items -H "Content-Type: application/json" -d '{ 48 | "text": "summer dress", 49 | "currency": "USD", 50 | "order": "price" 51 | }' 52 | ``` 53 | 54 | ### Get Item Info 📦 55 | 56 | Endpoint: `POST /api/item` 57 | 58 | Retrieve detailed information about a specific item using its identifier. 59 | 60 | **Example Request:** 61 | ```bash 62 | curl -X POST https://vinted-api.com/api/item -H "Content-Type: application/json" -d '{ 63 | "item": "item_id_here" 64 | }' 65 | ``` 66 | 67 | ### Search Members 👥 68 | 69 | Endpoint: `GET /api/members/search` 70 | 71 | Search for members using a query string to find member profiles. 72 | 73 | **Example Request:** 74 | ```bash 75 | curl -X GET "https://vinted-api.com/api/members/search?query=john_doe" 76 | ``` 77 | 78 | ### Get Member Items 📂 79 | 80 | Endpoint: `GET /api/member/:id/items` 81 | 82 | Retrieve items listed by a specific member using their ID. 83 | 84 | **Example Request:** 85 | ```bash 86 | curl -X GET "https://vinted-api.com/api/member/member_id_here/items?page=1" 87 | ``` 88 | 89 | ### Get Member Info 👤 90 | 91 | Endpoint: `GET /api/member/:memberId` 92 | 93 | Retrieve detailed information about a specific member using their ID. 94 | 95 | **Example Request:** 96 | ```bash 97 | curl -X GET "https://vinted-api.com/api/member/member_id_here" 98 | ``` 99 | 100 | ### Get Member Rates ⭐ 101 | 102 | Endpoint: `GET /api/member/:memberId/rate` 103 | 104 | Retrieve ratings and feedback for a specific member using their ID. 105 | 106 | **Example Request:** 107 | ```bash 108 | curl -X GET "https://vinted-api.com/api/member/member_id_here/rate" 109 | ``` 110 | 111 | ### Test Endpoint 🧪 112 | 113 | Endpoint: `GET /1` 114 | 115 | A test endpoint that returns "1". 116 | 117 | ## Usage 🛠️ 118 | 119 | 1. Install the required dependencies using `npm install`. 120 | 2. Start the server using `npm start`. 121 | 3. Access the API using the provided endpoints. 122 | 123 | ## Installation ⚙️ 124 | 125 | 1. Clone the repository: `git clone https://github.com/your-username/vinted-api.git` 126 | 2. Navigate to the project directory: `cd vinted-api` 127 | 3. Install dependencies: `npm install` 128 | 4. Start the server: `npm start` 129 | 130 | ## Contributing 🤝 131 | 132 | Contributions are welcome! If you find any issues or want to add enhancements, feel free to open a pull request. 133 | 134 | ## License 📜 135 | 136 | This project is licensed under the [MIT License](LICENSE). Feel free to use, modify, and distribute the code as needed. 137 | 138 | --- 139 | 140 | Feel free to explore the various endpoints and integrate this API with your projects. If you have any questions or feedback, don't hesitate to reach out! 💌 141 | --------------------------------------------------------------------------------