Integrate Typeform with V0-generated Next.js apps by creating an API route that fetches form responses via the Typeform Retrieve API and a webhook endpoint that receives real-time form submissions. Store your Typeform Personal Access Token as a Vercel environment variable and display responses in a V0-generated dashboard. The entire integration runs server-side through Next.js API routes.
Connecting Typeform Responses and Webhooks to V0-Generated Dashboards
Typeform creates the most engaging forms on the web — their conversational one-question-at-a-time approach consistently achieves higher completion rates than traditional form builders. For developers building V0-generated applications, connecting Typeform data enables powerful use cases: customer feedback dashboards, lead qualification workflows, survey analytics, registration processing, and automated follow-up email triggers.
Typeform provides two primary integration patterns for developers. The first is the Retrieve API: a REST API that lets you fetch stored form responses, list your forms, and access response metadata. This is ideal for building analytics dashboards that periodically load and display accumulated response data. The second is webhooks: Typeform sends an HTTP POST request to your endpoint immediately after each form submission, enabling real-time response processing for automations like sending confirmation emails, creating CRM records, or triggering notifications.
In the V0 workflow, you generate the response analytics UI using V0's chat interface, then create Next.js API routes that connect to Typeform's API. The Retrieve API route fetches historical responses for dashboard display. The webhook route receives new submissions in real time. All API authentication uses a Personal Access Token stored as a Vercel environment variable, keeping your credentials secure in the server-side Next.js code.
Integration method
Typeform integrates with V0 apps through two complementary mechanisms: polling via the Typeform Retrieve API (GET requests to fetch stored responses) and real-time webhooks (Typeform POSTs submission data to your API route immediately after each form completion). Both patterns require a Next.js API route — the retrieve route fetches data for dashboards, and the webhook route processes responses for automated workflows. Your Typeform Personal Access Token is stored as a Vercel environment variable.
Prerequisites
- A Typeform account (free plan works — you need at least one existing form with responses or a form ID)
- A Typeform Personal Access Token — generate one at typeform.com → Account Settings → Developer apps → Personal access tokens
- Your Typeform Form ID (found in the form URL: typeform.com/to/FORM_ID or in form settings)
- A V0 account at v0.dev and a Vercel account for deployment
- For webhooks: your deployed Vercel URL (you need a live HTTPS URL to configure as a Typeform webhook destination)
Step-by-step guide
Generate the Response Dashboard UI with V0
Generate the Response Dashboard UI with V0
Start by generating your response analytics or form management dashboard UI in V0 using mock response data. Building the visual design first lets you decide on the information architecture before dealing with the specifics of the Typeform API response format. Typeform responses have a structured format: each response has a `submitted_at` timestamp, a `landed_at` timestamp, `response_id`, and an `answers` array. Each answer contains a `field` object (with the question ID and title), a `type` (one of `text`, `choice`, `choices`, `number`, `boolean`, `date`, `email`, `url`, `file_url`, `payment`, `ranking`), and the actual answer value in a type-specific field (`text`, `choice.label`, `choices.labels[]`, `number`, `boolean`, `date`). When asking V0 to generate your dashboard, describe the answer types you expect from your Typeform questions. For a customer satisfaction form, you might have: a star rating (type `number`), an NPS score (type `number`), a satisfaction choice (type `choice` with values 'Very satisfied', 'Satisfied', 'Neutral', 'Dissatisfied'), and an open text comment (type `text`). V0 should generate components that handle each answer type appropriately — numbers as bar charts or gauges, choices as pie/bar charts, text as a scrollable list. Also ask V0 to generate an empty state component for when no responses exist yet, and a loading state with skeleton placeholders for the response list. These states improve the user experience during the API call latency.
Build a Typeform response analytics dashboard with: a top stats row showing Total Responses (342), Average Completion Time (2m 14s), Response Rate (68%), and Today's Responses (12) as KPI cards. Below, show a line chart of daily responses for the past 14 days, a horizontal bar chart showing distribution of answers to a 5-option satisfaction question, and a recent responses table with columns: response ID, submitted date, completion time, and answer to the first question. Add date range filter buttons: Today / Last 7 days / Last 30 days / All time.
Paste this in V0 chat
Pro tip: Ask V0 to generate TypeScript interfaces for Typeform API response objects early in your project. This makes it much easier to navigate the nested response structure when you connect real API data.
Expected result: A complete response analytics dashboard renders in V0's sandbox with mock data. The layout, chart types, and table columns are validated. TypeScript interfaces for Typeform response data are defined and ready.
Create the Typeform Retrieve API Route
Create the Typeform Retrieve API Route
Create a Next.js API route that fetches responses from your Typeform form using the Typeform Retrieve API. The base URL is `https://api.typeform.com/forms/{formId}/responses`. Authentication uses a Bearer token in the Authorization header. The retrieve endpoint supports several useful query parameters: `page_size` (number of responses per page, max 1000), `since` and `until` (ISO 8601 date strings for filtering by submission date), `sort` (sort order — `submitted_at,desc` for newest first), `query` (full-text search across text answers), and `token` (pagination cursor for fetching the next page). The response object from the Typeform API contains a `total_items` count, `page_count`, and an `items` array of response objects. Each response has the `response_id`, `submitted_at`, `landed_at`, `metadata` (including browser, platform, and referrer), and the `answers` array. To parse answers into a useful format for your frontend, create a utility function that transforms the raw Typeform answer format into a flat key-value object keyed by question title. This makes it much easier to display specific answers in your dashboard without navigating the nested `answers` array for each response. For example, transform `{ field: { id: 'abc123', title: 'What is your satisfaction level?' }, type: 'choice', choice: { label: 'Very satisfied' } }` into `{ 'What is your satisfaction level?': 'Very satisfied' }`.
Add a date range filter to the analytics dashboard with four preset buttons (Today, Last 7 Days, Last 30 Days, All Time) and a custom date range picker. When a filter is selected, show a loading state and update all charts and the response table with the filtered data. The currently active filter button should be highlighted in indigo.
Paste this in V0 chat
1// app/api/typeform/responses/route.ts2import { NextRequest, NextResponse } from 'next/server'34const TYPEFORM_BASE_URL = 'https://api.typeform.com'56function parseAnswers(answers: any[]): Record<string, string | string[] | number | boolean> {7 const parsed: Record<string, string | string[] | number | boolean> = {}8 for (const answer of answers) {9 const questionTitle = answer.field?.title || answer.field?.id10 switch (answer.type) {11 case 'text':12 parsed[questionTitle] = answer.text13 break14 case 'choice':15 parsed[questionTitle] = answer.choice?.label || answer.choice?.other || ''16 break17 case 'choices':18 parsed[questionTitle] = answer.choices?.labels || []19 break20 case 'number':21 parsed[questionTitle] = answer.number22 break23 case 'boolean':24 parsed[questionTitle] = answer.boolean25 break26 case 'email':27 parsed[questionTitle] = answer.email28 break29 case 'date':30 parsed[questionTitle] = answer.date31 break32 default:33 parsed[questionTitle] = JSON.stringify(answer[answer.type] || '')34 }35 }36 return parsed37}3839export async function GET(request: NextRequest) {40 const { searchParams } = new URL(request.url)41 const formId = process.env.TYPEFORM_FORM_ID42 const since = searchParams.get('since') || ''43 const until = searchParams.get('until') || ''44 const pageSize = searchParams.get('page_size') || '50'4546 if (!formId) {47 return NextResponse.json({ error: 'TYPEFORM_FORM_ID not configured' }, { status: 500 })48 }4950 const params = new URLSearchParams({51 page_size: pageSize,52 sort: 'submitted_at,desc',53 ...(since && { since }),54 ...(until && { until }),55 })5657 try {58 const response = await fetch(59 `${TYPEFORM_BASE_URL}/forms/${formId}/responses?${params}`,60 {61 headers: {62 Authorization: `Bearer ${process.env.TYPEFORM_ACCESS_TOKEN}`,63 'Content-Type': 'application/json',64 },65 next: { revalidate: 60 }, // Cache for 60 seconds66 }67 )6869 if (!response.ok) {70 const error = await response.json()71 return NextResponse.json({ error: error.description || 'Typeform API error' }, { status: response.status })72 }7374 const data = await response.json()7576 const responses = data.items.map((item: any) => ({77 response_id: item.response_id,78 submitted_at: item.submitted_at,79 landed_at: item.landed_at,80 completion_time_seconds: item.metadata?.time_to_complete || 0,81 answers: parseAnswers(item.answers || []),82 raw_answers: item.answers || [],83 }))8485 return NextResponse.json({86 responses,87 total_items: data.total_items,88 page_count: data.page_count,89 })90 } catch (error) {91 return NextResponse.json({ error: 'Failed to fetch Typeform responses' }, { status: 500 })92 }93}Pro tip: Request up to 1000 responses per page with page_size=1000 when building analytics dashboards that need to aggregate all data. For large forms with thousands of responses, implement cursor-based pagination using the token parameter Typeform returns.
Expected result: The /api/typeform/responses route fetches real response data from Typeform and returns it as structured JSON with parsed answers. Testing with your Vercel preview URL should show actual form responses.
Create the Webhook Endpoint for Real-Time Submissions
Create the Webhook Endpoint for Real-Time Submissions
Webhooks allow your application to receive Typeform submissions in real time — the moment someone completes your form, Typeform sends the response data to your webhook URL. This is ideal for triggering automations: sending confirmation emails, creating CRM records, sending Slack notifications, or any other action that should happen immediately after form completion. Create a webhook handler at `app/api/typeform/webhook/route.ts`. This route accepts POST requests from Typeform. Typeform sends a JSON body containing the form response in the same format as the Retrieve API, plus a `form_id` and `form_response` object. For security, Typeform signs webhook payloads with an HMAC SHA256 signature using a webhook secret you configure. Verify this signature in your handler to ensure the request actually came from Typeform and not an attacker. The signature is sent in the `Typeform-Signature` header as `sha256=BASE64_ENCODED_SIGNATURE`. Compare it against a SHA256 HMAC of the raw request body using your webhook secret. The webhook handler should process the response quickly and return a 200 status. If your processing logic takes more than a few seconds (e.g., sending an email), use a fire-and-forget pattern: return 200 immediately and process asynchronously, or queue the work in a background job system. Typeform will retry webhook delivery if it receives a non-2xx response. After creating the webhook route, configure it in Typeform: go to your form in Typeform → Connect → Webhooks → Add Webhook. Enter your Vercel webhook URL (`https://your-app.vercel.app/api/typeform/webhook`), add your webhook secret, and click Save. Use the 'Test' button to send a sample payload and verify your handler processes it correctly.
Add a real-time notifications panel to the dashboard that shows a toast notification in the bottom-right corner whenever a new Typeform response is received. The toast should show 'New response received' with the submission time, auto-dismiss after 5 seconds, and there should be a 'View response' link that navigates to the latest response in the table.
Paste this in V0 chat
1// app/api/typeform/webhook/route.ts2import { NextRequest, NextResponse } from 'next/server'3import crypto from 'crypto'45function verifyTypeformSignature(6 payload: string,7 signature: string,8 secret: string9): boolean {10 const expectedSignature = crypto11 .createHmac('sha256', secret)12 .update(payload)13 .digest('base64')14 const expected = `sha256=${expectedSignature}`15 return crypto.timingSafeEqual(16 Buffer.from(signature),17 Buffer.from(expected)18 )19}2021export async function POST(request: NextRequest) {22 const rawBody = await request.text()23 const signature = request.headers.get('Typeform-Signature') || ''24 const webhookSecret = process.env.TYPEFORM_WEBHOOK_SECRET2526 // Verify webhook signature if secret is configured27 if (webhookSecret && signature) {28 const isValid = verifyTypeformSignature(rawBody, signature, webhookSecret)29 if (!isValid) {30 return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })31 }32 }3334 let body: any35 try {36 body = JSON.parse(rawBody)37 } catch {38 return NextResponse.json({ error: 'Invalid JSON payload' }, { status: 400 })39 }4041 const formResponse = body.form_response42 if (!formResponse) {43 return NextResponse.json({ error: 'No form_response in payload' }, { status: 400 })44 }4546 const responseId = formResponse.token47 const submittedAt = formResponse.submitted_at48 const answers = formResponse.answers || []4950 // Process the submission — examples:51 // 1. Save to database52 // 2. Send confirmation email53 // 3. Create CRM lead54 // 4. Send Slack notification5556 // Extract specific answers by field ref57 const emailAnswer = answers.find((a: any) => a.field?.ref === 'email_field')58 const respondentEmail = emailAnswer?.email || ''5960 console.log(`New Typeform submission: ${responseId} from ${respondentEmail} at ${submittedAt}`)6162 // Return 200 immediately to acknowledge receipt63 return NextResponse.json({ received: true, response_id: responseId })64}Pro tip: Configure Typeform's webhook secret (any random string you generate) and store it as TYPEFORM_WEBHOOK_SECRET in Vercel. Always verify the signature before processing webhook data — this prevents anyone from POSTing fake form submissions to your webhook URL.
Expected result: The webhook endpoint is live at your Vercel URL and configured in Typeform. Submitting a test response in Typeform triggers an immediate POST to your webhook handler, which logs the response and returns 200 successfully.
Configure Vercel Environment Variables and Complete the Integration
Configure Vercel Environment Variables and Complete the Integration
Add your Typeform credentials to Vercel to connect the full integration. Go to Vercel Dashboard → your project → Settings → Environment Variables and add three variables. Add `TYPEFORM_ACCESS_TOKEN` with your Personal Access Token from Typeform → Account Settings → Developer apps → Personal access tokens. This token authenticates all Typeform Retrieve API calls. Keep this token server-side only — it has access to all your Typeform forms and responses. Add `TYPEFORM_FORM_ID` with the ID of the specific form you want to display responses for. Find this in your Typeform form URL (typeform.com/to/FORM_ID) or in your form's Settings page. If you want to support multiple forms, you can make this a runtime parameter in your API route instead of a fixed environment variable. Add `TYPEFORM_WEBHOOK_SECRET` with the random secret string you use to verify webhook signatures. You set this same string in Typeform's webhook configuration. This prevents unauthorized requests to your webhook endpoint. After saving all environment variables, trigger a new Vercel deployment. Test the full flow: load your dashboard and verify it shows real Typeform responses from your form. Then submit a test response through your Typeform form and verify the webhook handler receives it and logs correctly. For production use, consider adding response data caching to avoid hitting Typeform's API rate limits (1,000 requests/hour on Pro plans). Use Next.js's `{ next: { revalidate: 60 } }` option in your fetch calls to cache responses for 60 seconds — this reduces API calls significantly for dashboards that multiple users view simultaneously.
Add a response export feature to the dashboard with an Export CSV button that downloads all filtered responses as a CSV file with columns for submission date, respondent email (if collected), and each question answer. The button should show a download progress indicator while generating the file.
Paste this in V0 chat
1// Vercel Dashboard → Settings → Environment Variables:2// TYPEFORM_ACCESS_TOKEN = your_personal_access_token3// TYPEFORM_FORM_ID = your_form_id_from_typeform_url4// TYPEFORM_WEBHOOK_SECRET = random_secret_string_for_webhook_verification56// To get your form ID from Typeform URL:7// https://yourworkspace.typeform.com/to/FORM_ID ← this is your form ID8// Or from Typeform → Forms → click your form → Settings → Basic settings → Form ID910// Typeform webhook configuration:11// Typeform → your form → Connect → Webhooks → Add webhook12// URL: https://your-app.vercel.app/api/typeform/webhook13// Secret: same value as TYPEFORM_WEBHOOK_SECRET14// Enable: turn on and click Save1516// Test the retrieve API route:17// GET https://your-app.vercel.app/api/typeform/responses18// Should return { responses: [...], total_items: N }Pro tip: Use Typeform's webhook 'Test' button in the form settings to send a sample payload with real answer structure to your webhook URL. Review the payload in your Vercel function logs (Dashboard → Functions → View logs) to understand the exact field references for your form's questions.
Expected result: The full integration works: the dashboard loads real Typeform responses, date filtering fetches responses within the selected range, webhook submissions are received and logged instantly, and all credentials are secured as Vercel environment variables.
Common use cases
Customer Feedback Analytics Dashboard
A product team uses a Typeform survey to collect customer satisfaction feedback. They want a V0-generated dashboard showing response trends over time, NPS scores, average ratings by question, and individual response details with the ability to filter by date range and score.
Build a survey analytics dashboard showing NPS score gauge at the top (score of 47 shown as a semi-circle gauge in green), a line chart of daily response count over the last 30 days, a bar chart of answers to 'How satisfied are you?' with 5 bars for ratings 1-5, and a scrollable list of recent responses showing respondent email, submission date, NPS score, and their open-text comment.
Copy this prompt to try it in V0
Lead Qualification Webhook Processor
A sales team uses a Typeform qualifying questionnaire on their website. When someone submits the form, a webhook triggers an API route that scores the lead based on their answers, creates a record in their CRM, and sends a customized follow-up email based on the qualification tier.
Create a lead intake dashboard that shows incoming Typeform leads in real-time. Display a table with columns: submitted date/time, name, email, company, job title, budget range (from Typeform choice answer), and a qualification score badge (Hot/Warm/Cold). Include a row action to mark a lead as 'Contacted' which updates the status badge.
Copy this prompt to try it in V0
Event Registration Response Manager
An event organizer uses Typeform for event registration. They need a V0-generated admin interface to view all registrations, export attendee lists, track meal preferences and t-shirt sizes from multiple choice answers, and see registration counts by session.
Build an event registration management page showing total registrations count (247), a breakdown of session choice selections as a horizontal bar chart, a filterable attendee table with name, email, session, meal preference, t-shirt size columns, and an Export to CSV button. Add a 'Send Reminder' button that appears when filtering by attendees who haven't confirmed.
Copy this prompt to try it in V0
Troubleshooting
401 Unauthorized error when calling the Typeform Retrieve API
Cause: The TYPEFORM_ACCESS_TOKEN environment variable is missing, invalid, or the token has been revoked. Personal access tokens can be revoked in Typeform's account settings.
Solution: Go to Typeform → Account Settings → Developer apps → Personal access tokens. Verify the token exists and is active. If you deleted and recreated it, update the TYPEFORM_ACCESS_TOKEN value in Vercel Dashboard → Settings → Environment Variables and redeploy.
Webhook signature verification fails — returning 401 for legitimate Typeform submissions
Cause: The webhook secret in TYPEFORM_WEBHOOK_SECRET does not match the secret configured in Typeform's webhook settings, or the request body is being parsed before signature verification (which modifies the raw bytes and breaks the HMAC).
Solution: Always read the raw request body as text before verifying the signature: const rawBody = await request.text(). Do not call request.json() before signature verification as JSON parsing can normalize the payload. Verify the TYPEFORM_WEBHOOK_SECRET value exactly matches what you entered in Typeform's webhook configuration.
1// Correct: read raw text BEFORE any JSON parsing2export async function POST(request: NextRequest) {3 const rawBody = await request.text() // Must be first4 const signature = request.headers.get('Typeform-Signature') || ''5 // Verify signature using rawBody...6 const body = JSON.parse(rawBody) // Parse after verification7}Typeform API returns 404 Not Found for the form responses endpoint
Cause: The TYPEFORM_FORM_ID is incorrect, or the Personal Access Token belongs to a different Typeform account than the one that owns the form.
Solution: Verify the form ID by opening your form in Typeform and checking the URL: the ID is the alphanumeric string at the end. Also verify the Personal Access Token belongs to the same Typeform account/workspace that owns the form — tokens are account-specific.
Response answers are missing or undefined in the dashboard
Cause: Typeform answers are keyed by field ref (ID), not by question title. If a respondent skips an optional question, that question has no entry in the answers array. The parseAnswers utility needs to handle missing answers gracefully.
Solution: Ensure your answer parsing function returns null or empty string for unanswered questions rather than throwing an error. When displaying answers in the dashboard, use optional chaining and default values.
1// Safe answer lookup with default value2function getAnswerByRef(answers: any[], fieldRef: string, defaultValue: string = ''): string {3 const answer = answers.find((a) => a.field?.ref === fieldRef)4 if (!answer) return defaultValue5 return answer[answer.type]?.label || answer[answer.type] || defaultValue6}Best practices
- Store TYPEFORM_ACCESS_TOKEN as a server-only Vercel environment variable (no NEXT_PUBLIC_ prefix) — this token has read access to all your Typeform forms and must never reach the browser.
- Always verify the Typeform webhook signature before processing — this prevents anyone from sending fake submissions to your webhook endpoint.
- Cache retrieve API responses with a short revalidation period (60-120 seconds) to avoid hitting Typeform's rate limits when multiple users view the dashboard simultaneously.
- Use Typeform field refs (not question titles) as identifiers in your answer parsing logic — field refs are stable IDs that do not change when you edit question text.
- Configure your webhook endpoint before going to production — polling the retrieve API is fine for dashboards, but real-time webhooks are essential for any automation that needs to react immediately to new submissions.
- Handle all Typeform answer types in your parsing function — responses may contain text, choice, choices, number, boolean, date, email, and file_url types depending on your form questions.
- Implement pagination when fetching more than 1000 responses — the Typeform API has a maximum page size of 1000, and you need to use cursor pagination for full response history on high-volume forms.
Alternatives
HubSpot includes its own form builder with native CRM integration and lead management, making it a better choice if you need forms tightly integrated with a marketing workflow rather than just data collection.
Mailchimp offers embedded signup forms that directly grow your email list, whereas Typeform is better for longer surveys and qualifying questionnaires.
Drip provides its own form and survey tools with built-in email automation triggered by responses, useful when you want to avoid building a separate webhook automation layer.
SharpSpring includes form tracking and lead scoring natively within a full marketing automation platform, removing the need to build custom webhook processing.
Frequently asked questions
Can I display Typeform responses in real-time without webhooks?
Yes, but with a delay. You can use polling — your dashboard periodically calls the retrieve API every 30-60 seconds to check for new responses. This is simpler to set up but introduces latency. For true real-time updates, configure a webhook that POSTs to your API route immediately after each submission, then use a server-sent event or WebSocket to push the new data to your dashboard.
How do I get my Typeform Personal Access Token?
Go to typeform.com and log in. Click your profile picture in the top-right corner → Settings → Developer apps tab → Personal tokens section → Generate a new token. Give it a descriptive name like 'V0 Dashboard Integration'. Copy the token immediately — Typeform only shows it once. Store it as TYPEFORM_ACCESS_TOKEN in your Vercel environment variables.
Can I fetch responses from multiple Typeform forms?
Yes. Instead of hardcoding the form ID as an environment variable, make it a runtime parameter in your API route. Pass the form ID as a query parameter: /api/typeform/responses?formId=ABC123. Your route handler reads process.env.TYPEFORM_ACCESS_TOKEN for auth but uses the dynamic form ID from the query parameter. This lets one deployment serve response dashboards for multiple Typeform forms.
Does Typeform have rate limits I need to worry about?
Yes. Typeform's Retrieve API has a rate limit of 1,000 requests per hour per access token. For dashboards with multiple concurrent users, implement response caching in your API route using Next.js fetch cache with { next: { revalidate: 60 } }. This reduces API calls significantly — a 60-second cache means at most 60 calls per hour regardless of user count, well within the rate limit.
Can V0 generate a form that embeds directly in my Next.js page instead of redirecting to Typeform?
Typeform provides an embed library (@typeform/embed-react) that lets you embed your Typeform form directly in your page as a full-page embed, popup, side panel, or popover. Ask V0 to generate a component that uses the Typeform embed library — npm install @typeform/embed-react. This keeps users on your site while still using Typeform's form engine, and responses are still accessible via the API and webhooks.
How do I handle Typeform file upload responses in my API route?
When a Typeform question allows file uploads, the answer type is file_url and the value is a temporary URL to the uploaded file hosted on Typeform's servers. These URLs expire after a short time. In your webhook handler or retrieve API processing, download and re-upload any file_url answers to your own storage (Vercel Blob, AWS S3, or Supabase Storage) immediately to preserve the files long-term.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation