Skip to main content
RapidDev - Software Development Agency
lovable-integrationsEdge Function Integration

How to Integrate Lovable with Typeform

Integrate Typeform with a Lovable app by embedding forms using the @typeform/embed-react SDK for conversational survey experiences, and creating a Supabase Edge Function that handles Typeform response webhooks and queries the Typeform API with a Personal Access Token stored in Cloud → Secrets. Typeform's conversational UI achieves significantly higher completion rates than traditional survey forms.

What you'll learn

  • How to embed a Typeform form natively in a Lovable React app using the @typeform/embed-react SDK
  • How to pass hidden fields to Typeform for user identification and context enrichment
  • How to create a Supabase Edge Function that receives and processes Typeform response webhooks
  • How to fetch form responses via the Typeform API and display analytics in a Lovable dashboard
  • How to trigger Typeform forms at the right moment in the user journey using React state management
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate20 min read30 minutesAnalyticsMarch 2026RapidDev Engineering Team
TL;DR

Integrate Typeform with a Lovable app by embedding forms using the @typeform/embed-react SDK for conversational survey experiences, and creating a Supabase Edge Function that handles Typeform response webhooks and queries the Typeform API with a Personal Access Token stored in Cloud → Secrets. Typeform's conversational UI achieves significantly higher completion rates than traditional survey forms.

Integrating Typeform Conversational Forms into Your Lovable App

Typeform's defining characteristic is completion rate — its one-question-at-a-time conversational interface achieves completion rates of 57% versus the industry average of 14% for traditional multi-question forms. For Lovable app builders, this means an in-app NPS survey, onboarding questionnaire, or feedback form is significantly more likely to be completed when implemented with Typeform's conversational format rather than a traditional multi-field form. The psychological principle is simple: a single question feels manageable where a page of questions feels like work.

Typeform integrates with Lovable through two complementary paths. The @typeform/embed-react SDK provides a native React embedding experience with popup, slider, and full-page display modes — the form renders as an overlay within your Lovable app, not in a separate browser window. This SDK is the recommended approach for in-app use, providing smooth animations, mobile-responsive behavior, and event callbacks for completion detection. The second path is the Typeform API — a REST API that provides programmatic access to forms, responses, and webhook configurations using a Personal Access Token.

Response data in Typeform flows through webhooks — Typeform POSTs a JSON payload to your Edge Function URL when each form is submitted, delivering the response within seconds. Your Edge Function parses the response, extracts answers and hidden field values, stores the structured data in Supabase, and triggers any follow-up actions. This real-time webhook pattern is more reliable than polling the API for new responses. Lovable's security infrastructure blocks approximately 1,200 hardcoded API keys daily and holds SOC 2 Type II certification. Your Typeform Personal Access Token must live in Cloud → Secrets and be accessed only from Edge Functions via Deno.env.get().

Integration method

Edge Function Integration

Typeform integrates with Lovable through the @typeform/embed-react npm package for seamless in-app form embedding and a Supabase Edge Function that processes response webhooks and queries the Typeform API. A Personal Access Token from Typeform's account settings authenticates API calls for fetching form responses and building analytics dashboards. The embed SDK handles the form UI entirely within your Lovable app without requiring an iframe URL.

Prerequisites

  • A Lovable account with an existing project that has Supabase and user authentication configured
  • A Typeform account — free plan includes 10 responses per month; paid plans start at $25/month for unlimited responses
  • At least one Typeform form created and published, with its Form ID copied from the form's Share → Embed settings
  • A Typeform Personal Access Token from My Account → Personal Tokens (required for API access and webhook configuration)
  • Basic understanding of Typeform's response data structure — answers are keyed by question ID (ref field), not by question text

Step-by-step guide

1

Create a Typeform form and get your Personal Access Token

