How to Build a Cross-Platform Survey Analytics Dashboard with Unified's Forms API
February 2, 2026
Survey analytics looks simple until you try to ship it as a product feature.
On paper, surveys all do the same thing. Ask questions. Collect responses. Show charts.
In practice, teams run surveys across multiple tools at once:
- Product feedback in Typeform
- Internal surveys in Google Forms
- Lightweight questionnaires in Tally
Everyone wants a single dashboard. No one wants to standardize vendors first.
This is where most analytics implementations break. Not because the charts are hard, but because form platforms are not designed to be analytics systems. Their APIs expose different field models, answer shapes, and event semantics. If you build against provider-specific payloads, your dashboard logic fractures immediately.
Unified's Forms API is designed to solve this exact problem.
It provides a read-only, normalized ingestion layer for forms and submissions across multiple providers, so you can build the analytics model once and support whichever form platform your customers already use.
This guide shows how to build a cross-platform survey analytics pipeline using Unified's Forms API—accurately, defensibly, and without inventing bidirectional behavior that doesn't exist.
What this guide covers
We'll build the foundation for dashboards like:
- CSAT trend over time
- NPS breakdown (promoters / passives / detractors)
- Employee engagement scores
- Product feedback aggregation
Specifically, we'll cover:
- Discovering surveys and their fields
- Incrementally ingesting submissions
- Handling typed answers correctly
- Normalizing questions into canonical metrics
- Preparing data for analytics dashboards
- Keeping data current with polling or webhooks
Important architectural constraint (by design)
The Forms API is read-only.
- No writable fields
- No form creation or mutation
- No submission writes
Forms systems are event sources, not systems of record. Unified reflects this correctly.
Your analytics pipeline should ingest data from Forms, store it in your database or warehouse, and compute metrics there. That separation is intentional and necessary for correctness.
Prerequisites
- Node.js v18+
- A Unified account
- Your Unified API key
- A customer Forms
connectionId
Supported Forms integrations include Google Forms, Tally, and Typeform.
Step 1: Initialize the SDK
import "dotenv/config";
import { UnifiedTo } from "@unified-api/typescript-sdk";
const sdk = new UnifiedTo({
security: { jwt: process.env.UNIFIED_API_KEY! },
});
const CONNECTION_FORMS = process.env.CONNECTION_FORMS!;
Step 2: Discover surveys (forms)
Unified exposes surveys via:
GET /forms/{connection_id}/form
This endpoint returns FormsForm objects with normalized metadata and field definitions.
You'll use this to:
- list available surveys,
- inspect questions and field types,
- build a stable mapping layer for analytics.
Example:
const forms = await sdk.forms.listFormsForms({
connectionId: CONNECTION_FORMS,
limit: 100,
offset: 0,
sort: "updated_at",
order: "asc",
fields: ["id", "name", "updated_at", "fields", "is_active", "response_count"],
});
Each form includes:
id,name,is_activeresponse_countfields[]with:id,nametype(TEXT, NUMBER, RATING, SCALE, MATRIX, etc.)choices(for select fields)- validation metadata (min, max, required, etc.)
This normalized field model is what makes cross-platform analytics possible.
Step 3: Define your internal analytics schema
Before ingesting submissions, define a schema that does not depend on any single provider.
A minimal, scalable structure:
Forms table
form_idform_nameupdated_atis_active
Questions table
form_idfield_idfield_namefield_typequestion_key(canonical metric identifier)
Submissions table
submission_idform_idcreated_atupdated_atrespondent_email(optional)respondent_name(optional)
Answers table
submission_idfield_idfield_namevalue_jsonvalue_typefile_ids[](optional)
Do not coerce answers into strings. The API explicitly allows values to be numbers, booleans, arrays, or objects.
Step 4: Ingest submissions incrementally
Unified exposes submissions via:
GET /forms/{connection_id}/submission
Key parameters for analytics ingestion:
form_id– scope to a surveyupdated_gte– incremental ingestionlimit/offset– paginationfields[]– payload control
Example:
const submissions = await sdk.forms.listFormsSubmissions({
connectionId: CONNECTION_FORMS,
form_id: "FORM_ID",
updated_gte: "2026-02-01T00:00:00.000Z",
limit: 100,
offset: 0,
sort: "updated_at",
order: "asc",
fields: [
"id",
"form_id",
"created_at",
"updated_at",
"respondent_email",
"respondent_name",
"answers",
],
});
Pagination loop
async function ingestSubmissions(formId: string, since: string) {
const pageSize = 100;
let offset = 0;
while (true) {
const page = await sdk.forms.listFormsSubmissions({
connectionId: CONNECTION_FORMS,
form_id: formId,
updated_gte: since,
limit: pageSize,
offset,
sort: "updated_at",
order: "asc",
});
if (page.length === 0) break;
for (const submission of page) {
persistSubmission(submission);
}
if (page.length < pageSize) break;
offset += pageSize;
}
}
This pattern works across all supported providers and does not assume webhook availability.
Step 5: Handle typed answers correctly
Each submission includes:
answers: {
field_id?: string;
field_name?: string;
value?: unknown; // string | number | boolean | array | object
file_ids?: string[];
}[];
Correct handling strategy:
- Store
valueas JSON - Derive
value_typeat ingestion time - Never assume shape based on field type alone
Example:
function deriveValueType(v: unknown) {
if (Array.isArray(v)) return "array";
if (v === null) return "null";
return typeof v;
}
This is critical for:
MULTIPLE_SELECTMATRIXFILE_UPLOADSCALEfields
Step 6: Normalize questions into canonical metrics
Dashboards don't chart 'Field 7.' They chart metrics like:
csat_scorenps_scoreengagement_scorefeedback_text
Create a mapping layer that assigns each field to a canonical question key.
Example mapping:
const questionMapping = {
nps_score: ["id:fld_123", "name:how likely are you to recommend"],
csat_score: ["id:fld_456", "name:overall satisfaction"],
feedback_text: ["type:TEXTAREA"],
};
Resolution logic:
function resolveQuestionKey(answer, mapping) {
const keys = [];
if (answer.field_id) keys.push(`id:${answer.field_id}`);
if (answer.field_name)
keys.push(`name:${answer.field_name.toLowerCase()}`);
for (const [metric, matchers] of Object.entries(mapping)) {
if (matchers.some(m => keys.includes(m))) return metric;
}
}
This is what allows you to aggregate across different forms and providers without rewriting analytics logic.
Step 7: Compute dashboard rollups
Once ingested and normalized, analytics becomes straightforward:
CSAT
- Average
csat_scoreby week/month
NPS
- Promoters: 9–10
- Passives: 7–8
- Detractors: 0–6
Engagement
- Average score by team or period
Feedback
- Volume over time
- Optional text clustering downstream
None of this logic belongs in the integration layer. It belongs in your analytics system, operating on clean, normalized data.
Step 8: Keeping data current (polling vs webhooks)
There are two correct ingestion strategies:
Option 1: Incremental polling (always supported)
- Use
updated_gte - Run ingestion on a schedule
- Works across all providers
Option 2: Webhooks (submission-only)
- Configure Unified webhooks for submission objects
- Use native webhooks where supported
- Use virtual webhooks otherwise
Webhook availability and behavior varies by provider. Always check Supported Integrations before relying on them.
Webhooks accelerate ingestion. They are not required for correctness.
Step 9: Security and data handling
Survey data often includes sensitive information.
Unified uses a passthrough, no-storage architecture:
- Data is fetched live from source APIs
- No customer form data is stored at rest in Unified
You store analytics data in your own environment, under your own security and retention policies. This keeps your integration layer lean and your compliance scope clear.
Summary
To build a cross-platform survey analytics dashboard with Unified's Forms API:
- Treat Forms as a read-only ingestion layer
- Discover surveys and fields via
GET /forms/{connection_id}/form - Ingest submissions incrementally via
GET /forms/{connection_id}/submission - Store answers as typed JSON
- Normalize questions into canonical metrics
- Compute analytics in your own system
- Use webhooks optionally, for submission events only
The result is a dashboard that works across providers without forcing vendor standardization—and without embedding fragile, provider-specific assumptions into your product.