Skip to main content
Technology & EngineeringForm Survey Services247 lines

Typeform

Integrate Typeform APIs to create forms, retrieve responses, configure webhooks,

Quick Summary22 lines
You are an expert at integrating the Typeform platform via its REST APIs and JavaScript Embed SDK. You build conversational forms programmatically, consume response data reliably, and react to submissions in real time through webhooks.

## Key Points

- **Polling responses on a cron** instead of using webhooks. You waste API quota and introduce latency.
- **Ignoring `event_id` uniqueness** in webhooks. Typeform may retry delivery, causing duplicate processing.
- **Hardcoding field IDs** instead of using `ref` strings. Typeform auto-generates IDs that change when forms are duplicated.
- **Embedding with raw iframes** instead of the Embed SDK. You lose `onSubmit` callbacks, hidden fields, and responsive sizing.
- Building branded, conversational surveys where completion rate matters more than density.
- Creating dynamic feedback forms that branch based on user sentiment or role.
- Collecting lead information with conditional follow-up questions.
- Embedding interactive forms in marketing landing pages or single-page apps.
- Processing high-volume survey responses in real time via webhook pipelines.

## Quick Example

```bash
TYPEFORM_PERSONAL_ACCESS_TOKEN=tfp_...
TYPEFORM_WEBHOOK_SECRET=your-webhook-secret  # for signature verification
```
skilldb get form-survey-services-skills/TypeformFull skill: 247 lines
Paste into your CLAUDE.md or agent config

Typeform API Skill

You are an expert at integrating the Typeform platform via its REST APIs and JavaScript Embed SDK. You build conversational forms programmatically, consume response data reliably, and react to submissions in real time through webhooks.

Core Philosophy

Conversational-First Form Design

Typeform's power is one-question-at-a-time UX. Design forms that feel like conversations, not spreadsheets. Use logic jumps to branch paths based on answers so respondents only see relevant questions. Keep forms short — completion rates drop sharply after 10 questions.

Event-Driven Response Handling

Never poll the Responses API on a timer. Register webhooks to receive submissions the instant they complete. Use the webhook event_id for idempotent processing. Fall back to the Responses API only for historical backfills or reconciliation.

Type-Safe API Consumption

The Typeform API returns deeply nested JSON. Define TypeScript interfaces for every payload you consume — form definitions, response objects, and webhook events. Validate webhook signatures before processing to prevent spoofed submissions.

Setup

Install the HTTP client and configure authentication:

// No official Node SDK — use fetch or axios with personal access token
const TYPEFORM_TOKEN = process.env.TYPEFORM_PERSONAL_ACCESS_TOKEN;
const BASE_URL = "https://api.typeform.com";

async function typeformFetch<T>(path: string, init?: RequestInit): Promise<T> {
  const res = await fetch(`${BASE_URL}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${TYPEFORM_TOKEN}`,
      "Content-Type": "application/json",
      ...init?.headers,
    },
  });
  if (!res.ok) throw new Error(`Typeform ${res.status}: ${await res.text()}`);
  return res.json() as Promise<T>;
}

Environment variables:

TYPEFORM_PERSONAL_ACCESS_TOKEN=tfp_...
TYPEFORM_WEBHOOK_SECRET=your-webhook-secret  # for signature verification

Key Patterns

Create a Form with Logic Jumps

Do this — build forms programmatically with branching logic:

interface TypeformField {
  ref: string;
  type: "short_text" | "multiple_choice" | "yes_no" | "rating" | "email";
  title: string;
  properties?: Record<string, unknown>;
}

interface CreateFormPayload {
  title: string;
  fields: TypeformField[];
  logic?: Array<{
    type: "field";
    ref: string;
    actions: Array<{
      action: "jump";
      details: { to: { type: "field"; value: string } };
      condition: { op: string; vars: Array<{ type: string; value: unknown }> };
    }>;
  }>;
}

const form = await typeformFetch<{ id: string }>("/forms", {
  method: "POST",
  body: JSON.stringify({
    title: "Customer Feedback",
    fields: [
      { ref: "satisfaction", type: "rating", title: "How satisfied are you?" },
      { ref: "why_low", type: "short_text", title: "What could we improve?" },
      { ref: "why_high", type: "short_text", title: "What did you love?" },
    ],
    logic: [
      {
        type: "field",
        ref: "satisfaction",
        actions: [
          {
            action: "jump",
            details: { to: { type: "field", value: "why_low" } },
            condition: { op: "lower_than", vars: [{ type: "field", value: "satisfaction" }, { type: "constant", value: 3 }] },
          },
          {
            action: "jump",
            details: { to: { type: "field", value: "why_high" } },
            condition: { op: "greater_equal_than", vars: [{ type: "field", value: "satisfaction" }, { type: "constant", value: 3 }] },
          },
        ],
      },
    ],
  } satisfies CreateFormPayload),
});

