Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Typeform

Integrate Typeform with Bolt.new using two approaches: embed Typeform forms using their JavaScript SDK (works immediately in the WebContainer preview — just load the script), or use the Typeform REST API to create forms and read responses from a Next.js API route. The embed SDK is the fastest path. Typeform webhooks for real-time response processing require a deployed Netlify URL — the WebContainer cannot receive incoming connections.

What you'll learn

  • How to embed a Typeform form in a React component using the @typeform/embed-react package
  • How to fetch form responses from the Typeform REST API using a Next.js API route
  • How to build a form response dashboard showing submission data from Typeform
  • How to handle Typeform webhooks for real-time response processing after deployment
  • How to use Typeform hidden fields to pass context data from your app into form submissions
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate19 min read20 minutesMarketingApril 2026RapidDev Engineering Team
TL;DR

Integrate Typeform with Bolt.new using two approaches: embed Typeform forms using their JavaScript SDK (works immediately in the WebContainer preview — just load the script), or use the Typeform REST API to create forms and read responses from a Next.js API route. The embed SDK is the fastest path. Typeform webhooks for real-time response processing require a deployed Netlify URL — the WebContainer cannot receive incoming connections.

Embed Typeform Forms and Fetch Responses in Bolt.new

Typeform's one-question-at-a-time format produces dramatically higher completion rates than traditional multi-field forms — typically 55-65% compared to 20-25% for conventional forms. This makes it particularly valuable for lead capture, customer research surveys, NPS measurement, job applications, and onboarding questionnaires where form abandonment is a significant problem. The conversational interface works across desktop and mobile without any custom CSS or responsive design work on your part.

For Bolt.new developers, the Typeform integration story has two distinct approaches. The quickest path is the Typeform Embed SDK, available as an npm package (@typeform/embed-react) or as a script tag. The React package installs in the WebContainer without any issues — it is pure JavaScript with no native dependencies. You can embed inline forms, launch popups on button click, and configure slide-in widgets in your Bolt app within minutes, with form responses collected entirely in Typeform's backend.

For teams that need to process responses programmatically — building a CRM-style dashboard of lead submissions, routing responses to Slack or email, syncing data to Supabase, or triggering workflows based on specific answers — the Typeform Responses API provides access to all submissions. Combined with Typeform webhooks (available on paid plans), your app can react to new submissions in near real-time. Webhooks require a deployed URL, but fetching existing responses via the API route works in the Bolt preview.

Integration method

Bolt Chat + API Route

Typeform connects to Bolt.new apps through their Embed SDK for displaying forms and their REST API for reading responses. The Embed SDK loads via a script tag and renders forms as inline widgets, popups, or sliders — all work in Bolt's WebContainer preview without any API keys. For reading form submissions and building response dashboards, use the Typeform API with a personal access token stored in a server-side environment variable. Webhook delivery for real-time response processing requires a deployed URL.

Prerequisites

  • A Typeform account at typeform.com — the free plan allows 10 responses per month per form
  • At least one Typeform form created — note the Form ID from the form URL (e.g., yourname.typeform.com/to/{formId})
  • A Typeform Personal Access Token for API access — create one at typeform.com/user/tokens
  • A Bolt.new account with a new Next.js project open
  • A Netlify account for deployment if you plan to use Typeform response webhooks

Step-by-step guide

1

Install the Typeform Embed SDK and create your first embedded form

The Typeform Embed SDK is available as the `@typeform/embed-react` npm package, which provides React components for inline forms, popups, and sliders. The package is pure JavaScript with no native C/C++ dependencies, so it installs and runs in Bolt.new's WebContainer without any issues. The SDK exports three main components: `Widget` for inline embedded forms, `PopupButton` for forms that open as a full-screen modal, and `Slider` for forms that slide in from the side. All three accept a form `id` prop, a `style` prop for sizing, and an `onSubmit` callback that fires when the user completes the form. The `onSubmit` callback receives a `responseId` that you can use to fetch the specific response from the Typeform API. To find your form ID: open your Typeform, click Share, and look at the link. The form ID is the alphanumeric string at the end of the typeform URL (e.g., for `yourname.typeform.com/to/ABCD1234`, the form ID is `ABCD1234`). You can also find it in the form builder URL: `admin.typeform.com/form/ABCD1234/create`. For the inline `Widget`, you must set an explicit height — without it the widget renders at zero height. A height of 400-600px covers most forms without requiring the user to scroll inside the widget. For longer surveys, consider using the `PopupButton` or `Slider` variants instead, which give users a full-screen experience. Hidden fields let you pass contextual data from your app into form submissions — user ID, plan tier, page URL — without showing these inputs to the user. They appear in the Typeform response data, letting you link responses to specific users in your database.