Before embedding Typeform in your Lovable app, create the form you want to embed and gather the credentials needed for API access. Log in to your Typeform account at typeform.com. Click 'Create typeform' and select the type of form you want: a survey, quiz, or blank form. For NPS surveys, Typeform has a built-in NPS template — search 'NPS' in the templates gallery. For onboarding questionnaires, the blank form gives you the most flexibility. When building the form, add hidden fields for user identification. Hidden fields store values passed from your Lovable app alongside each response without displaying them to the respondent. Go to your form's Logic section → Hidden Fields and add a field named 'user_id' (you can also add 'user_email', 'user_plan', or any other context you want to pass). These field names will be used as URL parameters when embedding the form. To get your Form ID for embedding, click the Share button in the form editor header. Under 'Share link', the URL will look like https://yourname.typeform.com/to/XXXXXXXX — the alphanumeric string after '/to/' is your Form ID. Copy this for use as VITE_TYPEFORM_FORM_ID. For API access and webhook management, generate a Personal Access Token. Go to your Typeform account by clicking your profile picture → My Account → Personal Tokens → Generate a new token. Give it a name like 'Lovable Integration', select the scopes you need (at minimum: forms:read and responses:read for reading data; webhooks:write for creating webhooks), and click Generate Token. Copy the token immediately — you will only see it once. Go to your Lovable project, open the Cloud tab, click Secrets, and add TYPEFORM_ACCESS_TOKEN with your token value. Also add VITE_TYPEFORM_FORM_ID with your form ID for the frontend.

Pro tip: Typeform's question types each have a unique 'ref' field that identifies the question in the API response data. When you build your form, set meaningful refs for each question (like 'nps_score' or 'verbatim_feedback') in the question settings — this makes parsing webhook responses much more readable than using auto-generated IDs like 'abc123def456'.

Expected result: Your Typeform form is created and published with hidden fields configured. TYPEFORM_ACCESS_TOKEN and VITE_TYPEFORM_FORM_ID are stored in Lovable Cloud Secrets. The form's Share → Embed section shows the Form ID in the embed URL.

2

Install @typeform/embed-react and create the Typeform embed component

Install the @typeform/embed-react npm package and create a React component that embeds your Typeform form. The embed SDK provides several display modes: widget (inline, renders within the page layout), popup (modal overlay), slider (slides in from side or bottom), and side-tab (a tab that opens from the edge of the screen). For most in-app feedback use cases, popup or slider works best — they appear on demand without taking over the page. The @typeform/embed-react package provides typed React components for each embed mode: PopupButton, SliderButton, Widget, and PopupModal. Each component accepts a formId prop (your Typeform form ID), a hidden prop for hidden field values, onSubmit and onClose callbacks, and mode-specific styling props. The hidden prop is the key to user identification — pass an object with your hidden field names and values. For example: hidden={{ user_id: currentUserId, user_plan: currentPlan }}. Typeform URL-encodes these values and passes them to the form via query parameters, which your Edge Function webhook handler then reads from the response payload. This links every Typeform response back to the corresponding Supabase user without asking users for their email address in the form itself. For the onSubmit callback, Typeform calls this function when the form is successfully submitted. Use it to close the modal if using PopupButton, update the user's Supabase profile to mark the form as submitted (preventing repeat displays), and optionally show a thank-you message. The onSubmit callback receives a response_id that you can store for later lookup. For conditional display — showing the form only to users who have not already seen it — check a survey_completed field in the user's Supabase profile before rendering the Typeform component. Gate the form display with a useEffect that checks this field after authentication.

Lovable Prompt

Install @typeform/embed-react and create a TypeformSurvey component. Use PopupButton mode with the form ID from VITE_TYPEFORM_FORM_ID. Pass the current user's Supabase ID as a hidden field named user_id. On onSubmit, update the user's profile in Supabase to set typeform_survey_completed: true and show a toast notification thanking them. On onClose without submission, update profile with typeform_survey_dismissed: true to rate-limit how often the prompt shows.

Paste this in Lovable chat

