π Unofficial, dependency-free TypeScript client for the (reverse-engineered) private API of tutti.ch: search & filters, listings, seller profiles, categories, suggestions, live messaging, and Auth0 login.
β οΈ Not affiliated with tutti.ch. Reverse-engineered for interoperability and research. Respect tutti.ch's terms of service and rate limits β use at your own risk.
npm install tutti-api
Requires Node 18+ (global fetch) or a browser.
import { TuttiClient } from "tutti-api";
const client = new TuttiClient(); // anonymous; random device hash
// Fluent search with filters
const result = await client
.search("ledersofa")
.category("furniture")
.price({ min: 100, max: 5000 }) // or .freeOnly()
.location(locality) // from client.localities.search()
.select("companyAd", "private") // generic single-select
.multiSelect("language", ["de"]) // generic multi-select
.interval("year", { min: 2015 }) // generic numeric range
.sort("timestamp", "desc")
.fetch();
result.totalCount; // number
result.listings; // Listing[] (this page)
result.availableFilters; // filter names/options for this category
await result.next(); // next page (or null)
for await (const l of result.paginate()) {
/* every listing across pages */
}
// Listing detail + locality autocomplete + token browse
const listing = await client.listings.get("81078697");
const locs = await client.localities.search("zΓΌr");
const page = await client.browse(searchToken).fetch();
// Filters for a category without fetching a listings page
const { availableFilters } = await client.search().category("cars").updateFilters();
// Categories, featured, seller profiles, autocomplete
await client.categories.tree();
await client.categories.featured();
await client.profiles.get(publicAccountID);
await client.profiles.listings(publicAccountID, { offset: 0, size: 30 });
await client.suggestions.search("sof");
tutti uses Auth0, then exchanges the JWT for a tutti session token sent as X-Tutti-Auth (~1-year validity).
import { LLMCaptchaProvider, ManualCaptchaProvider, Session } from "tutti-api";
// Full login. The Auth0 page has a captcha, solved by a swappable provider:
// ManualCaptchaProvider (default) β saves the image, you type the text
// LLMCaptchaProvider β Google Gemini vision (needs GEMINI_API_KEY)
await client.account.login({ username, password, captcha: new LLMCaptchaProvider() });
// Or bring your own token / Auth0 access token:
client.account.useToken("mc1xβ¦");
await client.account.authenticateJWT(auth0AccessToken);
Session.toJSON()/fromJSON() give a plain snapshot; a SessionStore persists it under a key. Providers are interchangeable β InMemorySessionStore and FileSessionStore ship; add Redis/DB by implementing save / load / delete / keys.
import { FileSessionStore, Session, TuttiClient } from "tutti-api";
const store = new FileSessionStore("./sessions");
await store.save("alice", client.session.toJSON());
const snap = await store.load("alice"); // restore later, no re-login
const restored = new TuttiClient({ session: snap ? Session.fromJSON(snap) : undefined });
Live chat is streamed as NDJSON over a long-lived request, exposed as async iterators (backlog first, then live). Requires an authenticated session.
const ac = new AbortController();
for await (const m of client.messaging.streamMessages(convId, { signal: ac.signal })) {
const mine = m.senderPublicAccountId === client.session.auth?.accountId;
console.log(mine ? "β" : "β", m.content.text);
}
// ac.abort() to stop
await client.messaging.send(convId, "Hello!");
await client.messaging.markRead(convId, offset);
await client.messaging.reply({ itemId, name, email, body }); // start a chat from a listing
Store a session once; every demo loads it from ./.tutti-sessions (gitignored):
# 1) store a session (token fast-path, or full login)
TUTTI_TOKEN=<X-Tutti-Auth> npm run demo:session
# or: GEMINI_API_KEY=β¦ TUTTI_USER=β¦ TUTTI_PASS=β¦ npm run demo:session
# 2) the rest load it (anonymous fallback if none)
npm run demo # search "ubiquiti" + pagination
npm run demo:queries # categories, featured, suggestions, updateFilters, profiles
npm run demo:messages # live conversation + message streams (needs auth)
npm run build # bundle ESM + CJS + .d.ts into dist/
npm run typecheck # tsc --noEmit
npm run check # Biome lint + format (write)
npm run docs # generate API docs (TypeDoc) into docs/
price/location/interval/select payloads against live traffic.useToken() skips it. auth.tutti.ch is behind Cloudflare.new TuttiClient({ app: { β¦ } }).π€ Filippo Finke
Contributions, issues and feature requests are welcome!
Feel free to check the issues page. Commits follow Conventional Commits; releases are automated with release-please β merged commits accumulate into a Release PR that, once merged, tags the version and publishes to npm.
Give a βοΈ if this project helped you!
Copyright Β© 2026 Filippo Finke.
This project is MIT licensed.
Reverse-engineered for educational purposes β not affiliated with tutti.ch.