Export Twitter data to Google Sheets
Google Sheets can't pull Twitter data by itself, so tweets and followers reach a sheet through an API called from Apps Script on a timer; one spreadsheet holds up to 20 million cells, about 2 million tweets.
Short answer: Every route to a sheet runs on an API that returns JSON. The official X API bills $0.005 per post and $0.010 per follower and needs a developer app. For Google Sheets, use Sorsa API from Apps Script: one ApiKey header, 20 tweets or 200 followers per request, from $0.007 per 1,000 followers, and 100 free requests to start.
- Endpoint
POST /search-tweets- Page size
- 20 tweets per request
- Price
- from $0.07 per 1,000 tweets
#How Twitter data gets into Google Sheets
Twitter data gets into Google Sheets in one of four ways, and all four need a data source that returns JSON: an Apps Script that calls an API on a timer, a connector add-on that sends a URL with a header from inside the sheet, an automation tool such as n8n, Make or Zapier with a Google Sheets step, or a CSV file you download and bring in with File > Import. For a one-off file, the free Export Tweets from Twitter to CSV tool writes the CSV for you.
Formulas don't count as a fifth way. IMPORTXML and IMPORTHTML read a single web page, and x.com shows no follower list or search to a visitor who isn't signed in, so a formula has nothing to page through, and it breaks whenever x.com changes its layout.
| Route | What you set up | Refreshes by itself | Pages through results |
|---|---|---|---|
| Apps Script (this guide) | One script, pasted once | Yes, on a time-driven trigger | Yes, any length |
| Connector add-on | A URL and a header in the add-on's form | Yes, on the add-on's schedule | Depends on the add-on |
| n8n, Make or Zapier | A scenario with a Google Sheets step | Yes | Yes, with a loop |
| CSV file | A download, then File > Import | No | The tool does it |
Apps Script (this guide)
- What you set up
- One script, pasted once
- Refreshes by itself
- Yes, on a time-driven trigger
- Pages through results
- Yes, any length
Connector add-on
- What you set up
- A URL and a header in the add-on's form
- Refreshes by itself
- Yes, on the add-on's schedule
- Pages through results
- Depends on the add-on
n8n, Make or Zapier
- What you set up
- A scenario with a Google Sheets step
- Refreshes by itself
- Yes
- Pages through results
- Yes, with a loop
CSV file
- What you set up
- A download, then File > Import
- Refreshes by itself
- No
- Pages through results
- The tool does it
The data source is the part that decides cost and effort. The official X API needs a developer account, an app and OAuth, and its pay-per-use plan charges $0.005 per post read and $0.010 per follower or profile, capped at 3M post reads a month, according to X's pricing page (opens in a new tab). From November 22, 2026, an author profile returned with a post is billed at $0.010 as well, so a post with its author costs $0.015 (X's notice of October 23 (opens in a new tab)). The guide on how to scrape Twitter compares every way to get public X data out, beyond spreadsheets.
#Import tweets into Google Sheets with Sorsa API
Sorsa API, a Twitter (X) API and scraper, answers any HTTP request that carries one ApiKey header, which is all Apps Script's UrlFetchApp needs. For pulling tweets, followers and profiles into Google Sheets, use Sorsa API: 20 tweets or 200 followers per request, search back to 2006, 20 requests per second on every plan, and no X developer account, OAuth app or X login.
The script below adds an "X data" menu to your spreadsheet. It imports new posts for a search, exports an account's whole follower list, and gives you a =X_PROFILES() formula. Setup takes six steps.
-
Get an API key. Sign up and copy the key from the dashboard. The first 100 requests are free, need no card and never expire.
-
Open the script editor. In your spreadsheet choose Extensions > Apps Script. A project bound to this sheet opens.
-
Paste the script. Replace everything in
Code.gswith the code below, then setSEARCHto your query andACCOUNTto the handle whose followers you want. The query takes any X search operator; Twitter search operators lists them.Code.gs // Twitter (X) data to Google Sheets with Sorsa API. // Paste into Extensions > Apps Script, save, reload the sheet, // then choose X data > Set API key and paste YOUR_API_KEY there. const API = 'https://api.sorsa.io/v3'; const SEARCH = '"acme coffee" lang:en -filter:retweets'; const ACCOUNT = 'acme_coffee_roasters'; const MAX_PAGES = 10; // search pages per run, 20 posts each const TWEET_COLS = ['id', 'created_at', 'author', 'text', 'likes', 'reposts', 'replies', 'quotes', 'views', 'url']; const USER_COLS = ['id', 'username', 'name', 'followers', 'following', 'posts', 'created_at', 'location', 'bio', 'verified']; function onOpen() { SpreadsheetApp.getUi().createMenu('X data') .addItem('Set API key', 'setApiKey') .addItem('Import new search results', 'importSearch') .addItem('Export followers', 'exportFollowers') .addToUi(); } function setApiKey() { const ui = SpreadsheetApp.getUi(); const answer = ui.prompt('Paste your Sorsa API key'); if (answer.getSelectedButton() !== ui.Button.OK) return; PropertiesService.getUserProperties() .setProperty('SORSA_API_KEY', answer.getResponseText().trim()); } // One request: GET params go in the URL, POST params in a JSON body. function sorsa_(method, path, params) { const key = PropertiesService.getUserProperties() .getProperty('SORSA_API_KEY'); if (!key) throw new Error('Run X data > Set API key first.'); const options = { method: method, headers: { ApiKey: key }, muteHttpExceptions: true }; let url = API + path; if (method === 'get') { url += '?' + Object.keys(params).map((k) => [].concat(params[k]) .map((v) => k + '=' + encodeURIComponent(v)).join('&')).join('&'); } else { options.contentType = 'application/json'; options.payload = JSON.stringify(params); } for (let attempt = 1; attempt <= 3; attempt++) { const res = UrlFetchApp.fetch(url, options); const code = res.getResponseCode(); if (code === 200) return JSON.parse(res.getContentText()); if (code !== 429) throw new Error(code + ' ' + res.getContentText()); Utilities.sleep(1000 * attempt); // over 20 requests a second } throw new Error('429 three times in a row'); } function sheet_(name, cols) { const book = SpreadsheetApp.getActiveSpreadsheet(); const sh = book.getSheetByName(name) || book.insertSheet(name); if (sh.getLastRow() === 0) { sh.getRange('A:A').setNumberFormat('@'); // 19-digit IDs stay text sh.appendRow(cols); sh.setFrozenRows(1); } return sh; } function append_(sh, rows) { if (!rows.length) return; sh.getRange(sh.getLastRow() + 1, 1, rows.length, rows[0].length) .setValues(rows); } // Text that starts with = + or - would be read as a formula. function safe_(s) { s = s || ''; return /^[=+-]/.test(s) ? "'" + s : s; } function tweetRow_(t) { const u = t.user || {}; return [t.id, new Date(t.created_at), '@' + u.username, safe_(t.full_text), t.likes_count || 0, t.retweet_count || 0, t.reply_count || 0, t.quote_count || 0, t.view_count || 0, 'https://x.com/' + u.username + '/status/' + t.id]; } function userRow_(u) { return [u.id, u.username, safe_(u.display_name), u.followers_count, u.followings_count, u.tweets_count, new Date(u.created_at), safe_(u.location), safe_(u.description), u.verified]; } // New posts for SEARCH, newest first, until the first ID already in // the sheet. They are appended oldest first, so the sheet reads as a log. function importSearch() { const sh = sheet_('Search', TWEET_COLS); const last = sh.getLastRow(); const seen = new Set(last > 1 ? sh.getRange(2, 1, last - 1, 1).getDisplayValues().flat() : []); const rows = []; let cursor = null; pages: for (let page = 0; page < MAX_PAGES; page++) { const body = { query: SEARCH, order: 'latest' }; if (cursor) body.next_cursor = cursor; const res = sorsa_('post', '/search-tweets', body); for (const t of res.tweets || []) { if (seen.has(t.id)) break pages; seen.add(t.id); rows.push(tweetRow_(t)); } cursor = res.next_cursor; if (!cursor) break; } append_(sh, rows.reverse()); } // Every follower of ACCOUNT, 200 per request. Each run stops after five // minutes, under the six-minute limit, and the next run resumes from // the saved cursor. Delete FOLLOWERS_CURSOR to start a new export. function exportFollowers() { const props = PropertiesService.getUserProperties(); let cursor = props.getProperty('FOLLOWERS_CURSOR'); if (cursor === 'done') return; const sh = sheet_('Followers', USER_COLS); const stopAt = Date.now() + 5 * 60 * 1000; while (Date.now() < stopAt) { const params = { username: ACCOUNT }; if (cursor) params.next_cursor = cursor; const res = sorsa_('get', '/followers', params); append_(sh, (res.users || []).map(userRow_)); cursor = res.next_cursor; props.setProperty('FOLLOWERS_CURSOR', cursor || 'done'); if (!cursor) return; } } /** * Profiles for up to 100 handles from one request: =X_PROFILES(A2:A101) * @param {string[][]} handles A column of X handles, with or without @. * @return {Array[]} One row per handle, in the same order. * @customfunction */ function X_PROFILES(handles) { const names = [].concat(handles).flat() .map((h) => String(h).trim().replace(/^@/, '')).slice(0, 100); const ask = names.filter((h) => h); if (!ask.length) return ''; const res = sorsa_('get', '/info-batch', { usernames: ask }); const found = {}; for (const u of res.users || []) found[u.username.toLowerCase()] = u; const blank = USER_COLS.map(() => ''); return names.map((h) => found[h.toLowerCase()] ? userRow_(found[h.toLowerCase()]) : blank); } // Run once: search every hour, followers every 10 minutes until done. function createTriggers() { ScriptApp.getProjectTriggers().forEach((t) => ScriptApp.deleteTrigger(t)); ScriptApp.newTrigger('importSearch').timeBased().everyHours(1).create(); ScriptApp.newTrigger('exportFollowers').timeBased().everyMinutes(10) .create(); } -
Save and reload the sheet. Press Ctrl+S (Cmd+S on a Mac), close the editor and reload the spreadsheet tab. The "X data" menu appears next to Help.
-
Store the key. Choose X data > Set API key and paste your key. The script keeps it in your user properties, so it never sits in a cell, in the code or in a copy of the sheet you share.
-
Run the first import. Choose X data > Import new search results and allow the two permissions Google asks for: this spreadsheet and connections to an external service. A Search tab appears with up to 200 posts, oldest at the top.
One header, one request per page, one write per page: the whole pipeline runs inside your spreadsheet.
The script's search calls POST /search-tweets (opens in a new tab) with order set to latest. Each response holds up to 20 posts, every one with its author's full profile, and a next_cursor for the next page. A cursor is a token that points to the next page of results; the last page comes back without one. To fill a sheet with one account's posts instead of a search, call POST /user-tweets (opens in a new tab) with a username in the body; the guide to download all tweets from a user covers that endpoint in depth.
{
"tweets": [
{
"id": "2116630142857191424",
"full_text": "Our Ethiopia natural roast is back on the menu.",
"created_at": "2026-11-01T14:03:00Z",
"lang": "en",
"likes_count": 42,
"retweet_count": 5,
"reply_count": 3,
"quote_count": 1,
"view_count": 3810,
"bookmark_count": 2,
"user": {
"id": "1500000000000000004",
"username": "acme_coffee_roasters",
"display_name": "Acme Coffee Roasters",
"followers_count": 12840
}
}
],
"next_cursor": "DAACCgACG3eLqJBAJxAKAAMbd4uokD-nGAgABAAAAAIAAA"
}
#What lands in each column
Each post becomes one row of ten columns, and three of them need care that a plain copy of the JSON would get wrong. The table maps every column to its field in the response.
| Column | Field in the response | What the script does |
|---|---|---|
| id | id | Writes it into a column formatted as plain text |
| created_at | created_at | Turns the ISO 8601 string into a real date |
| author | user.username | Adds the @ |
| text | full_text | Adds an apostrophe before a leading =, + or - |
| likes, reposts, replies, quotes, views | likes_count, retweet_count, reply_count, quote_count, view_count | Writes 0 where a count is missing |
| url | built from user.username and id | Links to the post on x.com |
id
- Field in the response
id- What the script does
- Writes it into a column formatted as plain text
created_at
- Field in the response
created_at- What the script does
- Turns the ISO 8601 string into a real date
author
- Field in the response
user.username- What the script does
- Adds the @
text
- Field in the response
full_text- What the script does
- Adds an apostrophe before a leading =, + or -
likes, reposts, replies, quotes, views
- Field in the response
likes_count,retweet_count,reply_count,quote_count,view_count- What the script does
- Writes 0 where a count is missing
url
- Field in the response
- built from
user.usernameandid - What the script does
- Links to the post on x.com
The three traps, each one a known cause of broken sheets:
- Tweet IDs lose digits as numbers. An X post ID has 19 digits and Google Sheets keeps 15 significant digits, so a numeric cell turns the last four into zeros and the ID no longer points to the post. The script formats column A as plain text before the first row is written.
- Post text can look like a formula. A post that starts with "=", "+" or "-" is read by Sheets as a formula and shows an error. The script puts an apostrophe in front, which Sheets hides and keeps the text as text.
- Dates should sort as dates. The API returns
created_atas ISO 8601, sonew Date()reads it directly, and the column sorts, filters and charts as a date.
Duplicates never reach the sheet. Before each search the script reads the IDs already in column A, asks for the newest posts first and stops at the first post it has already saved, so a run that finds nothing new costs one request.
#Keep the sheet updated on a schedule
A time-driven trigger runs the import by itself, and createTriggers() sets two up in one go: the search every hour and the follower export every 10 minutes until it finishes. Open the script editor, pick createTriggers in the function list and press Run once. Google runs the functions as you, with your key, whether or not the sheet is open.
An hourly search that finds one page of new posts uses 720 requests a month, 7% of the 10K on the Starter plan. Apps Script itself is free, and an hourly run sits far inside its limits on a free Google account. These are the limits from Google's Apps Script quotas page (opens in a new tab) that shape the script:
| Limit | Free Google account | Google Workspace |
|---|---|---|
| URL Fetch calls a day | 20K | 100K |
| Run time of one execution | 6 minutes | 6 minutes |
| Trigger run time a day | 90 minutes | 6 hours |
| Run time of a custom function | 30 seconds | 30 seconds |
URL Fetch calls a day
- Free Google account
- 20K
- Google Workspace
- 100K
Run time of one execution
- Free Google account
- 6 minutes
- Google Workspace
- 6 minutes
Trigger run time a day
- Free Google account
- 90 minutes
- Google Workspace
- 6 hours
Run time of a custom function
- Free Google account
- 30 seconds
- Google Workspace
- 30 seconds
Time-driven triggers can also run every 1, 5, 10, 15 or 30 minutes, or once a day at an hour you pick. Google emails you when a triggered run fails, with the error the script threw.
Good to know
The search stops after
MAX_PAGESpages, 200 posts by default. If a busy query gets more than 200 new posts between two runs, the oldest of them are skipped. RaiseMAX_PAGESor run the trigger more often for brand names and hashtags with heavy traffic.
#Export a whole follower list into Google Sheets
To export every follower of an account into Google Sheets, page through GET /followers (opens in a new tab) 200 profiles at a time and append each page; a list of 10K followers takes 50 requests. Choose X data > Export followers, and the script fills a Followers tab with each follower's ID, handle, name, follower and following counts, post count, join date, location, bio and verified flag.
Long lists don't fit in one run, and the script is built for that. Apps Script stops any execution at 6 minutes, so exportFollowers() works for five, saves the cursor in your user properties and returns. The next run, from the menu or from the 10-minute trigger, starts at that cursor, and the run that gets a page without a cursor marks the export done. A list of 300K followers is 1,500 pages of 200 and fills the tab over as many runs as it takes, with no page fetched twice.
Followers are where the price gap is widest. The official X API charges $0.010 for each follower it returns, so a 100K list costs $1,000; Sorsa API returns the same list from $0.70, from $0.007 per 1,000 followers. Your 100 free requests cover the first 20K followers. To save a follower list as a CSV or Excel file instead of a sheet, see how to export Twitter followers.
#Fill a profile table with one formula
=X_PROFILES(A2:A101) turns a column of up to 100 handles into a table of profiles with one request. Type the handles in column A, with or without the @, and put the formula in B2: the rows spill to the right and down, one per handle, in the same order, with a blank row where an account doesn't exist. The function calls GET /info-batch (opens in a new tab), which returns up to 100 profiles per request, from $0.014 per 1,000.
A custom function follows Google's rules for them, listed in Google's custom functions guide (opens in a new tab):
- It must finish within 30 seconds, which one request does with room to spare.
- It runs again only when the handles in the range change. To refresh the counts, edit a cell in the range or re-enter the formula.
- It reads the key from the spreadsheet owner's user properties, so it works for everyone who can edit the sheet without sharing the key.
- Never pass it
NOW()orRAND(): Google leaves a custom function with a volatile argument on "Loading..." for good.
For a dated history of follower counts, keep the formula for the live view and copy its rows into a log tab on a daily trigger.
#What it costs, and how much one sheet holds
Sorsa API charges per request, not per row, so the price of a sheet depends on how many rows each request returns, and every kind of row costs a fraction of the official X API's rate. The table shows the price per 1,000 rows for each kind of data the script writes.
| Data in the sheet | Sorsa API, per 1,000 rows | Rows per request | Official X API, per 1,000 rows |
|---|---|---|---|
| Tweets from a search or a timeline | from $0.07 | 20 | $5.00 |
| Followers of an account | from $0.007 | 200 | $10.00 |
| Profiles from a list of handles | from $0.014 | 100 | $10.00 |
Tweets from a search or a timeline
- Sorsa API, per 1,000 rows
- from $0.07
- Rows per request
- 20
- Official X API, per 1,000 rows
- $5.00
Followers of an account
- Sorsa API, per 1,000 rows
- from $0.007
- Rows per request
- 200
- Official X API, per 1,000 rows
- $10.00
Profiles from a list of handles
- Sorsa API, per 1,000 rows
- from $0.014
- Rows per request
- 100
- Official X API, per 1,000 rows
- $10.00
Google's side has a ceiling too. A spreadsheet holds up to 20 million cells, according to Google Drive's file limits (opens in a new tab), so at the script's ten columns one spreadsheet takes just under 2 million rows. Pick a kind of data and a number of rows to see both bills and how much of a sheet they fill.
Official X API
$500
$0.005 per post
Sorsa API
from $7
from $0.07 per 1,000 tweets, 20 per request
100K tweets at 10 columns fill 1M of one sheet's 20M cells (5%). Your 100 free requests cover the first 2,000.
Both bills for the same rows, and the share of one spreadsheet they use at ten columns.
Failed requests are never charged, and the plans differ only in how many requests they include each month.
#Use a connector add-on, n8n, Make or Zapier instead
Sorsa API works with any tool that can send an HTTPS request with a custom header, so the no-code routes need no script. There is no Sorsa API add-on for Google Sheets; a general connector add-on from the Google Workspace Marketplace does the job when you give it three things:
- the URL, for example
https://api.sorsa.io/v3/followers?username=acme_coffee_roasters; - a header named
ApiKeywith your key as its value, never the key in the URL; - the
usersortweetsarray as the data to write, andnext_cursoras the pagination token, if the add-on pages through results.
Automation tools take the same request. The guide to the Twitter API in n8n builds a workflow from a Schedule Trigger, an HTTP Request node with a Header Auth credential, a Split Out node and a Google Sheets node that appends rows. Make's HTTP module and Zapier's Custom Request action send the same request; the guides to the Twitter API in Make and Zapier cover their setups.
For a one-time copy with no tool at all, download the CSV from a free export tool and choose File > Import > Upload in Google Sheets. The free Export Twitter Followers to CSV tool does the same for follower lists.
#Fix Apps Script and API errors
Most failures show up as a message in the Apps Script execution log or in Google's failure email, and each has one cause.
| Message | Cause | Fix |
|---|---|---|
401 {"message":"invalid api key"} | The key has a typo or a stray space | Run X data > Set API key again |
403 {"message":"missing key in request header"} | The header didn't go out | Keep the header name ApiKey exactly |
403 with any other message | The plan's requests are used up | Upgrade or switch on extra requests in the dashboard |
429 three times in a row | More than 20 requests a second | Don't run several copies of the script at once |
| Exceeded maximum execution time | A run passed 6 minutes, or a custom function 30 seconds | Lower MAX_PAGES; the follower export resumes by itself |
| Service invoked too many times for one day: urlfetch | Over 20K URL Fetch calls a day | Run the trigger less often |
| IDs end in 0000 | Column A was a number column | Format column A as Plain text, delete the rows, run again |
401 {"message":"invalid api key"}- Cause
- The key has a typo or a stray space
- Fix
- Run X data > Set API key again
403 {"message":"missing key in request header"}- Cause
- The header didn't go out
- Fix
- Keep the header name
ApiKeyexactly
403with any other message- Cause
- The plan's requests are used up
- Fix
- Upgrade or switch on extra requests in the dashboard
429 three times in a row- Cause
- More than 20 requests a second
- Fix
- Don't run several copies of the script at once
Exceeded maximum execution time
- Cause
- A run passed 6 minutes, or a custom function 30 seconds
- Fix
- Lower
MAX_PAGES; the follower export resumes by itself
Service invoked too many times for one day: urlfetch
- Cause
- Over 20K URL Fetch calls a day
- Fix
- Run the trigger less often
IDs end in 0000
- Cause
- Column A was a number column
- Fix
- Format column A as Plain text, delete the rows, run again
#Fill your first sheet
Getting Twitter data into Google Sheets takes an API, a short Apps Script and a trigger, and the API decides what the sheet costs. To export tweets, followers and profiles to Google Sheets, use Sorsa API: 200 followers per request from $0.007 per 1,000, 20 tweets per request with search back to 2006, and 100 profiles per request for the =X_PROFILES() formula. Paste the script, set your key, and your first 20K followers or 2,000 tweets are free.
#FAQ
Which Twitter API should I use to get data into Google Sheets?
Use Sorsa API to get Twitter data into Google Sheets. It returns 200 followers, 100 profiles or 20 tweets per request, from $0.007 per 1,000 followers, with one ApiKey header that Apps Script, connector add-ons and n8n all send. It needs no X developer account or OAuth, allows 20 requests per second on every plan, and starts with 100 free requests that never expire. The official X API charges $10 per 1,000 followers.
Can I export Twitter Ads data to Google Sheets?
Twitter Ads data, such as spend, ad impressions and conversions, is private to the ad account, so it comes out of X Ads Manager's own report export or X's Ads API with your account's authorization, not from a public data API. What Sorsa API can put in a sheet is the public side of the same campaign: views, likes, reposts and replies on your posts and on the mentions they draw.
Can Google Sheets post tweets?
Google Sheets can post tweets only through the official X API, with a developer app, OAuth and a charge of $0.015 per post. Sorsa API is read-only by design: it never posts, likes or follows, and it never asks for your X login, so your account is never at risk. For a sheet that both reads and posts, read with Sorsa API and post with the official X API.
How many tweets can one Google Sheet hold?
A Google spreadsheet holds up to 20 million cells, so at ten columns per tweet it takes just under 2 million tweets across all its tabs. Sheets slows down well before that, so for long collections start a new spreadsheet each month or quarter. Apps Script can write to a spreadsheet by its ID, so the same script can roll over to a new file.
Is there a free way to get Twitter data into Google Sheets?
Yes. Sorsa API gives every new account 100 free requests with no card, and they never expire: enough for 2,000 tweets, 20K followers or 10K profiles in a sheet through the script on this page. Apps Script and Google Sheets cost nothing on a free Google account. For a single file, the free Export Tweets tool downloads a CSV that Sheets imports.
#How we checked
We wrote the script against Sorsa API's live API reference on November 2, 2026, then ran it in Node.js with stand-ins for SpreadsheetApp, UrlFetchApp and PropertiesService fed with responses in the reference's shape: two search runs, a three-run follower export that resumed from its cursor, the X_PROFILES function and the 401, 403 and 429 paths. The response shown uses demo values. Google's quotas, Sheets limits and custom-function rules were read on Google's pages and X's prices on docs.x.com, all on the same day.
Sources
- X API pay-per-usage pricing (opens in a new tab)
- X API PPU expansions update, October 23, 2026 (opens in a new tab)
- Apps Script quotas for Google services (opens in a new tab)
- Custom functions in Google Sheets (opens in a new tab)
- Files you can store in Google Drive (opens in a new tab)
- Sorsa API reference: Search Tweets (opens in a new tab)