src/components/TypeformSurvey.tsx
1// src/components/TypeformSurvey.tsx
2import { useEffect, useState } from 'react';
3import { PopupButton } from '@typeform/embed-react';
4import { supabase } from '@/integrations/supabase/client';
5import { useToast } from '@/hooks/use-toast';
6
7interface TypeformSurveyProps {
8 userId: string;
9 triggerLabel?: string;
10 autoOpen?: boolean;
11}
12
13const FORM_ID = import.meta.env.VITE_TYPEFORM_FORM_ID;
14
15export function TypeformSurvey({
16 userId,
17 triggerLabel = 'Share Feedback',
18 autoOpen = false,
19}: TypeformSurveyProps) {
20 const { toast } = useToast();
21 const [completed, setCompleted] = useState(false);
22
23 useEffect(() => {
24 // Check if user has already completed the survey
25 async function checkStatus() {
26 const { data } = await supabase
27 .from('profiles')
28 .select('typeform_survey_completed')
29 .eq('id', userId)
30 .single();
31 if (data?.typeform_survey_completed) setCompleted(true);
32 }
33 checkStatus();
34 }, [userId]);
35
36 const handleSubmit = async ({ responseId }: { responseId: string }) => {
37 await supabase
38 .from('profiles')
39 .update({ typeform_survey_completed: true, typeform_response_id: responseId })
40 .eq('id', userId);
41 setCompleted(true);
42 toast({ title: 'Thank you for your feedback!', description: 'Your response has been recorded.' });
43 };
44
45 if (!FORM_ID || completed) return null;
46
47 return (
48 <PopupButton
49 id={FORM_ID}
50 hidden={{ user_id: userId }}
51 onSubmit={handleSubmit}
52 autoClose={3}
53 size={75}
54 className="px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm font-medium"
55 open={autoOpen ? 'load' : undefined}
56 >
57 {triggerLabel}
58 </PopupButton>
59 );
60}

Pro tip: The autoOpen prop with open='load' causes the Typeform popup to open immediately when the component mounts — useful for showing the form automatically after a trigger event like completing onboarding. Set a delay using the open-value attribute (open='time' with openValue={5000} for 5 seconds) to give users a moment to settle on the page before the form appears.

Expected result: The TypeformSurvey component renders a button that opens the Typeform form as a popup. The form displays correctly with one-question-at-a-time navigation. Completing the form closes the popup and updates the user's profile. The component does not render for users who have already completed the survey.

3

Create a webhook receiver Edge Function for real-time responses

