02 Get your data in /Integrations
Jotform
Send form submissions from Jotform to Dimension Labs with the universal API.
- J JotformTickets, 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 Jotform submissions: each text field and what the respondent wrote in it, one conversation per submission, with the scores you choose alongside. You run it on your side: a small script pulls from the Jotform API and posts each message to the universal API.
What you need
Section titled “What you need”- A Jotform account. Jotform caps API calls per day by plan (see below), so a long history load on a Starter account may need more than one day.
- A Jotform API key, and 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 Jotform
Section titled “1. Get API access in Jotform”Jotform’s steps are in the API documentation.
- Go to My Account and open the API section (jotform.com/myaccount/api).
- Click Create a new API Key and copy it.
- Send it as the header
APIKEY: YOUR_API_KEY. Jotform also takes it as theapiKeyquery parameter; the header keeps it out of URLs and logs. - Use the API address for your account:
https://api.jotform.comby default,https://eu-api.jotform.comfor EU accounts, andhttps://hipaa-api.jotform.comfor HIPAA accounts. Jotform Enterprise accounts can use their own domain. - The form ID is the number in the form’s URL: in Jotform’s example,
https://www.jotformpro.com/form/30693660548966, it is30693660548966.GET /user/formsalso lists your forms with theirid.
2. Pull new submissions
Section titled “2. Pull new submissions”curl -G -H "APIKEY: $JOTFORM_API_KEY" \ --data-urlencode 'filter={"created_at:gt":"2026-10-01 00:00:00"}' \ --data-urlencode "orderby=created_at" --data-urlencode "limit=1000" --data-urlencode "offset=0" \ "https://api.jotform.com/form/FORM_ID/submissions"GET /form/{id}/submissionslists a form’s submissions.GET /user/submissionslists them for every form on the account.- Incremental:
filteris a JSON object such as{"created_at:gt":"2013-01-01 00:00:00"}. It supportsgt,ltandneonid,created_at,updated_at,statusand a few other fields.orderbytakescreated_at. - Pagination:
offset(default 0) andlimit(default 20, maximum 1,000). The response’sresultSethas theoffset,limitandcount. - Each submission has an
id,created_atandupdated_at(YYYY-MM-DD HH:MM:SS), astatusand ananswersobject keyed by question ID. Each answer hastext(the question label),type(such ascontrol_textboxorcontrol_textarea) andanswer(what the submitter entered). - Daily limits (Jotform’s): 1,000 API calls per day on Starter, 10,000 on Bronze, 50,000 on Silver and 100,000 on Gold. Jotform Enterprise has no daily limit. Every response includes
limit-left, the calls you have left today.
3. Map the fields
Section titled “3. Map the fields”Each text field becomes an outgoing message (the question label) followed by an incoming message (the answer), under the submission ID. Skip blank answers.
| Dimension Labs | Jotform | Notes |
|---|---|---|
text | answer text (outgoing), answer answer (incoming) | For control_textbox and control_textarea fields. Labels can hold HTML: strip it. |
userId | submission id | Jotform returns no respondent ID, so use the submission ID. |
incoming | false for the question, true for the answer. | |
sessionId | submission id | Prefix it, for example jotform-237955080346633702. |
conversationId | submission id | Same value as sessionId. |
dimensionlabs_timestamp | created_at | Convert with toMillis. Add 1,000 ms per message. |
platformJson | form ID, status, the answers to the score questions you list | Name rating, scale or number questions in JOTFORM_SCORE_QUESTION_IDS. |
Jotform’s API docs don’t give a time zone for created_at. The code reads it as UTC, and builds the filter the same way, so the cursor stays consistent. Before you go live, compare one submission’s created_at with the time you see for it in Jotform: if they differ, every timestamp is off by that offset.
Personal data. control_fullname, control_email and phone fields aren’t text fields here, so the code doesn’t send them. Don’t add them as score questions. The submission’s ip isn’t sent either. A text box can still hold a name or email: redact before you send. See PII redaction.
4. Send it
Section titled “4. Send it”// jotform.mjs. Environment: JOTFORM_API_KEY, JOTFORM_FORM_IDS (comma-separated),// JOTFORM_API (default https://api.jotform.com; https://eu-api.jotform.com or https://hipaa-api.jotform.com if yours is there),// JOTFORM_SCORE_QUESTION_IDS (optional, comma-separated question IDs), 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.JOTFORM_API ?? 'https://api.jotform.com';const list = (v) => (v ?? '').split(',').map((x) => x.trim()).filter(Boolean);const SCORE_IDS = new Set(list(process.env.JOTFORM_SCORE_QUESTION_IDS));const TEXT_TYPES = ['control_textbox', 'control_textarea'];const stamp = (ms) => new Date(ms).toISOString().slice(0, 19).replace('T', ' '); // "YYYY-MM-DD HH:MM:SS"const strip = (s) => String(s ?? '').replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/\s+/g, ' ').trim();
// Retries rate limits (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: { APIKEY: process.env.JOTFORM_API_KEY } }); if (res.ok) return res.json(); if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Jotform returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
async function syncForm(formId, since, historical) { // gt is "after", to the second: start one second early so nothing on the boundary is missed. const filter = JSON.stringify({ 'created_at:gt': stamp(since - 1000) }); let newest = since; for (let offset = 0; ; offset += 1000) { const q = new URLSearchParams({ filter, orderby: 'created_at', limit: '1000', offset: String(offset) }); const { content = [] } = await get(`${API}/form/${formId}/submissions?${q}`); for (const s of content) { const created = toMillis(s.created_at); newest = Math.max(newest, created); const answers = Object.entries(s.answers ?? {}).sort(([a], [b]) => Number(a) - Number(b)); const scores = Object.fromEntries(answers.filter(([qid, a]) => SCORE_IDS.has(qid) && a.answer !== undefined && a.answer !== '') .map(([qid, a]) => [qid, Number(a.answer)])); const id = `jotform-${s.id}`; const base = { userId: id, sessionId: id, conversationId: id, platformJson: { formId, status: s.status, scores } }; const old = historical || created < Date.now() - HOUR; let t = created; for (const [, a] of answers) { if (!TEXT_TYPES.includes(a.type) || typeof a.answer !== 'string' || !a.answer.trim()) continue; await sendToDimensionLabs({ ...base, text: strip(a.text), incoming: false, dimensionlabs_timestamp: t }, { historical: old }); await sendToDimensionLabs({ ...base, text: a.answer.trim(), incoming: true, dimensionlabs_timestamp: t + 1000 }, { historical: old }); t += 2000; } } if (content.length < 1000) return newest; }}
// Sends every submission created at or after `since` (Unix ms), for each form. Returns the new `since`.export async function syncJotform({ since, historical = false }) { let newest = since; for (const formId of list(process.env.JOTFORM_FORM_IDS)) { newest = Math.max(newest, await syncForm(formId, since, historical)); } return newest;}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 jotform.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncJotform as sync } from './jotform.mjs';.
- History first:
node run.mjs 2025-01-01sends every submission since that date to/trackhistorical. Then start a backfill from the Backfill tab. - Then every 15 minutes: the scheduled job sends new submissions to
/track, and they are enriched automatically every day. A submission 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 costs one call per form, plus one per 1,000 submissions. Every 15 minutes that is 96 runs a day per form, which fits inside even the Starter cap of 1,000 calls for a handful of forms. If you have submissions in a file instead, upload it and check it first with the file checker, then run a backfill.
Jotform reference
Section titled “Jotform reference”- API documentation (API endpoints, authentication, daily limits, form submissions, form questions)
- API key page
Checked against Jotform’s documentation on 2026-10-26.