Skip to content

02 Get your data in /Integrations

Jotform

Send form submissions from Jotform to Dimension Labs with the universal API.

  1. J JotformTickets, 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 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.

  • 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.

Jotform’s steps are in the API documentation.

  1. Go to My Account and open the API section (jotform.com/myaccount/api).
  2. Click Create a new API Key and copy it.
  3. Send it as the header APIKEY: YOUR_API_KEY. Jotform also takes it as the apiKey query parameter; the header keeps it out of URLs and logs.
  4. Use the API address for your account: https://api.jotform.com by default, https://eu-api.jotform.com for EU accounts, and https://hipaa-api.jotform.com for HIPAA accounts. Jotform Enterprise accounts can use their own domain.
  5. The form ID is the number in the form’s URL: in Jotform’s example, https://www.jotformpro.com/form/30693660548966, it is 30693660548966. GET /user/forms also lists your forms with their id.
Terminal window
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}/submissions lists a form’s submissions. GET /user/submissions lists them for every form on the account.
  • Incremental: filter is a JSON object such as {"created_at:gt":"2013-01-01 00:00:00"}. It supports gt, lt and ne on id, created_at, updated_at, status and a few other fields. orderby takes created_at.
  • Pagination: offset (default 0) and limit (default 20, maximum 1,000). The response’s resultSet has the offset, limit and count.
  • Each submission has an id, created_at and updated_at (YYYY-MM-DD HH:MM:SS), a status and an answers object keyed by question ID. Each answer has text (the question label), type (such as control_textbox or control_textarea) and answer (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.

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 LabsJotformNotes
textanswer text (outgoing), answer answer (incoming)For control_textbox and control_textarea fields. Labels can hold HTML: strip it.
userIdsubmission idJotform returns no respondent ID, so use the submission ID.
incomingfalse for the question, true for the answer.
sessionIdsubmission idPrefix it, for example jotform-237955080346633702.
conversationIdsubmission idSame value as sessionId.
dimensionlabs_timestampcreated_atConvert with toMillis. Add 1,000 ms per message.
platformJsonform ID, status, the answers to the score questions you listName 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.

// 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(/&nbsp;/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.

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-01 sends 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.

Checked against Jotform’s documentation on 2026-10-26.