Typeform webhooks deliver response data to your Edge Function URL within seconds of form submission, enabling immediate follow-up actions and real-time dashboards. Before creating the Edge Function, understand Typeform's webhook payload structure — it differs from other webhook systems in how answers are structured. A Typeform webhook payload is a JSON object with a form_response top-level key. Inside form_response, the key fields are: form_id, event_id (a unique webhook delivery ID), submitted_at (ISO timestamp), hidden (an object containing the hidden field values you passed during embedding — like { user_id: 'abc123' }), calculated (aggregate scores if configured), and answers (an array of answer objects). Each answer object has a field key identifying which question was answered (using the question's ref), a type indicating the answer format (number, text, choice, etc.), and a value key containing the actual answer. Answers with type 'number' contain the numeric value (for NPS scores, a number from 0-10). Answers with type 'text' or 'long_text' contain the verbatim string. Answers with type 'choice' contain a { label, ref } object. To set up the webhook in Typeform, go to your form → Connect → Webhooks → Add Webhook. Enter your Edge Function URL (https://[project-ref].supabase.co/functions/v1/typeform-webhook) and enable the webhook. Typeform will POST to this URL when each form is submitted. Typeform also supports webhook signature verification using an HMAC secret — if you enable this in Typeform's webhook settings, your Edge Function should verify the signature before processing the payload. In the Edge Function, extract the answers from the payload, parse the NPS score (from the question with ref 'nps_score'), the verbatim feedback (from the question with ref 'verbatim_feedback'), and the hidden user_id. Store the structured data in your Supabase database. Add business logic for follow-up actions: create support tickets for detractors, send thank-you messages to promoters, or update the user's profile with their NPS score.

Lovable Prompt

Create a Supabase Edge Function called 'typeform-webhook' that receives POST requests from Typeform. Parse the form_response.answers array to extract: the answer with field ref 'nps_score' (type number), the answer with ref 'verbatim_feedback' (type text). Get the user_id from form_response.hidden.user_id. Store in Supabase 'nps_responses' table: response_id (event_id), user_id, nps_score, verbatim_feedback, submitted_at. For nps_score below 7, insert into 'follow_up_queue' with status 'pending'.

Paste this in Lovable chat

supabase/functions/typeform-webhook/index.ts
1// supabase/functions/typeform-webhook/index.ts
2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
4
5const corsHeaders = {
6 'Access-Control-Allow-Origin': '*',
7 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
8};
9
10function getAnswerValue(answers: Array<{ field: { ref: string }; type: string; number?: number; text?: string; long_text?: string }>, ref: string) {
11 const answer = answers.find(a => a.field?.ref === ref);
12 if (!answer) return null;
13 if (answer.type === 'number') return answer.number;
14 if (answer.type === 'text') return answer.text;
15 if (answer.type === 'long_text') return answer.long_text;
16 return null;
17}
18
19serve(async (req) => {
20 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders });
21 if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
22
23 try {
24 const supabase = createClient(
25 Deno.env.get('SUPABASE_URL') ?? '',
26 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
27 );
28
29 const body = await req.json();
30 const formResponse = body.form_response;
31
32 if (!formResponse) {
33 return new Response(JSON.stringify({ error: 'Invalid Typeform payload' }), {
34 status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
35 });
36 }
37
38 const answers = formResponse.answers ?? [];
39 const hidden = formResponse.hidden ?? {};
40 const npsScore = getAnswerValue(answers, 'nps_score') as number | null;
41 const verbatim = getAnswerValue(answers, 'verbatim_feedback') as string | null;
42 const userId = hidden.user_id ?? null;
43
44 const { error } = await supabase.from('nps_responses').upsert({
45 response_id: formResponse.event_id,
46 form_id: formResponse.form_id,
47 user_id: userId,
48 nps_score: npsScore,
49 verbatim_feedback: verbatim,
50 submitted_at: formResponse.submitted_at,
51 raw_answers: answers,
52 }, { onConflict: 'response_id' });
53
54 if (error) console.error('DB insert error:', error);
55
56 if (npsScore !== null && npsScore < 7) {
57 await supabase.from('follow_up_queue').insert({
58 user_id: userId,
59 response_id: formResponse.event_id,
60 nps_score: npsScore,
61 status: 'pending',
62 priority: npsScore <= 3 ? 'high' : 'medium',
63 });
64 }
65
66 return new Response(JSON.stringify({ success: true }), {
67 headers: { ...corsHeaders, 'Content-Type': 'application/json' },
68 });
69 } catch (error) {
70 console.error('Webhook error:', error);
71 return new Response(JSON.stringify({ error: String(error) }), {
72 status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
73 });
74 }
75});

Pro tip: Typeform delivers webhooks with a Typeform-Signature header when you configure a webhook secret. Verify this signature in your Edge Function for security: compute HMAC-SHA256 of the raw request body using your webhook secret and compare it to the header value. This prevents malicious requests from other sources from injecting fake response data.

Expected result: The typeform-webhook Edge Function is deployed and its URL is registered in Typeform's webhook settings. Submitting the form creates a new row in the nps_responses Supabase table within seconds. The user_id from the hidden field is correctly captured in the response record.

4

Fetch form responses via the Typeform API for analytics

Build a Supabase Edge Function that fetches form responses from the Typeform API for building analytics dashboards. While the webhook approach handles real-time processing of individual responses, the API is useful for fetching all historical responses for analysis, building summary statistics, and populating dashboards with aggregate data. Typeform's API responses endpoint is GET https://api.typeform.com/forms/{form_id}/responses. Authentication uses the Authorization header with 'Bearer [personal_access_token]'. The endpoint accepts pagination parameters (page_size, before, after) and filters (completed for only completed responses, since and until for date filtering). Responses are paginated — each page returns up to 1,000 responses by default. Create a proxy Edge Function that accepts a form_id parameter, fetches responses from the Typeform API using the TYPEFORM_ACCESS_TOKEN secret, and optionally stores the aggregate statistics in your Supabase database for fast dashboard rendering. For NPS dashboards, calculate the NPS score from the responses: (promoters% - detractors%) where promoters are scores 9-10 and detractors are scores 0-6. To display Typeform analytics inside your Lovable app, create a React component that calls the proxy Edge Function and renders summary statistics — total responses, NPS score trend over time, answer distribution for each question, and a feed of recent verbatim comments. For complex Typeform analytics implementations including multi-form dashboards and respondent segmentation, RapidDev's team can help build the data pipeline and visualization components.

