02 Get your data in /Integrations
Google Forms
Send form responses from Google Forms to Dimension Labs with the universal API.
- GF Google FormsTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- Dimension LabsRead in full · enriched daily
This brings in your Google Forms responses: each text question and the respondent’s answer, one conversation per response, with scale, rating and quiz scores alongside. You run it on your side: a small script pulls from the Google Forms API and posts each message to the universal API.
What you need
Section titled “What you need”- A Google Cloud project with the Google Forms API enabled.
- An OAuth 2.0 client ID and secret, and a Google account that can open the forms, to authorize once with the
https://www.googleapis.com/auth/forms.body.readonlyandhttps://www.googleapis.com/auth/forms.responses.readonlyscopes and get a refresh token. - The ID of each form.
- A Dimension Labs API key for a new Universal API source. See Your API key.
1. Get API access in Google Forms
Section titled “1. Get API access in Google Forms”Google’s steps are in the Forms API quickstart and Configure the OAuth consent screen.
- In the Google Cloud console, create or pick a project and enable the Google Forms API.
- Set up the consent screen under Menu > Google Auth platform > Branding. Choose Internal under Audience if the forms belong to your Google Workspace organization. If you choose External and leave the app in Testing, Google expires refresh tokens after 7 days, so publish it to In production.
- Go to Menu > Google Auth platform > Clients and click Create Client. Choose Web application, add
https://developers.google.com/oauthplaygroundas an authorized redirect URI, and copy the client ID and client secret. - Get a refresh token once: open the OAuth 2.0 Playground, click the settings icon, select Use your own OAuth credentials and enter your client ID and secret. Authorize the two scopes above with an account that can open the forms, click Exchange authorization code for tokens, and copy the refresh token. Keep it in a secrets manager.
- The code below uses
google-auth-libraryto turn the refresh token into a fresh access token whenever it needs one, and sends it asAuthorization: Bearer ACCESS_TOKEN. - The form ID is in the form’s URL: in
https://docs.google.com/forms/d/FORM_ID/editit isFORM_ID.
2. Pull new responses
Section titled “2. Pull new responses”curl -G -H "Authorization: Bearer $ACCESS_TOKEN" \ --data-urlencode "filter=timestamp >= 2026-10-01T00:00:00Z" \ "https://forms.googleapis.com/v1/forms/FORM_ID/responses"GET /v1/forms/{formId}/responseslists the responses to one form. There is no call that lists every form, so list the form IDs yourself.- Incremental:
filtersupports onlytimestamp > Nandtimestamp >= N, which return responses submitted after (or at) that time.Nis RFC 3339 UTC, for example2014-10-02T15:01:23Z. - Pagination:
pageSizedefaults to a maximum of 5,000. PassnextPageTokenaspageTokenfor the next page, with the samefilteras the first request. - Answers are keyed by question ID. The question titles come from
GET /v1/forms/{formId}: each entry initemshas atitle, and aquestionItem.questionwith itsquestionId. AtextQuestionis a short answer or, withparagraph: true, a paragraph question. - Rate limits (Google’s): listing responses counts as an expensive read: 450 per minute per project and 180 per minute per user per project. Over the limit, Google returns
429.
3. Map the fields
Section titled “3. Map the fields”Each text question becomes an outgoing message (the question title) followed by an incoming message (the answer), under the response ID. Skip blank answers.
| Dimension Labs | Google Forms | Notes |
|---|---|---|
text | item title (outgoing), answers[questionId].textAnswers.answers[].value (incoming) | Only for textQuestion items. |
userId | responseId | Google returns no respondent ID, so use the response ID. Never respondentEmail. |
incoming | false for the question, true for the answer. | |
sessionId | responseId | Prefix it, for example google-forms-ACYDBNj.... |
conversationId | responseId | Same value as sessionId. |
dimensionlabs_timestamp | lastSubmittedTime | RFC 3339. Convert with toMillis. Add 1,000 ms per message. |
platformJson | form ID, totalScore (quizzes), the answers to scale and rating questions | Scale and rating answers come back as text; the code turns them into numbers. |
Personal data. respondentEmail is filled in when the form collects email addresses: the code doesn’t send it, so keep it that way. Text answers can hold names, emails and phone numbers too: redact them before you send. See PII redaction.
4. Send it
Section titled “4. Send it”Install the auth library in your project first: npm install google-auth-library.
// google-forms.mjs. Environment: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN,// GOOGLE_FORM_IDS (comma-separated), DL_API_KEY.import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';import { OAuth2Client } from 'google-auth-library';
const HOUR = 60 * 60 * 1000;const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const API = 'https://forms.googleapis.com/v1/forms';const oauth = new OAuth2Client(process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET);oauth.setCredentials({ refresh_token: process.env.GOOGLE_REFRESH_TOKEN });
// Uses a fresh access token on every call (the library refreshes it when it expires),// and retries Google's rate limits (429) and server errors (5xx) with a growing pause.async function get(url) { for (let attempt = 0; ; attempt++) { const { token } = await oauth.getAccessToken(); const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) return res.json(); if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Google returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
// The form's text questions, in form order, and the IDs of its scale and rating questions.async function readForm(formId) { const textQuestions = [], scoreIds = new Set(); for (const item of (await get(`${API}/${formId}`)).items ?? []) { const q = item.questionItem?.question; if (!q) continue; if (q.textQuestion) textQuestions.push({ id: q.questionId, title: item.title }); if (q.scaleQuestion || q.ratingQuestion) scoreIds.add(q.questionId); } return { textQuestions, scoreIds };}
async function syncForm(formId, since, historical) { const { textQuestions, scoreIds } = await readForm(formId); const filter = `timestamp >= ${new Date(since).toISOString().replace(/\.\d{3}Z$/, 'Z')}`; let newest = since, pageToken = ''; do { const q = new URLSearchParams({ filter, ...(pageToken && { pageToken }) }); const page = await get(`${API}/${formId}/responses?${q}`); for (const r of page.responses ?? []) { const submitted = toMillis(r.lastSubmittedTime ?? r.createTime); newest = Math.max(newest, submitted); const text = (qid) => (r.answers?.[qid]?.textAnswers?.answers ?? []).map((a) => a.value).join(', ').trim(); const scores = Object.fromEntries([...scoreIds].filter((qid) => text(qid)).map((qid) => [qid, Number(text(qid))])); const id = `google-forms-${r.responseId}`.replace(/:/g, '-'); const base = { userId: id, sessionId: id, conversationId: id, platformJson: { formId, totalScore: r.totalScore, scores } }; const old = historical || submitted < Date.now() - HOUR; let t = submitted; for (const question of textQuestions) { const answer = text(question.id); if (!answer) continue; await sendToDimensionLabs({ ...base, text: question.title, incoming: false, dimensionlabs_timestamp: t }, { historical: old }); await sendToDimensionLabs({ ...base, text: answer, incoming: true, dimensionlabs_timestamp: t + 1000 }, { historical: old }); t += 2000; } } pageToken = page.nextPageToken ?? ''; } while (pageToken); return newest;}
// Sends every response submitted at or after `since` (Unix ms), for each form. Returns the new `since`.export async function syncGoogleForms({ since, historical = false }) { let newest = since; for (const formId of process.env.GOOGLE_FORM_IDS.split(',').map((x) => x.trim()).filter(Boolean)) { newest = Math.max(newest, await syncForm(formId, since, historical)); } return newest;}Questions inside a grid (questionGroupItem) are choice rows, not free text, so the code leaves them out. One Dimension Labs source can hold all your forms: the form ID goes in platformJson.formId.
5. Run it
Section titled “5. Run it”Save the code as google-forms.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncGoogleForms as sync } from './google-forms.mjs';.
- History first:
node run.mjs 2025-01-01sends every response since that date to/trackhistorical. Then start a backfill from the Backfill tab. - Then every 15 minutes: the scheduled job sends new responses to
/track, and they are enriched automatically every day. A response that is more than an hour old when the job sees it goes to/trackhistorical, and needs a backfill. - Re-runs are safe: duplicates are removed automatically. Each run reads each form once and lists its responses, so it stays far below Google’s limits.
If you have responses in a file instead, upload it and check it first with the file checker, then run a backfill.
Google Forms reference
Section titled “Google Forms reference”- Google Forms API overview (form IDs)
- Forms API quickstart (enable the API, consent screen, OAuth client)
- Configure the OAuth consent screen
- Retrieve forms and responses
- Method: forms.responses.list
- REST Resource: forms.responses
- Method: forms.get
- REST Resource: forms
- Usage limits
- Using OAuth 2.0 to access Google APIs (refresh token expiration)
Checked against Google’s documentation on 2026-10-26.