Export all videos of a TikTok profile to CSV (dates, views, links)

Updated October 22, 2026

Get a list of every video a TikTok profile has posted (exact upload date, views, caption and link) as CSV or text. Free, runs in your browser, works on any public profile.

Works on your own profile and on any public one, on tiktok.com in a desktop browser.

export-profile-videos.js · 128 linespaste in the DevTools console on www.tiktok.com
// Responsive Muse · TikTok Profile Video Exporter · v1.0 · 2026-10-22
// Saves every video of a TikTok profile (yours or any public account) to CSV or TXT: upload
// date, views, caption, pinned and link. The exact upload date comes from the video's own ID,
// so nothing extra is loaded. Runs only on a tiktok.com/@profile page; TikTok loads older videos
// while the script scrolls. It makes no network requests of its own and never reads cookies,
// passwords or tokens.
(async () => {
  const TOOL = 'Export TikTok videos';
  const FORMAT = 'csv';        // 'csv' (Excel, Sheets) or 'txt' (one link per line)
  const LIMIT = 0;             // stop after this many videos (0 = all)

  if (!/(^|\.)tiktok\.com$/.test(location.hostname) || !/^\/@[^/]+\/?$/.test(location.pathname)) {
    alert('Open a TikTok profile first, for example:\nhttps://www.tiktok.com/@nasa');
    return;
  }
  // A small box on the page shows progress and asks before changing anything,
  // so you never need to read or edit the console.
  const many = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;   // 1 video, 2 videos
  const panel = (() => {
    const box = document.createElement('div');
    box.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483647;width:340px;max-width:calc(100vw - 32px);' +
      'background:#111318;color:#e6e8ee;font:14px/1.5 system-ui,sans-serif;border-radius:12px;padding:16px;' +
      'box-shadow:0 8px 30px rgba(0,0,0,.4);text-align:left';
    const head = document.createElement('strong');
    head.textContent = TOOL;
    const msg = document.createElement('p');
    msg.style.cssText = 'margin:8px 0 12px;white-space:pre-line';
    const row = document.createElement('div');
    row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end';
    box.append(head, msg, row);
    document.body.append(box);
    const button = (label, primary, onClick) => {
      const b = document.createElement('button');
      b.textContent = label;
      b.style.cssText = 'font:600 14px system-ui,sans-serif;padding:8px 14px;border-radius:8px;cursor:pointer;' +
        (primary ? 'background:#0f766e;color:#fff;border:1px solid #0f766e' : 'background:none;color:#e6e8ee;border:1px solid #3a3f4b');
      b.onclick = onClick;
      row.append(b);
    };
    return {
      text(t) { msg.textContent = t; },
      ask(t, yes) {
        msg.textContent = t;
        row.replaceChildren();
        return new Promise(r => {
          button('Cancel', false, () => { row.replaceChildren(); r(false); });
          button(yes, true, () => { row.replaceChildren(); r(true); });
        });
      },
      stoppable(onStop) { row.replaceChildren(); button('Stop', false, onStop); },
      actions(t, list) {
        msg.textContent = t;
        row.replaceChildren();
        list.forEach(([label, primary, run]) => button(label, primary, run));
      },
      finish(t) { this.actions(t, [['Close', true, () => box.remove()]]); },
      close() { box.remove(); },
    };
  })();
  // Offer the result as a downloaded file or as clipboard text (a real click, so it always works)
  const deliver = (summary, body, file) => {
    const save = () => {
      const a = document.createElement('a');
      a.href = URL.createObjectURL(new Blob([/\.csv$/.test(file) ? '\ufeff' + body : body], { type: 'text/plain;charset=utf-8' }));
      a.download = file;
      a.click();
      setTimeout(() => URL.revokeObjectURL(a.href), 5000);
      panel.text(`${summary}\nSaved as ${file} (check your Downloads).`);
    };
    const copy = () => navigator.clipboard.writeText(body)
      .then(() => panel.text(`${summary}\nCopied. Paste it into a spreadsheet or a note.`))
      .catch(() => panel.text(`${summary}\nThe browser blocked the clipboard: use Download instead.`));
    panel.actions(summary, [['Close', false, () => panel.close()], ['Copy', false, copy], ['Download', true, save]]);
  };
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const videos = new Map();
  // A TikTok ID starts with the upload time: the first 32 bits are seconds since 1970
  const dateOf = id => new Date(Number(BigInt(id) >> 32n) * 1000).toISOString().slice(0, 16).replace('T', ' ');
  // The thumbnail's description ends with "created by NAME with SOUND" (in the page's language)
  const caption = alt => alt.replace(/\s+(created by|creado por|criado por|créé par|erstellt von|creato da|gemaakt door)\s.*$/i, '').trim();

  const collect = () => {
    for (const item of document.querySelectorAll('[data-e2e="user-post-item"]')) {
      const a = item.querySelector('a[href*="/video/"], a[href*="/photo/"]');
      const id = (a?.getAttribute('href').match(/\/(?:video|photo)\/(\d+)/) || [])[1];
      if (!id || videos.has(id)) continue;
      videos.set(id, {
        date: dateOf(id),
        views: (item.querySelector('[data-e2e="video-views"]')?.textContent || '').trim(),
        pinned: item.querySelector('[data-e2e="video-card-badge"]') ? 'yes' : '',
        type: a.getAttribute('href').includes('/photo/') ? 'photo' : 'video',
        caption: caption(item.querySelector('img[alt]')?.alt || ''),
        url: a.href,
      });
      if (LIMIT && videos.size >= LIMIT) return;
    }
  };

  // 1. Scroll to the end, collecting as we go, until nothing new appears four times in a row
  let stop = false;
  panel.stoppable(() => { stop = true; });
  let idle = 0;
  while (idle < 4 && !stop && !(LIMIT && videos.size >= LIMIT)) {
    const before = videos.size;
    collect();
    window.scrollTo(0, document.documentElement.scrollHeight);
    await sleep(1800);
    idle = videos.size === before ? idle + 1 : 0;
    panel.text(`Collecting videos… ${videos.size}`);
  }
  const list = [...videos.values()];
  if (!list.length) {
    panel.finish('No videos found. Is the profile private, or is TikTok asking you to sign in or solve a puzzle? If not, please report it on responsive-muse.com.');
    return 0;
  }

  // 2. Build the file and offer it (newest first, pinned videos in their real place)
  list.sort((x, y) => y.date.localeCompare(x.date));
  const cell = v => '"' + String(v).replace(/"/g, '""') + '"';
  const body = FORMAT === 'txt'
    ? list.map(v => v.url).join('\n')
    : ['Uploaded (UTC),Views,Pinned,Type,Caption,URL', ...list.map(v =>
        [v.date, v.views, v.pinned, v.type, v.caption, v.url].map(cell).join(','))].join('\n');
  const handle = location.pathname.replace(/\W+/g, '');
  deliver(`${stop ? 'Stopped early. ' : ''}Found ${many(list.length, 'video')}.`, body, `tiktok-${handle}.${FORMAT}`);
  return list.length;
})();

Bookmarklet: drag the middle button to your bookmarks bar, open www.tiktok.com and click the bookmark. It runs the same script with the options above.

Why it's safe to runAutomated check passed Oct 22, 2026 · tested on a real account Oct 22, 2026
  • Sends nothing anywhere. No fetch, XHR or external scripts. The only network activity is www.tiktok.com itself loading your list.
  • Never touches your login. It doesn't read cookies, passwords, tokens or browser storage.
  • Only runs on www.tiktok.com. Anywhere else it stops before doing anything.
  • Short and readable. 128 commented lines, not minified or obfuscated.
  • Shows its progress on the page. A small box tells you what it's doing and offers Download or Copy at the end.
  • Same code for everyone. SHA-256 of the default version: 86b12ed3b6708957…
Verify it yourself in 30 seconds
  1. Read it: every line is above, with comments.
  2. Search the code for fetch, XMLHttpRequest, cookie, localStorage or eval. None of them appear.
  3. Open the Network tab before running it: you'll only see requests to www.tiktok.com.
Did it work for you?

How to use it

  1. Open the profile on tiktok.com in a desktop browser, for example tiktok.com/@nasa.
  2. Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type allow pasting and press Enter.
  3. Pick a format, copy the script, paste it in the console and press Enter. It scrolls the whole profile, then shows a box: click Download (or Copy).

What changed since our 2023 snippet

Our first version of this page gave you three snippets to paste one after another. TikTok has since removed the caption element they relied on, so they stopped working. The new script does it all in one go, and adds things the old one couldn’t: the exact upload date and time of each video (read from its ID), the view count, photo posts and which video is pinned. In our test it exported all 58 videos of NASA’s profile.

FAQ

Where does the exact upload date come from?

From the video ID itself: TikTok builds every ID from the moment the video was posted, so the script can read the date and time (UTC) without opening each video.

Why are some captions cut?

The profile grid only carries the start of long captions. Open the video if you need the full text.

Can I export likes or comments?

Not with this tool: it lists the videos of a profile. Views are the number TikTok shows on each thumbnail (for example 1.7M).

TikTok asked me to solve a puzzle

TikTok sometimes checks for bots while you scroll very long profiles. Solve it, then run the script again: it starts over and collects everything that is loaded.

More free tools