Lovable Prompt

Create a Supabase Edge Function called 'typeform-proxy' that fetches form responses from the Typeform API. Accept a POST request with form_id and optional date_from/date_to filters. Use TYPEFORM_ACCESS_TOKEN as a Bearer token. Return the responses array and calculate aggregate NPS stats: total_responses, promoters_count (scores 9-10), passives_count (scores 7-8), detractors_count (scores 0-6), and nps_score (promoters% - detractors%). Include CORS headers.

Paste this in Lovable chat

supabase/functions/typeform-proxy/index.ts
1// supabase/functions/typeform-proxy/index.ts
2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
3
4const corsHeaders = {
5 'Access-Control-Allow-Origin': '*',
6 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
7};
8
9serve(async (req) => {
10 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders });
11
12 try {
13 const token = Deno.env.get('TYPEFORM_ACCESS_TOKEN');
14 if (!token) {
15 return new Response(JSON.stringify({ error: 'TYPEFORM_ACCESS_TOKEN not configured' }), {
16 status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
17 });
18 }
19
20 const { form_id, date_from, date_to, nps_question_ref = 'nps_score' } = await req.json();
21 if (!form_id) {
22 return new Response(JSON.stringify({ error: 'form_id required' }), {
23 status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
24 });
25 }
26
27 let url = `https://api.typeform.com/forms/${form_id}/responses?page_size=200&completed=true`;
28 if (date_from) url += `&since=${date_from}`;
29 if (date_to) url += `&until=${date_to}`;
30
31 const response = await fetch(url, {
32 headers: { 'Authorization': `Bearer ${token}` },
33 });
34
35 if (!response.ok) {
36 const err = await response.text();
37 return new Response(JSON.stringify({ error: `Typeform API: ${response.status}`, detail: err }), {
38 status: response.status, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
39 });
40 }
41
42 const data = await response.json();
43 const responses = data.items ?? [];
44
45 // Calculate NPS stats
46 const npsAnswers: number[] = responses.flatMap((r: { answers: Array<{ field: { ref: string }; type: string; number?: number }> }) =>
47 (r.answers ?? [])
48 .filter((a) => a.field?.ref === nps_question_ref && a.type === 'number')
49 .map((a) => a.number ?? 0)
50 );
51
52 const promoters = npsAnswers.filter(s => s >= 9).length;
53 const detractors = npsAnswers.filter(s => s <= 6).length;
54 const total = npsAnswers.length;
55 const npsScore = total > 0
56 ? Math.round(((promoters - detractors) / total) * 100)
57 : null;
58
59 return new Response(JSON.stringify({
60 total_responses: responses.length,
61 nps_stats: { total, promoters, passives: total - promoters - detractors, detractors, nps_score: npsScore },
62 responses: responses.slice(0, 50), // Return first 50 for display
63 }), {
64 headers: { ...corsHeaders, 'Content-Type': 'application/json' },
65 });
66 } catch (error) {
67 return new Response(JSON.stringify({ error: String(error) }), {
68 status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
69 });
70 }
71});

Pro tip: Typeform's API rate limits responses to 2 requests per second per token. For dashboards that display data for multiple forms simultaneously, fetch data for each form sequentially rather than in parallel to avoid hitting this limit. Cache the aggregate stats in Supabase with a 15-minute TTL to minimize API calls.

Expected result: The typeform-proxy Edge Function fetches form responses and returns NPS statistics including total responses, promoter/detractor counts, and the calculated NPS score. Calling it with a valid form_id returns the aggregate data for use in dashboard components.

Common use cases

Collect onboarding information with a conversational form

Replace a multi-field signup form with a Typeform conversational questionnaire that collects the user's role, team size, use case, and goals one question at a time. The higher completion rate ensures more users complete onboarding setup, improving activation. Pass the user's Supabase ID as a hidden field so responses can be matched to the correct user profile.

Lovable Prompt

Create an OnboardingForm component using @typeform/embed-react that shows a Typeform form as a popup after signup. Use the form ID from VITE_TYPEFORM_ONBOARDING_ID. Pass the user's Supabase user ID as a hidden field named 'user_id'. On form submission (onSubmit callback), update the user's Supabase profile to set onboarding_completed: true and close the popup. Use the SliderButton embed mode so the form opens smoothly from the bottom of the screen.