Bolt.new Prompt

Set up Typeform Embed in my Next.js app. Install @typeform/embed-react. Create a .env.local file with NEXT_PUBLIC_TYPEFORM_FORM_ID=your-form-id and TYPEFORM_API_TOKEN=your-token-here. Create a TypeformEmbed component at src/components/TypeformEmbed.tsx that accepts props: formId (string, defaults to NEXT_PUBLIC_TYPEFORM_FORM_ID), type ('inline' | 'popup' | 'slider', defaults to 'inline'), height (number, defaults to 500), hiddenFields (optional Record<string, string>), onSubmit (optional callback). Render the appropriate component from @typeform/embed-react based on the type prop. For popup and slider, render a styled trigger button. Add TypeScript types.

Paste this in Bolt.new chat

src/components/TypeformEmbed.tsx
1// src/components/TypeformEmbed.tsx
2'use client';
3import { Widget, PopupButton, SliderButton } from '@typeform/embed-react';
4import '@typeform/embed-react/build/css/widget.css';
5import '@typeform/embed-react/build/css/popup.css';
6import '@typeform/embed-react/build/css/slider.css';
7
8interface TypeformEmbedProps {
9 formId?: string;
10 type?: 'inline' | 'popup' | 'slider';
11 height?: number;
12 hiddenFields?: Record<string, string>;
13 onSubmit?: (responseId: string) => void;
14 buttonText?: string;
15 className?: string;
16}
17
18export function TypeformEmbed({
19 formId,
20 type = 'inline',
21 height = 500,
22 hiddenFields,
23 onSubmit,
24 buttonText = 'Open Survey',
25 className = '',
26}: TypeformEmbedProps) {
27 const targetId = formId || process.env.NEXT_PUBLIC_TYPEFORM_FORM_ID;
28
29 if (!targetId) {
30 return (
31 <div className="p-4 border border-dashed border-gray-300 rounded text-gray-500 text-sm">
32 Add NEXT_PUBLIC_TYPEFORM_FORM_ID to your .env file
33 </div>
34 );
35 }
36
37 const handleSubmit = ({ responseId }: { responseId: string }) => {
38 onSubmit?.(responseId);
39 };
40
41 const sharedProps = {
42 id: targetId,
43 hidden: hiddenFields,
44 onSubmit: handleSubmit,
45 };
46
47 if (type === 'popup') {
48 return (
49 <PopupButton
50 {...sharedProps}
51 className={`px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors ${className}`}
52 >
53 {buttonText}
54 </PopupButton>
55 );
56 }
57
58 if (type === 'slider') {
59 return (
60 <SliderButton
61 {...sharedProps}
62 className={`px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors ${className}`}
63 >
64 {buttonText}
65 </SliderButton>
66 );
67 }
68
69 return (
70 <Widget
71 {...sharedProps}
72 style={{ height: `${height}px`, width: '100%' }}
73 className={className}
74 />
75 );
76}

Pro tip: Import the Typeform CSS files (@typeform/embed-react/build/css/widget.css, popup.css, slider.css) to ensure the embed renders correctly. Without the CSS, the form may display unstyled or miss important layout styles. Add these imports to your component file or your global stylesheet.

Expected result: A TypeformEmbed React component that renders Typeform forms as inline widgets, popups, or sliders in the Bolt.new WebContainer preview.

2

Build the Typeform API response fetching route

While the Typeform embed captures submissions in Typeform's own backend, you may need to pull those responses into your own application — to build a CRM-style inbox, run analytics, sync data to Supabase, or trigger workflows based on specific answers. The Typeform Responses API provides access to all submissions for any form you own. Authentication uses a Personal Access Token sent as a Bearer token in the Authorization header. Generate a Personal Access Token at typeform.com/user/tokens — click 'Generate a new token', give it a descriptive name, and copy it. This token grants access to all your Typeform forms and responses, so treat it as a secret credential stored only in server-side environment variables. The Responses API returns a complex nested structure. Each response contains an `answers` array where each answer has a `field` (with the field ID, type, and reference), a `type` (text, number, choice, date, etc.), and the actual answer value in a type-specific field. For example, a text answer is in `answer.text`, a choice answer is in `answer.choice.label`, and a boolean is in `answer.boolean`. Normalizing this structure into a flat key-value object makes it easier to work with in your dashboard UI. The API supports pagination via `page_size` (max 1000) and `before` / `after` cursor parameters. For a dashboard that shows all responses, fetch in batches and use the `total_items` count in the response to know when you have fetched everything. Storing responses in Supabase rather than fetching from Typeform on every page load is strongly recommended. Typeform's API rate limits (1 request per second per token) make real-time fetching impractical for dashboards accessed frequently.

Bolt.new Prompt

Create a Typeform responses API route at app/api/typeform/responses/route.ts. Accept GET with query params: formId (optional, defaults to TYPEFORM_FORM_ID env var), pageSize (number, defaults to 100), since (optional ISO date string). Use TYPEFORM_API_TOKEN to call GET https://api.typeform.com/forms/{formId}/responses with Authorization Bearer header. Parse the response: extract items array, and for each response create a normalized object with id, submitted_at, and an answers object mapping field references to answer values (handle text, number, choice, boolean, email, file_url answer types). Return the normalized array. Also create a ResponsesTable React component that displays the data in a sortable table with columns for submission date, email (if present), and up to 4 answer columns.

Paste this in Bolt.new chat

app/api/typeform/responses/route.ts
1// app/api/typeform/responses/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4interface TypeformAnswer {
5 field: { id: string; type: string; ref: string };
6 type: string;
7 text?: string;
8 number?: number;
9 boolean?: boolean;
10 email?: string;
11 date?: string;
12 url?: string;
13 file_url?: string;
14 choice?: { label: string; other?: string };
15 choices?: { labels: string[]; other?: string };
16}
17
18interface NormalizedResponse {
19 id: string;
20 submitted_at: string;
21 answers: Record<string, string | number | boolean>;
22}
23
24function normalizeAnswer(answer: TypeformAnswer): string | number | boolean {
25 switch (answer.type) {
26 case 'text':
27 case 'long_text':
28 return answer.text || '';
29 case 'number':
30 return answer.number ?? 0;
31 case 'boolean':
32 return answer.boolean ?? false;
33 case 'email':
34 return answer.email || '';
35 case 'date':
36 return answer.date || '';
37 case 'url':
38 return answer.url || '';
39 case 'file_url':
40 return answer.file_url || '';
41 case 'choice':
42 return answer.choice?.label || answer.choice?.other || '';
43 case 'choices':
44 return (answer.choices?.labels || []).join(', ') + (answer.choices?.other ? `, ${answer.choices.other}` : '');
45 default:
46 return '';
47 }
48}
49
50export async function GET(request: NextRequest) {
51 const { searchParams } = new URL(request.url);
52 const formId = searchParams.get('formId') || process.env.TYPEFORM_FORM_ID;
53 const pageSize = parseInt(searchParams.get('pageSize') || '100', 10);
54 const since = searchParams.get('since') || '';
55
56 if (!formId) {
57 return NextResponse.json({ error: 'No form ID provided' }, { status: 400 });
58 }
59
60 const token = process.env.TYPEFORM_API_TOKEN;
61 if (!token) {
62 return NextResponse.json({ error: 'TYPEFORM_API_TOKEN is not set' }, { status: 500 });
63 }
64
65 const url = new URL(`https://api.typeform.com/forms/${formId}/responses`);
66 url.searchParams.set('page_size', String(Math.min(pageSize, 1000)));
67 if (since) url.searchParams.set('since', since);
68
69 const response = await fetch(url.toString(), {
70 headers: { Authorization: `Bearer ${token}` },
71 });
72
73 if (!response.ok) {
74 const error = await response.json();
75 return NextResponse.json(
76 { error: error.description || 'Typeform API error' },
77 { status: response.status }
78 );
79 }
80
81 const data = await response.json();
82
83 const normalized: NormalizedResponse[] = (data.items || []).map((item: { response_id: string; submitted_at: string; answers?: TypeformAnswer[] }) => {
84 const answers: Record<string, string | number | boolean> = {};
85 for (const answer of item.answers || []) {
86 const key = answer.field?.ref || answer.field?.id;
87 if (key) answers[key] = normalizeAnswer(answer);
88 }
89 return { id: item.response_id, submitted_at: item.submitted_at, answers };
90 });
91
92 return NextResponse.json({
93 total: data.total_items,
94 responses: normalized,
95 });
96}

Pro tip: Typeform uses field 'ref' values as the key for identifying questions in responses — these are the custom reference names you can set in the form builder (by default they are auto-generated IDs). Set meaningful ref values like 'email', 'nps_score', 'company_name' in your Typeform form builder to make the normalized response data easier to work with in your code.

Expected result: A Typeform responses API route that fetches and normalizes form submissions, ready to populate a response dashboard in your Bolt.new app.

3

Use Typeform hidden fields to link responses to app users

Typeform hidden fields are a powerful feature that lets you pass data from your app into form submissions without showing input fields to respondents. This allows you to link every Typeform response back to a specific user, session, or context in your application — essential for features like NPS tracking, post-onboarding surveys, or support ticket research where you need to know which user gave which response. Hidden fields are configured in two places: first in the Typeform form builder under Logic → Hidden Fields, where you define the names of fields to accept (e.g., `user_id`, `plan`, `page`). Then in your embed component, pass values for these fields via the `hidden` prop (on `Widget`, `PopupButton`, or `SliderButton`). Typeform attaches these values to each response, and they appear in the `hidden_fields` object of the response in the API. For dynamic hidden field values in a React app, you would typically pass them from the parent component or read them from your auth context. For example, if you use Supabase Auth, read the current user's ID from the session and pass it as the `user_id` hidden field. If you want to track which page or feature triggered the survey, pass a `source` field. This approach works in Bolt.new's WebContainer preview — hidden fields are part of the client-side embed and do not require any server-side logic to work. The values appear in responses fetched from the Typeform API.

Bolt.new Prompt

Update the TypeformEmbed component to support user context from Supabase Auth. Import the Supabase client hook. In the component, get the current user's ID and email from the Supabase session using useUser or useSession hook. Pass userId and userEmail as hidden fields to the Typeform widget automatically, alongside any additional hiddenFields passed as props. Create a UserSurveyTrigger component that shows a 'How are we doing?' button using TypeformEmbed in popup mode, automatically passing the current user's ID, plan tier (from a prop), and the current page URL as hidden fields. The button should only show after the user has been active for 30 seconds (use a useEffect timer).

Paste this in Bolt.new chat

src/components/UserSurveyTrigger.tsx
1// src/components/UserSurveyTrigger.tsx
2'use client';
3import { useEffect, useState } from 'react';
4import { PopupButton } from '@typeform/embed-react';
5import '@typeform/embed-react/build/css/popup.css';
6
7interface UserSurveyTriggerProps {
8 formId?: string;
9 userId?: string;
10 userEmail?: string;
11 plan?: string;
12 delaySeconds?: number;
13 onSubmit?: (responseId: string) => void;
14}
15
16export function UserSurveyTrigger({
17 formId,
18 userId,
19 userEmail,
20 plan = 'free',
21 delaySeconds = 30,
22 onSubmit,
23}: UserSurveyTriggerProps) {
24 const [isVisible, setIsVisible] = useState(false);
25 const targetId = formId || process.env.NEXT_PUBLIC_TYPEFORM_FORM_ID;
26
27 useEffect(() => {
28 const timer = setTimeout(() => {
29 setIsVisible(true);
30 }, delaySeconds * 1000);
31 return () => clearTimeout(timer);
32 }, [delaySeconds]);
33
34 if (!targetId || !isVisible) return null;
35
36 const hiddenFields: Record<string, string> = {
37 plan,
38 source: typeof window !== 'undefined' ? window.location.pathname : '',
39 };
40 if (userId) hiddenFields.user_id = userId;
41 if (userEmail) hiddenFields.user_email = userEmail;
42
43 return (
44 <div className="fixed bottom-4 right-4 z-50">
45 <PopupButton
46 id={targetId}
47 hidden={hiddenFields}
48 onSubmit={({ responseId }) => onSubmit?.(responseId)}
49 className="px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-full shadow-lg hover:bg-indigo-700 transition-colors"
50 >
51 How are we doing?
52 </PopupButton>
53 </div>
54 );
55}