Not this — creating all questions linearly without logic, forcing every respondent through irrelevant paths.

Process Webhook Submissions

Do this — verify the signature and handle idempotently:

import crypto from "node:crypto";

interface TypeformWebhookPayload {
  event_id: string;
  event_type: "form_response";
  form_response: {
    form_id: string;
    token: string;
    submitted_at: string;
    answers: Array<{ field: { ref: string }; type: string; text?: string; number?: number; boolean?: boolean; choice?: { label: string } }>;
  };
}

function verifySignature(payload: string, signature: string, secret: string): boolean {
  const hash = crypto.createHmac("sha256", secret).update(payload).digest("base64");
  return `sha256=${hash}` === signature;
}

// Express route handler
app.post("/webhooks/typeform", (req, res) => {
  const sig = req.headers["typeform-signature"] as string;
  if (!verifySignature(JSON.stringify(req.body), sig, process.env.TYPEFORM_WEBHOOK_SECRET!)) {
    return res.status(403).send("Invalid signature");
  }
  const event = req.body as TypeformWebhookPayload;
  // Use event_id for idempotent processing
  await processSubmission(event.event_id, event.form_response);
  res.status(200).send("OK");
});

Not this — skipping signature verification or relying on polling the Responses API every minute.

Embed Forms in React

Do this — use the official embed SDK with proper lifecycle management:

import { createWidget } from "@typeform/embed";
import "@typeform/embed/build/css/widget.css";
import { useEffect, useRef } from "react";

function TypeformEmbed({ formId, onSubmit }: { formId: string; onSubmit: (responseId: string) => void }) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current) return;
    const { unmount } = createWidget(formId, {
      container: containerRef.current,
      hidden: { source: "webapp" },
      onSubmit: ({ responseId }) => onSubmit(responseId),
    });
    return () => unmount();
  }, [formId, onSubmit]);

  return <div ref={containerRef} style={{ height: 500 }} />;
}

Not this — embedding via raw iframe without the SDK, losing event callbacks and responsive sizing.

Common Patterns

Fetch Paginated Responses

interface ResponsesPage {
  total_items: number;
  page_count: number;
  items: Array<{ response_id: string; submitted_at: string; answers: unknown[] }>;
}

async function fetchAllResponses(formId: string): Promise<ResponsesPage["items"]> {
  const all: ResponsesPage["items"] = [];
  let before: string | undefined;
  do {
    const params = new URLSearchParams({ page_size: "100" });
    if (before) params.set("before", before);
    const page = await typeformFetch<ResponsesPage>(`/forms/${formId}/responses?${params}`);
    all.push(...page.items);
    before = page.items.at(-1)?.submitted_at;
    if (page.items.length < 100) break;
  } while (true);
  return all;
}

Register a Webhook

await typeformFetch(`/forms/${formId}/webhooks/my-hook`, {
  method: "PUT",
  body: JSON.stringify({
    url: "https://myapp.com/webhooks/typeform",
    enabled: true,
    secret: process.env.TYPEFORM_WEBHOOK_SECRET,
  }),
});

Retrieve a Single Form Definition

interface TypeformDefinition {
  id: string;
  title: string;
  fields: TypeformField[];
  _links: { display: string };
}

const definition = await typeformFetch<TypeformDefinition>(`/forms/${formId}`);

Anti-Patterns

  • Polling responses on a cron instead of using webhooks. You waste API quota and introduce latency.
  • Ignoring event_id uniqueness in webhooks. Typeform may retry delivery, causing duplicate processing.
  • Hardcoding field IDs instead of using ref strings. Typeform auto-generates IDs that change when forms are duplicated.
  • Embedding with raw iframes instead of the Embed SDK. You lose onSubmit callbacks, hidden fields, and responsive sizing.

When to Use

  • Building branded, conversational surveys where completion rate matters more than density.
  • Creating dynamic feedback forms that branch based on user sentiment or role.
  • Collecting lead information with conditional follow-up questions.
  • Embedding interactive forms in marketing landing pages or single-page apps.
  • Processing high-volume survey responses in real time via webhook pipelines.

Install this skill directly: skilldb add form-survey-services-skills

Get CLI access →