Copy this prompt to try it in Lovable

Run an in-app NPS survey via conversational form

Deploy a quarterly NPS survey using a Typeform form embedded as a popup that appears after users have been active for 30 days. The conversational format (score question, then follow-up 'why' question) collects both the numeric NPS score and qualitative verbatim feedback in a single form that takes under 60 seconds to complete. Webhook processing stores results in Supabase and triggers follow-up workflows.

Lovable Prompt

Create a Supabase Edge Function called 'typeform-webhook' that receives Typeform response webhooks for the NPS form. Extract the hidden user_id field, the NPS score answer, and the verbatim feedback answer from the response payload. Store in a 'nps_responses' Supabase table with columns: response_id, user_id, nps_score, verbatim, submitted_at. For NPS scores below 7, also insert a row into 'support_follow_ups' with status 'pending' and priority 'high'.

Copy this prompt to try it in Lovable

Build a form response analytics dashboard

Create an in-app dashboard that fetches Typeform response data via the Typeform API and displays aggregate analytics — response counts over time, answer distribution for each question, average NPS score trend, and verbatim response feed. This gives team members a self-serve analytics view without requiring Typeform account access.

Lovable Prompt

Create a TypeformAnalytics page that fetches form responses from the Typeform API via a Supabase Edge Function called 'typeform-proxy'. For a specific form ID (from VITE_TYPEFORM_NPS_ID), display: total responses this month, NPS score distribution as a bar chart (0-10), a feed of the 10 most recent verbatim comments, and a weekly response count trend chart. Fetch from the Typeform API responses endpoint and store in Supabase for caching.

Copy this prompt to try it in Lovable

Troubleshooting

The Typeform popup does not open when the PopupButton is clicked

Cause: The form ID is undefined (VITE_TYPEFORM_FORM_ID is not set as a build environment variable), the @typeform/embed-react CSS is not loaded, or a Content Security Policy is blocking the Typeform script loading.

Solution: Check that VITE_TYPEFORM_FORM_ID is set in Lovable's environment configuration as a build variable, not just in Cloud Secrets. In the browser console, check for errors related to typeform.com being blocked. Import the embed CSS in your main.tsx or App.tsx: import '@typeform/embed-react/build/css/popup.css'. Verify the Form ID is the alphanumeric string from the Typeform share URL, not the full URL.

typescript
1// Add to src/main.tsx or src/App.tsx:
2import '@typeform/embed-react/build/css/popup.css';

Hidden fields are not appearing in Typeform webhook responses — the hidden object is empty

Cause: The hidden fields are not defined in the Typeform form's Logic → Hidden Fields section, the field names in the hidden prop do not exactly match the field names configured in Typeform, or the form was embedded without the hidden prop.

Solution: Log in to Typeform, open the form editor, go to Logic → Hidden Fields and verify that fields named 'user_id' (and any others you are passing) are listed there. The field names are case-sensitive and must exactly match the keys in the hidden prop object passed to the React embed component. After adding hidden fields in Typeform, test by submitting a new response and checking the webhook payload in Cloud Logs.

Typeform API returns 401 when called from the Edge Function

Cause: The TYPEFORM_ACCESS_TOKEN secret is missing from Cloud Secrets, has expired, or the token scopes do not include the 'responses:read' permission needed to access form responses.

Solution: Go to Cloud → Secrets and verify TYPEFORM_ACCESS_TOKEN is present. In your Typeform account, go to My Account → Personal Tokens and check that the token still exists and has responses:read scope. If the token was recently regenerated, update the Cloud Secret with the new value and redeploy the Edge Function. Typeform Personal Access Tokens do not expire automatically but can be revoked.

Webhook events arrive but the NPS score is null in the Supabase table

Cause: The question ref used in getAnswerValue() does not match the actual ref set in Typeform for the NPS question — either the question ref was not customized and uses an auto-generated value, or a typo exists between the Edge Function code and the Typeform form configuration.

Solution: Log the full answers array from the webhook payload in Cloud Logs to see the actual field refs: console.log('Answers:', JSON.stringify(formResponse.answers)). Compare the field.ref values in the log output against what your Edge Function expects. Update either the form's question refs in Typeform (Logic → Hidden Fields, then each question's Settings → Reference) or the Edge Function code to use the correct ref value.