Pro tip: Before using hidden fields, define them in your Typeform form builder. Go to your form → Logic → Hidden Fields → Add a hidden field and enter the field name (e.g., 'user_id'). If you pass hidden field values in the embed without declaring them in the form, Typeform silently ignores them and they will not appear in responses.

Expected result: A survey trigger component that passes user context as hidden fields, linking Typeform responses to specific users in your application.

4

Deploy to Netlify and configure Typeform response webhooks

Typeform webhooks enable real-time processing of form submissions — your app receives a POST request within seconds of a user completing the form. This is valuable for immediate actions: sending a confirmation email, creating a lead record in your CRM, notifying your team in Slack, or routing the user to a personalized next step based on their answers. Typeform webhooks are available on paid Typeform plans (Plus and above). To configure: in the Typeform admin, open your form, go to Connect → Webhooks → Add Webhook. Enter your endpoint URL and optionally enable webhook verification (which adds an HMAC signature to the request headers that you can verify). Webhook delivery requires a publicly accessible URL — Bolt.new's WebContainer cannot receive incoming HTTP connections during development. Deploy your app to Netlify first (click Deploy in Bolt.new, connect Netlify via OAuth), then use your `*.netlify.app` URL as the webhook endpoint. After deploying, go to Netlify dashboard → Site Configuration → Environment Variables and add TYPEFORM_API_TOKEN and any other required variables. The Typeform webhook payload has the same structure as the Responses API response but for a single submission. Your handler should extract the form response, normalize the answers, and perform the desired action — store in Supabase, call Slack's API, trigger an email, etc. Always return 200 quickly, even if processing fails, to prevent Typeform from retrying the webhook unnecessarily.

Bolt.new Prompt

Create a Typeform webhook handler at app/api/typeform/webhook/route.ts. Accept POST requests from Typeform. Parse the webhook payload: extract form_response.answers and form_response.hidden fields. Build a normalized answers object by iterating over answers. For each submission, store the data in a Supabase table typeform_responses with columns: response_id, form_id, submitted_at (timestamp), answers (jsonb), hidden_fields (jsonb). Return 200 immediately. Also add a netlify.toml with Next.js 14 build configuration. Include a comment in the webhook route explaining that it requires a deployed URL and cannot be tested in Bolt's WebContainer preview.

Paste this in Bolt.new chat

app/api/typeform/webhook/route.ts
1// app/api/typeform/webhook/route.ts
2// NOTE: Typeform webhooks require a public URL.
3// This route cannot receive events in Bolt's WebContainer preview.
4// Deploy to Netlify first, then register your *.netlify.app URL in Typeform.
5import { NextRequest, NextResponse } from 'next/server';
6import { createClient } from '@supabase/supabase-js';
7
8const supabase = createClient(
9 process.env.NEXT_PUBLIC_SUPABASE_URL!,
10 process.env.SUPABASE_SERVICE_ROLE_KEY!
11);
12
13export async function POST(request: NextRequest) {
14 const body = await request.json();
15
16 const formResponse = body.form_response;
17 if (!formResponse) {
18 return NextResponse.json({ error: 'Invalid payload' }, { status: 400 });
19 }
20
21 const answers: Record<string, string | number | boolean> = {};
22 for (const answer of formResponse.answers || []) {
23 const key = answer.field?.ref || answer.field?.id;
24 if (!key) continue;
25 switch (answer.type) {
26 case 'text': case 'long_text': answers[key] = answer.text || ''; break;
27 case 'number': answers[key] = answer.number ?? 0; break;
28 case 'boolean': answers[key] = answer.boolean ?? false; break;
29 case 'email': answers[key] = answer.email || ''; break;
30 case 'choice': answers[key] = answer.choice?.label || ''; break;
31 case 'choices': answers[key] = (answer.choices?.labels || []).join(', '); break;
32 default: answers[key] = '';
33 }
34 }
35
36 const hiddenFields = formResponse.hidden || {};
37
38 const { error } = await supabase.from('typeform_responses').upsert({
39 response_id: formResponse.token,
40 form_id: formResponse.form_id,
41 submitted_at: formResponse.submitted_at,
42 answers,
43 hidden_fields: hiddenFields,
44 }, { onConflict: 'response_id' });
45
46 if (error) {
47 console.error('Supabase insert error:', error);
48 // Still return 200 to prevent Typeform retries
49 }
50
51 return NextResponse.json({ received: true });
52}

