Export SurveyMonkey responses to Google Sheets, including the nested answer IDs that hide your actual question text and the daily API cap on free apps.
SurveyMonkey's API returns responses as a structure of IDs rather than readable text: an answer points at a question ID and a choice ID, with the actual wording held elsewhere. Getting a usable spreadsheet means fetching the survey definition first and translating. Add a daily request cap on free apps that is low enough to exhaust in one careless loop, and there is real value in doing this deliberately.
The paid option: CSV export
On paid plans, open the survey's Analyze Results tab and export to CSV or XLS, then import into Sheets. Free plans have limited export, which is precisely why the API route below matters for anyone not paying.
SurveyMonkey to Google Sheets for free (Apps Script + API v3)
Register an app in the SurveyMonkey developer portal and get an access token, which is sent as a bearer token. Find the survey ID in the survey's URL.
The two-step pattern is essential. First call /surveys/{id}/details to get the questionnaire structure, building a map of question IDs to their text and choice IDs to their labels. Then call the bulk responses endpoint and translate every answer through those maps.
function importSurveyMonkeyResponses() { const TOKEN = "YOUR_ACCESS_TOKEN"; const SURVEY_ID = "123456789"; const headers = { Authorization: "Bearer " + TOKEN }; const root = "https://api.surveymonkey.com/v3"; // 1. The survey definition holds the actual wording. Responses only // contain IDs, so without this the export is unreadable. const details = JSON.parse(UrlFetchApp.fetch( root + "/surveys/" + SURVEY_ID + "/details", { headers } ).getContentText()); const questionText = {}, choiceText = {}, order = []; (details.pages || []).forEach((page) => { (page.questions || []).forEach((q) => { questionText[q.id] = (q.headings[0] || {}).heading || q.id; order.push(q.id); const answers = q.answers || {}; (answers.choices || []).concat(answers.rows || []).forEach((c) => { choiceText[c.id] = c.text; }); }); }); // 2. Use the bulk endpoint: /responses alone returns metadata only. const rows = []; let page = 1; while (true) { const res = JSON.parse(UrlFetchApp.fetch( root + "/surveys/" + SURVEY_ID + "/responses/bulk?per_page=100&page=" + page, { headers, muteHttpExceptions: true } ).getContentText()); if (res.error) throw new Error(JSON.stringify(res.error)); if (!res.data || !res.data.length) break; res.data.forEach((r) => { const byQuestion = {}; (r.pages || []).forEach((p) => { (p.questions || []).forEach((q) => { const values = (q.answers || []).map((a) => { if (a.choice_id) return choiceText[a.choice_id] || a.choice_id; if (a.text !== undefined) return a.text; if (a.row_id) return choiceText[a.row_id] || a.row_id; return ""; }); byQuestion[q.id] = values.join(", "); }); }); rows.push([ r.id, r.date_created, r.response_status, // completed / partial / disqualified ...order.map((qid) => byQuestion[qid] || ""), ]); }); if (!res.links || !res.links.next) break; page++; Utilities.sleep(600); // stay inside the per-minute limit } const header = ["Response ID","Created","Status", ...order.map((qid) => questionText[qid])]; const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName("Responses") || ss.insertSheet("Responses"); sheet.clearContents(); sheet.getRange(1, 1, 1, header.length).setValues([header]); if (rows.length) { sheet.getRange(2, 1, rows.length, header.length).setValues(rows); }}Building the header from the survey's own question order, as above, is what keeps columns aligned when respondents skip questions: every response is mapped into the same fixed set of columns rather than written in whatever order its answers happened to arrive.
Limits: around 125 requests a minute, with public apps also capped near 500 a day. Use per_page=100 on the bulk endpoint so a large survey costs a handful of requests rather than hundreds, and note Apps Script stops at six minutes.
The Brooked method
Brooked fetches the survey definition and translates question and choice IDs into real headers and values, keeps columns aligned across skipped questions, and separates partial from completed responses. Matrix and ranking questions expand into sensible columns rather than collapsing into one cell.
Step 1: Install Brooked
Install Brooked from the Google Workspace Marketplace. Free tier includes 100 imports per month, no credit card required.
Step 2: Connect SurveyMonkey
Choose SurveyMonkey in Brooked and authorise your account. Pick the survey, choose whether to include partial responses, and import.
Step 3: Schedule the refresh
Set the import to re-run daily while a survey is in the field, so response tracking and early results update without anyone exporting by hand.
Method comparison
| Method | Cost | Setup | Refresh | Two-way | Best for |
|---|---|---|---|---|---|
| SurveyMonkey CSV export | Paid plans for full export | 2 min | Manual | No | A one-off export on a paid plan |
| Apps Script + API v3 | Free | 30 to 45 min | Time-driven trigger | No | Refreshing data and custom mapping |
| Native Google Sheets integration | Paid plans | 5 min | On new response | No | Appending responses going forward |
| Brooked | Free tier · Pro $29/user/mo | ~3 min | Scheduled (hourly, daily, weekly) | No | Scheduled survey reporting with readable labels |
Common use cases
- NPS and CSAT tracking: score responses and trend them over survey waves
- Open-text analysis: categorise free-text comments into themes in the sheet
- Segmented reporting: cross-tabulate answers by any demographic question
- Response monitoring: watch completion and drop-off while a survey is live
- Longitudinal comparison: combine several survey waves into one table
Troubleshooting common issues
Related Articles
- How to Connect Typeform to Google Sheets
- How to Connect Pipedrive to Google Sheets
- How to Connect Monday.com to Google Sheets
Frequently asked questions
Why are SurveyMonkey responses full of IDs instead of question text?
The bulk responses endpoint returns structural IDs rather than text: each answer references a question ID and, for multiple choice, a choice ID. To produce a readable table you first call the survey details endpoint, build maps of question ID to question text and choice ID to choice label, then translate every answer. Without that step the export is unusable.
What is the SurveyMonkey API rate limit?
Roughly 125 requests per minute, and public apps are additionally capped at around 500 requests per day. That daily ceiling is the one that surprises people: it is low enough that a naive script fetching each response individually can exhaust an entire day's quota on one survey. Use the bulk endpoint, which returns full responses in pages of up to 100.
Can I export SurveyMonkey responses for free?
Through the API, yes: creating an app and using an access token costs nothing, and the Apps Script method here runs on a free trigger. SurveyMonkey's own CSV export and its native Google Sheets integration are gated behind paid plans, which is the usual reason free-plan users end up at the API.
How do I get full response data rather than just metadata?
Use the /surveys/{id}/responses/bulk endpoint. The plain /responses endpoint returns response metadata such as timestamps and status without the answers themselves, so people often conclude the answers are missing when they are simply on the other endpoint.
How do I tell partial responses from complete ones?
Each response carries a response_status of completed, partial, disqualified or overquota. Analysing without filtering mixes half-finished responses into your results, which usually skews later questions badly since partials abandon part-way through.
Can I write back to SurveyMonkey from Sheets?
No. Responses are immutable once submitted, so there is no write-back path. What you can do is enrich responses on the Sheets side with scoring, categorisation or CRM data, and push that enriched table onward.
Connect SurveyMonkey to Google Sheets today.
Free tier: 100 imports per month, no credit card required. Or see the full SurveyMonkey integration details.
Install Brooked free →