typescript
1// Add to Edge Function to debug answer refs:
2console.log('Answer refs:', JSON.stringify(answers.map((a: { field: { ref: string }; type: string }) => ({ ref: a.field?.ref, type: a.type }))));

Best practices

  • Always set meaningful question refs in Typeform (Logic → question Settings → Reference) before deploying — auto-generated refs like 'abc123' make webhook payload parsing opaque and difficult to maintain when the form changes.
  • Pass the Supabase user ID as a hidden field in every Typeform embed so all responses are linked to specific users in your database — this enables personalized follow-up and prevents orphaned response records.
  • Implement upsert with response_id as the unique key in your Supabase nps_responses table — Typeform may deliver duplicate webhook events for the same response in rare cases, and upsert prevents duplicate rows.
  • Store the raw answers JSONB alongside extracted structured fields in your database — Typeform question structures change when forms are edited, and raw data preservation allows re-extraction of additional fields later without re-fetching from the API.
  • Trigger Typeform surveys at high-intent moments — after a successful purchase, after completing onboarding, or after a user has been active for 30 days — rather than on every page load. High-intent timing dramatically improves completion rates.
  • Use Typeform's logic jumps to show different follow-up questions based on the NPS score — ask detractors 'What could we improve?' and ask promoters 'What would you tell a friend about us?' This personalizes the survey experience and collects more actionable feedback.
  • Rate-limit survey display using the typeform_survey_completed and typeform_survey_dismissed fields in user profiles — do not show the same survey again to users who have already responded or explicitly dismissed it.

Alternatives

Frequently asked questions

Is Typeform free to use with a Lovable app?

Typeform's free plan includes 10 responses per month, which is useful for initial testing but insufficient for production use. The Basic plan ($29/month) allows 100 responses per month, while the Plus plan ($59/month) is unlimited. The @typeform/embed-react SDK is free to use regardless of plan. API access for webhook configuration and programmatic response retrieval is available on all paid plans.

What is the difference between Typeform and SurveyMonkey for in-app forms?

Typeform displays one question at a time in a conversational flow, with animations and progress indicators that feel more like an app than a form — this achieves completion rates of 50-60% compared to 14-20% for traditional multi-question surveys. SurveyMonkey displays all questions on a page in a traditional survey format, which is familiar to enterprise users and better for long, structured surveys. Choose Typeform for short (under 10 questions) customer-facing feedback forms where completion rate matters most.

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

Pass the Supabase user's UUID as a hidden field in the Typeform embed using the hidden prop: hidden={{ user_id: userId }}. First, define a hidden field named 'user_id' in Typeform's Logic → Hidden Fields section. When the form is submitted, the webhook payload includes this value in formResponse.hidden.user_id. Store this user_id in your Supabase table to link responses to specific user profiles.

Can I trigger a Typeform form automatically without the user clicking a button?

Yes. The @typeform/embed-react PopupButton component accepts an open prop — set it to 'load' to open the form immediately when the component mounts, or set it to 'time' with an openValue in milliseconds for a delayed auto-open. The PopupModal component (different from PopupButton) can also be controlled programmatically using a ref and calling the .open() method to trigger the form in response to any application event.

How secure is Typeform webhook data — should I verify webhook signatures?

Typeform supports webhook signature verification using HMAC-SHA256. Configure a webhook secret in Typeform's webhook settings, then verify the Typeform-Signature header in your Edge Function by computing HMAC-SHA256 of the raw request body and comparing it to the header value. This prevents malicious actors from sending fake form submission data to your webhook URL. For production applications handling sensitive data (like NPS responses with user identity), webhook signature verification is strongly recommended.

Can I show different questions to different users in a Typeform form?

Yes, using Typeform's Logic Jumps feature. Define conditions in the Logic panel that skip or show questions based on previous answers or hidden field values. For example, show a follow-up question only to users who give an NPS score below 7 — this is configured in Typeform's interface without any changes to your Lovable app code. Hidden fields (passed from your app) can also be used in logic conditions to personalize the question flow based on the user's profile data.

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.