Pro tip: Typeform retries webhook delivery up to 3 times if your endpoint returns a non-200 response. To avoid duplicate records in Supabase, use upsert with onConflict on the response_id (Typeform's unique token per submission). This makes your webhook handler idempotent — processing the same response twice produces the same result.

Expected result: A deployed Netlify app with a Typeform webhook endpoint that stores normalized responses in Supabase in real-time.

Common use cases

Embedded Lead Capture Form

Embed a Typeform survey or lead capture form directly in your landing page or app page. The form displays inline using Typeform's polished conversational UI while your React app controls when and where it appears. Completed submissions go to your Typeform account for management.

Bolt.new Prompt

Embed a Typeform form in my landing page. Install @typeform/embed-react. Create a LeadCaptureForm component that renders a Typeform inline widget using the Widget component from @typeform/embed-react. Use the form ID from NEXT_PUBLIC_TYPEFORM_FORM_ID env var as the id prop. Set style to height: 500px and width: 100%. Add an onSubmit callback that logs the response ID and shows a thank you message overlay. Place this form in the middle of my landing page between the features section and the pricing section.

Copy this prompt to try it in Bolt.new

Response Dashboard for Survey Results

Build an internal dashboard that pulls all submissions from a Typeform survey and displays them in a searchable table with answer details, submission dates, and the ability to mark entries as reviewed. Useful for teams collecting leads, job applications, or customer feedback via Typeform.

Bolt.new Prompt

Build a Typeform response dashboard. Create an API route at app/api/typeform/responses/route.ts that fetches responses from GET https://api.typeform.com/forms/{formId}/responses using TYPEFORM_API_TOKEN and TYPEFORM_FORM_ID env vars. Parse the responses into a flat structure mapping field references to answer values. Store fetched responses in a Supabase table typeform_responses. Create a ResponseDashboard React component showing a searchable table of submissions with columns for submission date, email (if captured), and the first 3 answers. Add a Sync button that triggers a fresh fetch from Typeform.

Copy this prompt to try it in Bolt.new

NPS Survey with Real-Time Routing

After a user completes a key action in your app (finishing onboarding, making their first purchase, reaching a usage milestone), trigger a Typeform NPS survey popup. When responses come in via webhook, route detractors (0-6) to a support follow-up flow and promoters (9-10) to a referral prompt.

Bolt.new Prompt

Create an NPS survey trigger. Build a TriggerNPSSurvey component that uses the @typeform/embed-react Popup component with a ref to programmatically open a Typeform NPS survey (NEXT_PUBLIC_TYPEFORM_NPS_ID env var). Add hidden fields to pre-fill the user's ID and plan tier so responses can be linked back to users. Create an API route at app/api/typeform/nps-webhook/route.ts that receives Typeform webhooks, extracts the NPS score and user ID, classifies into promoter/passive/detractor, and stores the result in Supabase. Expose a triggerNPS() function that the app can call after key user milestones.

Copy this prompt to try it in Bolt.new

Troubleshooting

TypeformEmbed Widget renders but appears blank or at zero height

Cause: The Widget component requires an explicit CSS height set via the style prop. Without it, the widget container collapses to zero pixels. Additionally, the Typeform CSS files must be imported for the widget to render correctly.

Solution: Add style={{ height: '500px', width: '100%' }} to the Widget component. Also ensure you import @typeform/embed-react/build/css/widget.css in the component file. If the form still appears blank, check the browser console for JavaScript errors and verify the form ID is correct.

typescript
1import '@typeform/embed-react/build/css/widget.css';
2// In your component:
3<Widget
4 id={formId}
5 style={{ height: '500px', width: '100%' }}
6/>

Typeform API returns 401 Unauthorized when fetching responses

Cause: The Personal Access Token is missing, incorrect, or stored in the wrong environment variable. Using NEXT_PUBLIC_TYPEFORM_API_TOKEN exposes it to the browser — the token should only be in a server-side variable.

Solution: Verify the token is stored as TYPEFORM_API_TOKEN (without NEXT_PUBLIC_ prefix) in .env.local. Confirm the token is valid by testing it at typeform.com/user/tokens — check that it has not expired or been revoked. The token goes in the Authorization header as a Bearer token: Authorization: Bearer {token}.

Hidden fields passed in the embed are not appearing in Typeform responses

Cause: Hidden fields must be declared in the Typeform form builder before they can be passed via the embed. If you pass field values in the embed without first defining the field names in Typeform's Logic → Hidden Fields settings, Typeform silently ignores them.

Solution: Open your Typeform form, go to Logic → Hidden Fields (at the bottom of the page), click 'Add a hidden field', and type the exact field name you are passing in the embed (e.g., 'user_id'). After saving, the field will appear in future response data. Existing responses before the field was added will not have the hidden field value.

Typeform webhooks are not being received during Bolt.new development

Cause: Bolt.new's WebContainer cannot receive incoming HTTP connections. Typeform webhooks need a publicly accessible URL to deliver POST requests. The dynamic WebContainer preview URL changes each session and cannot be registered as a webhook endpoint.

Solution: Deploy your app to Netlify, then register your Netlify URL (https://your-app.netlify.app/api/typeform/webhook) in Typeform's Connect → Webhooks settings. You can test the webhook endpoint manually by clicking 'Delivery Attempts' → 'Send test request' in Typeform after deploying. This is a core WebContainer limitation that applies to all incoming webhook integrations.

Best practices

  • Set meaningful 'ref' values on your Typeform questions in the form builder — use readable names like 'email', 'nps_score', 'company_name' instead of auto-generated IDs to make response normalization code readable
  • Declare hidden fields in Typeform's Logic → Hidden Fields settings before passing values via the embed, otherwise Typeform silently ignores the field values
  • Import Typeform embed CSS files (@typeform/embed-react/build/css/widget.css, popup.css, slider.css) to ensure widgets render correctly
  • Always return 200 from webhook handlers immediately — log errors internally but do not return error status codes, which cause Typeform to retry delivery
  • Store Typeform API tokens in server-side environment variables only — never use NEXT_PUBLIC_ prefix for credentials that grant access to all your form responses
  • Use Typeform's popup or slider embed variants for longer surveys — inline widgets work best for 3-5 question forms, while longer surveys benefit from the full-screen popup experience
  • Cache Typeform responses in Supabase and refresh periodically rather than fetching from the Typeform API on every page load — the API allows 1 request per second per token
  • Test the popup trigger timing with real users — the default 30-second delay for NPS surveys often works well, but timing should match when users have experienced enough of your product to give meaningful feedback

Alternatives

Frequently asked questions

Does Typeform embed work in Bolt.new's WebContainer preview?

Yes. The @typeform/embed-react package is pure JavaScript with no native dependencies, so it installs and runs in Bolt's WebContainer without issues. The inline Widget, PopupButton, and SliderButton components all render correctly in the preview. The only Typeform feature requiring deployment is webhooks, which need a publicly accessible URL.

Can I use Typeform's free plan with Bolt.new?

Yes for form embedding — the embed SDK works regardless of your Typeform plan. However, the free plan limits you to 10 responses per month per form. Webhooks require the Plus plan or higher. The Typeform Responses API is available on all paid plans. For development and testing, the free plan is sufficient.

How do I link Typeform responses to specific users in my Supabase database?

Use Typeform hidden fields. In your form builder, go to Logic → Hidden Fields and add a field named 'user_id'. In your TypeformEmbed component, pass the current user's Supabase ID as hidden={{ user_id: currentUser.id }}. The user ID appears in every response in the hidden_fields object, allowing you to join Typeform data with your Supabase user records.

How do I get real-time notifications when someone completes a Typeform?

Typeform webhooks deliver a POST request to your endpoint within seconds of form completion. You need a deployed URL (Netlify or Bolt Cloud) — webhooks cannot reach Bolt's WebContainer preview. Set up the webhook in Typeform under Connect → Webhooks. For instant team notifications during development, you can instead poll the Typeform Responses API endpoint you built on a short interval.

What is the difference between Typeform and SurveyMonkey for Bolt.new integrations?

Both have embed options and REST APIs for fetching responses. Typeform's @typeform/embed-react package is cleaner to integrate into React apps, and its conversational format produces higher completion rates for shorter forms. SurveyMonkey has more advanced branching logic and analytics reporting, and its embed is less React-native. For most Bolt apps using forms for lead capture or feedback, Typeform's developer experience is better.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.