Skip to content

02 Get your data in /Integrations

Typeform

Send form responses from Typeform to Dimension Labs with the universal API.

  1. T TypeformTickets, chats, calls, reviews
  2. ⟳ Your jobPull new · map fields · every 15 min
  3. { } Universal APIPOST /track · /trackhistorical
  4. Dimension LabsRead in full · enriched daily

This brings in your Typeform responses: each text question and the respondent’s answer, with ratings, scores and hidden fields alongside. You run it on your side with the universal API, using Typeform’s Responses API.

  • A Typeform account. Typeform says API access works on standard accounts, with no developer account needed.
  • A Typeform personal access token with the responses:read and forms:read scopes, and the ID of each form.
  • A Dimension Labs API key for a new Universal API source. See Your API key.

Typeform’s steps are in Personal access token.

  1. Log in to Typeform. In the drop-down menu next to your username in the upper-left corner, click Account.
  2. In the left menu, click Personal tokens, then Generate a new token.
  3. Name the token, choose the scopes responses:read and forms:read, and click Generate token.
  4. Send it as Authorization: Bearer YOUR_TOKEN. The form ID is in the form’s URL: in https://mysite.typeform.com/to/u6nXL7 it is u6nXL7.

If your responses are stored in the EU Data Center, use https://api.eu.typeform.com or https://api.typeform.eu instead of https://api.typeform.com.

Terminal window
curl -H "Authorization: Bearer $TYPEFORM_TOKEN" \
"https://api.typeform.com/forms/FORM_ID/responses?page_size=100&since=2026-10-01T00:00:00&sort=submitted_at,asc"
  • GET /forms/{form_id}/responses returns completed responses by default.
  • Incremental: since and until take a UTC time to the second (2020-03-20T14:00:59) or Unix seconds, and are inclusive. excluded_response_ids skips IDs you already sent. The default sort is submitted_at,desc.
  • Typeform notes that very recent responses (within about the last 30 minutes) may not be returned yet. Set until to 30 minutes ago so a late response isn’t skipped.
  • Pagination: for more than 1,000 responses, Typeform says to narrow each request with since/until or before/after. The example below moves since forward page by page.
  • Question titles come from GET /forms/{form_id}: each item in fields has an id, a title and a type.
  • Rate limit (Typeform’s): two requests per second per Typeform account, for the Create and Responses APIs.

Each text question becomes an outgoing message (the field title) followed by an incoming message (the answer), under the response ID. Skip blank answers.

Dimension LabsTypeformNotes
textfield title (outgoing), answers[].text where field.type is short_text or long_text (incoming)
userIda hidden field such as your internal customer ID, if you pass oneOtherwise use response_id. Never an email address.
incomingfalse for the question, true for the answer.
sessionIdresponse_id
conversationIdresponse_id
dimensionlabs_timestampsubmitted_atConvert with toMillis. Add 1,000 ms per message.
platformJsonanswers[].number for rating, opinion_scale and number fields, calculated.score, hidden, metadata.platformHidden fields are the place for account or order IDs to join on.

Personal data. Typeform’s email and phone_number answers aren’t sent by this code; keep it that way. Don’t pass emails or names in hidden fields, because the code sends hidden in platformJson. Redact open-text answers too. See PII redaction.

// typeform.mjs. Environment: TYPEFORM_TOKEN, TYPEFORM_FORM_IDS (comma-separated),
// TYPEFORM_API (default https://api.typeform.com; use your EU address if your data is there), DL_API_KEY.
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const API = process.env.TYPEFORM_API ?? 'https://api.typeform.com';
const iso = (ms) => new Date(ms).toISOString().slice(0, 19);
// Retries Typeform's rate limit (429) and server errors (5xx), honoring Retry-After when it is sent.
async function get(url) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.TYPEFORM_TOKEN}` } });
if (res.ok) return res.json();
if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Typeform returned ${res.status}: ${await res.text()}`);
await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
}
}
async function syncForm(formId, since, historical) {
const titles = {};
const walk = (fields = []) => fields.forEach((f) => { titles[f.id] = f.title; walk(f.properties?.fields); });
walk((await get(`${API}/forms/${formId}`)).fields);
const until = iso(Date.now() - 30 * 60 * 1000); // Typeform may not return the last 30 minutes yet
let from = since, seen = [], newest = since;
for (;;) {
const q = new URLSearchParams({ page_size: '100', since: iso(from), until, sort: 'submitted_at,asc' });
if (seen.length) q.set('excluded_response_ids', seen.join(','));
const { items } = await get(`${API}/forms/${formId}/responses?${q}`);
for (const r of items) {
const scores = Object.fromEntries((r.answers ?? []).filter((a) => a.type === 'number').map((a) => [a.field.ref ?? a.field.id, a.number]));
const base = { userId: r.hidden?.customer_id ?? r.response_id, sessionId: r.response_id, conversationId: r.response_id,
platformJson: { formId, scores, score: r.calculated?.score, hidden: r.hidden, platform: r.metadata?.platform } };
const submitted = toMillis(r.submitted_at);
const old = historical || submitted < Date.now() - HOUR;
let t = submitted;
for (const a of r.answers ?? []) {
if (!['short_text', 'long_text'].includes(a.field.type) || !a.text?.trim()) continue;
await sendToDimensionLabs({ ...base, text: titles[a.field.id] ?? a.field.ref, incoming: false, dimensionlabs_timestamp: t }, { historical: old });
await sendToDimensionLabs({ ...base, text: a.text.trim(), incoming: true, dimensionlabs_timestamp: t + 1000 }, { historical: old });
t += 2000;
}
const second = Math.floor(submitted / 1000) * 1000;
seen = second === from ? [...seen, r.response_id] : [r.response_id]; // skip these on the next page
from = second;
newest = Math.max(newest, submitted);
}
if (items.length < 100) return newest;
}
}
// Sends every response submitted at or after `since` (Unix ms), for each form. Returns the new `since`.
export async function syncTypeform({ since, historical = false }) {
let newest = since;
for (const formId of process.env.TYPEFORM_FORM_IDS.split(',').map((x) => x.trim()).filter(Boolean)) {
newest = Math.max(newest, await syncForm(formId, since, historical));
}
return newest;
}

customer_id stands for a hidden field you define yourself. Remove it if you don’t pass one. One Dimension Labs source can hold all your forms: the form ID goes in platformJson.formId.

Save the code as typeform.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncTypeform as sync } from './typeform.mjs';.

  • History first: node run.mjs 2025-01-01 sends 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. Because the code waits 30 minutes for Typeform to settle, a response is 30 to 45 minutes old when it is sent: it still goes to /track, and is 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.

If you download responses from Typeform as a file instead, upload it and check it first with the file checker, then run a backfill.

Checked against Typeform’s documentation on 2026-10-13.