Unified.to
All articles

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:

  1. Discovering surveys and their fields
  2. Incrementally ingesting submissions
  3. Handling typed answers correctly
  4. Normalizing questions into canonical metrics
  5. Preparing data for analytics dashboards
  6. 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_active
  • response_count
  • fields[] with:
    • id, name
    • type (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_id
  • form_name
  • updated_at
  • is_active

Questions table

  • form_id
  • field_id
  • field_name
  • field_type
  • question_key (canonical metric identifier)

Submissions table

  • submission_id
  • form_id
  • created_at
  • updated_at
  • respondent_email (optional)
  • respondent_name (optional)

Answers table

  • submission_id
  • field_id
  • field_name
  • value_json
  • value_type
  • file_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 survey
  • updated_gte – incremental ingestion
  • limit / offset – pagination
  • fields[] – 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:

  1. Store value as JSON
  2. Derive value_type at ingestion time
  3. 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_SELECT
  • MATRIX
  • FILE_UPLOAD
  • SCALE fields

Step 6: Normalize questions into canonical metrics

Dashboards don't chart 'Field 7.' They chart metrics like:

  • csat_score
  • nps_score
  • engagement_score
  • feedback_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_score by 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.

Start your 30-day free trial

Book a demo

All articles