To integrate Replit with Typeform, generate a Personal Access Token from your Typeform account settings, store it in Replit Secrets (lock icon π), and call the Typeform API v1 from your Python or Node.js server to retrieve form definitions, fetch responses, and receive real-time webhook events when someone submits a form. Use Autoscale deployment for webhook-driven data pipelines.
Why Connect Replit to Typeform?
Typeform's conversational one-question-at-a-time format consistently achieves higher completion rates than traditional multi-field forms β often 3-5x higher for longer surveys. The Typeform API provides programmatic access to form definitions and responses, making it possible to build custom downstream pipelines that automatically process submissions without manual CSV exports.
The most impactful integration patterns are: receiving webhook notifications when forms are submitted and immediately routing the data to your CRM, email list, or database; pulling batch response data for analysis; and programmatically creating or updating forms for dynamic questionnaire generation. Typeform's hidden fields feature also allows you to pass customer context (like a user ID or account name) into the form at load time, which then comes back with the response β enabling seamless attribution without asking the user to fill in data your app already has.
Replit's Secrets system (lock icon π in the sidebar) keeps your Typeform Personal Access Token encrypted and separate from your codebase. The token grants access to all forms and responses in your Typeform account, including potentially sensitive survey data. Always call the Typeform API from your Replit backend β never from client-side JavaScript that runs in the user's browser.
Integration method
You connect Replit to Typeform by generating a Personal Access Token from your Typeform account settings, storing it in Replit Secrets, and calling the Typeform API v1 from your server-side Python or Node.js code. All requests use Bearer token authentication. Typeform webhooks deliver form submissions to your Replit server in real time, enabling instant data processing without polling.
Prerequisites
- A Replit account with a Python or Node.js project created
- A Typeform account (free plan includes API access for up to 10 responses per month; paid plans for full access)
- At least one Typeform form created with some responses
- Basic familiarity with REST APIs and webhook handling
- Python 3.10+ or Node.js 18+ (both available on Replit by default)
Step-by-step guide
Generate a Typeform Personal Access Token
Generate a Typeform Personal Access Token
Log in to your Typeform account at app.typeform.com. Click your avatar or profile icon in the top-right corner of the dashboard. Select 'Account' from the dropdown menu. In the Account settings, look for the 'Personal access tokens' section and click 'Generate a new token'. Give the token a descriptive name like 'Replit Integration'. Select the scopes you need: 'Read forms', 'Read responses', and optionally 'Write forms' if you plan to create or update forms programmatically. Click 'Generate token' and copy the generated token immediately β it is shown only once. Open your Replit project and click the lock icon π in the left sidebar to open the Secrets pane. Add a new Secret with key TYPEFORM_TOKEN and paste your Personal Access Token as the value. If you also plan to verify webhook signatures, add TYPEFORM_SECRET with a value you choose (you will use this when registering the webhook in Typeform). All Typeform API requests use Bearer token authentication: include 'Authorization: Bearer {token}' in the request headers. The Typeform API base URL is https://api.typeform.com. In Python, access your token with os.environ['TYPEFORM_TOKEN']; in Node.js, use process.env.TYPEFORM_TOKEN.
Pro tip: Typeform Personal Access Tokens do not expire but can be revoked manually. Create a separate token for each integration so you can revoke one without affecting others. Token scopes are set at creation time and cannot be changed β create a new token with updated scopes if needed.
Expected result: TYPEFORM_TOKEN and TYPEFORM_SECRET appear in the Replit Secrets pane. A test API call to GET /me returns your Typeform account information, confirming the token is valid.
Retrieve Forms and Responses in Python
Retrieve Forms and Responses in Python
The Typeform API v1 has a consistent structure: GET /forms returns all forms, GET /forms/{form_id} returns a single form definition, and GET /forms/{form_id}/responses returns paginated response data. Forms have a unique ID visible in the Typeform editor URL. Processing Typeform responses requires mapping answer values back to the field definitions in the form. Each response contains an answers array where each item has a field.id reference and a typed value (text, email, number, choice, choices, date, etc.). To get human-readable field names, you cross-reference the response answers with the form's fields array. The Python code below demonstrates how to list forms, fetch responses, and parse the answer data into a flat dictionary with field titles as keys. This is the most common preprocessing step before sending response data to a CRM, database, or analytics tool.
1import os2import requests3from typing import Optional45TOKEN = os.environ["TYPEFORM_TOKEN"]6BASE_URL = "https://api.typeform.com"78HEADERS = {9 "Authorization": f"Bearer {TOKEN}",10 "Content-Type": "application/json"11}1213def list_forms() -> list:14 """Get all forms in the Typeform account."""15 response = requests.get(f"{BASE_URL}/forms", headers=HEADERS)16 response.raise_for_status()17 return response.json().get("items", [])1819def get_form(form_id: str) -> dict:20 """Get the full definition of a form including field names."""21 response = requests.get(f"{BASE_URL}/forms/{form_id}", headers=HEADERS)22 response.raise_for_status()23 return response.json()2425def get_responses(form_id: str, page_size: int = 25,26 since: str = None, until: str = None) -> dict:27 """Fetch form responses with optional date range filtering."""28 params = {"page_size": page_size}29 if since:30 params["since"] = since # ISO 8601 datetime string31 if until:32 params["until"] = until3334 response = requests.get(35 f"{BASE_URL}/forms/{form_id}/responses",36 params=params,37 headers=HEADERS38 )39 response.raise_for_status()40 return response.json()4142def build_field_map(form: dict) -> dict:43 """Build a map from field ID to field title for answer parsing."""44 field_map = {}45 for field in form.get("fields", []):46 field_map[field["id"]] = field.get("title", field["id"])47 # Handle group fields (pages/sets)48 for sub_field in field.get("properties", {}).get("fields", []):49 field_map[sub_field["id"]] = sub_field.get("title", sub_field["id"])50 return field_map5152def parse_answer(answer: dict) -> any:53 """Extract the value from a Typeform answer regardless of type."""54 answer_type = answer.get("type")55 if answer_type == "text":56 return answer.get("text")57 elif answer_type == "email":58 return answer.get("email")59 elif answer_type == "number":60 return answer.get("number")61 elif answer_type == "choice":62 return answer.get("choice", {}).get("label")63 elif answer_type == "choices":64 return [c.get("label") for c in answer.get("choices", {}).get("labels", [])]65 elif answer_type == "boolean":66 return answer.get("boolean")67 elif answer_type == "date":68 return answer.get("date")69 elif answer_type == "file_url":70 return answer.get("file_url")71 return answer.get(answer_type) # Fallback for other types7273def responses_to_dicts(form_id: str) -> list:74 """Convert raw API responses into flat dicts with field names as keys."""75 form = get_form(form_id)76 field_map = build_field_map(form)77 responses = get_responses(form_id)78 79 results = []80 for item in responses.get("items", []):81 response_dict = {82 "response_id": item["response_id"],83 "submitted_at": item["submitted_at"],84 "email": item.get("hidden", {}).get("email", "") # Hidden fields85 }86 for answer in item.get("answers", []):87 field_id = answer.get("field", {}).get("id", "")88 field_title = field_map.get(field_id, field_id)89 response_dict[field_title] = parse_answer(answer)90 results.append(response_dict)91 return results9293# Example usage94if __name__ == "__main__":95 forms = list_forms()96 print(f"Forms in account: {len(forms)}")97 for form in forms:98 print(f" {form['id']}: {form['title']}")99100 if forms:101 form_id = forms[0]["id"]102 responses = responses_to_dicts(form_id)103 print(f"\nLatest {len(responses)} responses for '{forms[0]['title']}':")104 for r in responses[:3]:105 print(f" {r['submitted_at']}: {r}")Pro tip: Typeform hidden fields let you pass custom data into a form via URL parameters (e.g., ?user_id=123&plan=pro). These values appear in the response's 'hidden' object alongside the answers, giving you customer context without asking users to re-enter it.
Expected result: Running the script lists all Typeform forms and prints the parsed responses for the first form with field names as keys rather than field IDs.
Set Up Real-Time Webhooks with Signature Verification
Set Up Real-Time Webhooks with Signature Verification
Typeform webhooks deliver form submissions to your Replit server within seconds of the form being submitted. This is far more efficient than polling the responses API β especially for lead capture forms where immediate CRM entry matters. To register a webhook, you can either use the Typeform API (POST /forms/{form_id}/webhooks) or set it up in the Typeform UI under your form's Connect > Webhooks section. The webhook requires a URL and optionally a 'secret' for HMAC signature verification. Typeform signs each webhook delivery with SHA-256 using your secret β always verify the signature to prevent accepting fake payloads. The webhook payload mirrors the structure of the GET /responses endpoint β each delivery contains a single response item with all answers. The Express server below handles the webhook with signature verification and response parsing. Install 'npm install express axios' in the Replit shell.
1const express = require('express');2const axios = require('axios');3const crypto = require('crypto');45const app = express();67const TOKEN = process.env.TYPEFORM_TOKEN;8const WEBHOOK_SECRET = process.env.TYPEFORM_SECRET;9const BASE_URL = 'https://api.typeform.com';1011const typeform = axios.create({12 baseURL: BASE_URL,13 headers: { 'Authorization': `Bearer ${TOKEN}` }14});1516function verifySignature(rawBody, signature) {17 if (!WEBHOOK_SECRET) return true; // Skip if not configured18 const expected = 'sha256=' + crypto19 .createHmac('sha256', WEBHOOK_SECRET)20 .update(rawBody)21 .digest('base64'); // Note: Typeform uses base64, not hex22 return crypto.timingSafeEqual(23 Buffer.from(expected),24 Buffer.from(signature || '')25 );26}2728function parseAnswer(answer) {29 const type = answer.type;30 switch (type) {31 case 'text': return answer.text;32 case 'email': return answer.email;33 case 'number': return answer.number;34 case 'choice': return answer.choice?.label;35 case 'choices': return answer.choices?.labels || [];36 case 'boolean': return answer.boolean;37 case 'date': return answer.date;38 default: return answer[type];39 }40}4142// Webhook endpoint β receives form submissions in real time43app.post('/typeform/webhook',44 express.raw({ type: 'application/json' }),45 (req, res) => {46 const signature = req.headers['typeform-signature'];47 if (!verifySignature(req.body, signature)) {48 console.warn('Invalid webhook signature β rejecting');49 return res.status(403).json({ error: 'Invalid signature' });50 }5152 const data = JSON.parse(req.body.toString());53 const formId = data.form_response?.form_id;54 const responseId = data.form_response?.token;55 const submittedAt = data.form_response?.submitted_at;56 const answers = data.form_response?.answers || [];57 const hidden = data.form_response?.hidden || {};5859 console.log(`New submission on form ${formId} (${responseId}) at ${submittedAt}`);60 console.log('Hidden fields:', hidden);6162 // Parse answers into a usable object63 const parsed = {};64 for (const answer of answers) {65 const fieldId = answer.field?.id || 'unknown';66 parsed[fieldId] = parseAnswer(answer);67 }68 console.log('Parsed answers:', parsed);6970 // TODO: send to CRM, database, Slack, etc.71 // Example: send to your own /crm/contact endpoint72 // axios.post('/crm/contact', { email: parsed.email, ...parsed });7374 res.json({ received: true });75 }76);7778// Get forms list79app.get('/forms', async (req, res) => {80 try {81 const { data } = await typeform.get('/forms');82 res.json(data.items || []);83 } catch (err) {84 res.status(500).json({ error: err.message });85 }86});8788// Get latest responses for a form89app.get('/forms/:formId/responses', async (req, res) => {90 try {91 const { data } = await typeform.get(`/forms/${req.params.formId}/responses`, {92 params: { page_size: 25 }93 });94 res.json(data);95 } catch (err) {96 res.status(500).json({ error: err.message });97 }98});99100// Health check (must use regular JSON body parsing for non-webhook routes)101app.use(express.json());102app.get('/health', (req, res) => res.json({ status: 'ok' }));103104app.listen(3000, '0.0.0.0', () => {105 console.log('Typeform integration server running on port 3000');106});Pro tip: Typeform webhook signature uses HMAC-SHA256 but encodes the digest as base64 (not hex). Make sure your signature verification uses base64 encoding or the comparison will always fail even with the correct secret.
Expected result: The server starts and the webhook endpoint is ready to receive Typeform submissions. Submit a test response in Typeform and verify it appears in your Replit console logs.
Register the Webhook and Deploy
Register the Webhook and Deploy
Once your server is ready, register the webhook URL in Typeform either through the UI or the API. To use the API, call POST /forms/{form_id}/webhooks with a JSON body containing tag (a unique name for this webhook), url (your deployed Replit URL plus the path), enabled (true), and optionally secret (the value from your TYPEFORM_SECRET Replit Secret). Alternatively, in your Typeform account, open the form, click 'Connect' in the top menu, then 'Webhooks', and add the URL manually. The Typeform UI also has a 'Test' button that sends a sample payload to your endpoint β use this to verify the signature verification and response parsing are working correctly before going live. Deploy your Replit app before registering the webhook. Click 'Deploy' and select Autoscale for most use cases β form submission webhooks are event-driven and Autoscale handles the variable traffic efficiently. Use Reserved VM if your form processes real-time customer data where cold-start delays would cause noticeable latency. Copy the stable deployment URL and use it as your webhook target.
1import os2import requests34TOKEN = os.environ["TYPEFORM_TOKEN"]5WEBHOOK_SECRET = os.environ["TYPEFORM_SECRET"]6BASE_URL = "https://api.typeform.com"78HEADERS = {9 "Authorization": f"Bearer {TOKEN}",10 "Content-Type": "application/json"11}1213def register_webhook(form_id: str, webhook_url: str,14 tag: str = "replit-webhook") -> dict:15 """Register a webhook for a specific Typeform form."""16 payload = {17 "tag": tag,18 "url": webhook_url,19 "enabled": True,20 "secret": WEBHOOK_SECRET,21 "verify_ssl": True22 }23 response = requests.post(24 f"{BASE_URL}/forms/{form_id}/webhooks/{tag}",25 json=payload,26 headers=HEADERS27 )28 response.raise_for_status()29 return response.json()3031def list_webhooks(form_id: str) -> list:32 """List all webhooks registered for a form."""33 response = requests.get(34 f"{BASE_URL}/forms/{form_id}/webhooks",35 headers=HEADERS36 )37 response.raise_for_status()38 return response.json().get("items", [])3940def delete_webhook(form_id: str, tag: str) -> None:41 """Remove a webhook from a form."""42 response = requests.delete(43 f"{BASE_URL}/forms/{form_id}/webhooks/{tag}",44 headers=HEADERS45 )46 response.raise_for_status()4748# Example: register webhook for a form49if __name__ == "__main__":50 form_id = "YOUR_FORM_ID" # Replace with your form ID51 webhook_url = "https://your-app.replit.app/typeform/webhook"5253 result = register_webhook(form_id, webhook_url)54 print(f"Webhook registered: {result}")5556 webhooks = list_webhooks(form_id)57 print(f"\nAll webhooks for form {form_id}:")58 for wh in webhooks:59 print(f" {wh['tag']}: {wh['url']} (enabled: {wh['enabled']})")Pro tip: Run register_webhook.py once after deployment to register the webhook URL. You only need to run this once per form β Typeform remembers the webhook registration until you delete it.
Expected result: The webhook is registered against your Typeform form and the list shows it as enabled. Submitting a test response triggers delivery to your deployed Replit server.
Common use cases
Form Submission to CRM Data Pipeline
When a visitor submits a lead qualification form, feedback survey, or contact request on Typeform, your Replit server receives the webhook event in real time, extracts the form field values, and creates or updates a contact record in your CRM. The entire pipeline from form submission to CRM record takes seconds without any manual data entry.
Build a Flask endpoint that receives Typeform webhook payloads, extracts the respondent's name, email, and company from the form answers, and creates a HubSpot contact using the data from TYPEFORM_SECRET stored in Replit Secrets for signature verification.
Copy this prompt to try it in Replit
Survey Response Analysis Dashboard
A Replit Python script pulls all responses to a customer satisfaction survey from the Typeform API, calculates NPS scores, aggregates multi-choice answer distributions, and writes the analysis to a database or Google Sheet. The script runs weekly to give product managers a fresh view of customer sentiment without logging into Typeform manually.
Write a Python script that fetches all responses to a specific Typeform survey using the TYPEFORM_TOKEN from Replit Secrets, calculates the NPS score from the 0-10 scale question, and prints a breakdown of promoters, passives, and detractors.
Copy this prompt to try it in Replit
Dynamic Form Prefilling with Hidden Fields
Your Replit app generates unique Typeform URLs for each customer that pre-populate the form with the customer's account ID, subscription tier, and name using Typeform's hidden fields URL parameter feature. When the response arrives via webhook, the customer context is already attached β no need to ask users to re-enter information your database already has.
Create a Node.js function that generates a personalized Typeform URL for a customer by appending their user_id, plan, and name as hidden field URL parameters to the base form URL, then sends the URL to the customer via email.
Copy this prompt to try it in Replit
Troubleshooting
API returns 401 Unauthorized on all requests
Cause: The Personal Access Token is invalid, was revoked, or the TYPEFORM_TOKEN Secret contains extra whitespace. Tokens can also expire if you selected an expiry time when creating them.
Solution: Check the TYPEFORM_TOKEN Secret in Replit Secrets for extra spaces or newline characters. Verify the token is still active by checking your Typeform account under Account Settings > Personal Access Tokens. Generate a new token if the existing one was revoked or expired.
1# Python: quick auth test2import requests, os3response = requests.get(4 'https://api.typeform.com/me',5 headers={'Authorization': f"Bearer {os.environ['TYPEFORM_TOKEN'].strip()}"}6)7print(response.status_code, response.json())Webhook signature verification fails for all deliveries
Cause: Typeform uses HMAC-SHA256 with base64 encoding for the webhook signature, not hex encoding. If your verification code uses hexdigest() or hex encoding, the comparison will always fail.
Solution: Use base64 encoding for the HMAC digest. In Python, use base64.b64encode(hmac_obj.digest()).decode() instead of hmac_obj.hexdigest(). In Node.js, use .digest('base64') instead of .digest('hex').
1# Python: correct base64 signature verification2import hmac, hashlib, base6434def verify_signature(payload: bytes, signature: str, secret: str) -> bool:5 expected = base64.b64encode(6 hmac.new(secret.encode(), payload, hashlib.sha256).digest()7 ).decode()8 return hmac.compare_digest(f"sha256={expected}", signature)Webhook payload arrives but answer values are all None or missing
Cause: The answer parsing code is not handling all Typeform answer types correctly. Typeform has many answer types (text, email, number, choice, choices, boolean, date, file_url, payment, rating, opinion_scale), each with a different key for the value.
Solution: Add comprehensive type handling in the parse_answer function. Always check the answer's 'type' field before accessing the value β using the type as the key (e.g., answer[answer_type]) works as a fallback for unrecognized types. Add logging to print the raw answer objects during development to see the exact structure.
1# Python: log raw answer for debugging2for answer in item.get('answers', []):3 print(f"Field: {answer.get('field', {}).get('id')}, Type: {answer.get('type')}, Raw: {answer}")Responses endpoint returns results out of order or misses recent submissions
Cause: The GET /responses endpoint returns responses in reverse chronological order by default (newest first). The page_size limit means you may miss responses if there are more than the page size in the requested time window.
Solution: For time-sensitive integrations, use the 'since' parameter to fetch only responses after a known timestamp. Store the 'submitted_at' of the last processed response and use it as the 'since' value on the next poll. For real-time needs, use webhooks instead of polling.
1# Python: fetch only new responses since last check2from datetime import datetime, timezone3last_checked = "2025-03-30T10:00:00+00:00" # Store this between runs4responses = get_responses(form_id, since=last_checked)5# Update last_checked to responses['items'][0]['submitted_at'] after processingBest practices
- Store TYPEFORM_TOKEN and TYPEFORM_SECRET in Replit Secrets (lock icon π) β the token grants access to all your forms and response data, including potentially sensitive survey submissions.
- Always verify Typeform webhook signatures using HMAC-SHA256 with base64 encoding β Typeform's signature format uses base64, not hex, which is a common source of verification failures.
- Use webhooks rather than polling the responses API for real-time integrations β webhooks deliver submissions within seconds and are more reliable than periodic polling.
- Use express.raw() middleware for the webhook route in Node.js to get the raw request body for signature verification β JSON.parse() first would lose the original bytes needed for HMAC.
- Parse Typeform answers by checking the 'type' field first β different answer types store values under different keys (text, email, number, choice, choices, etc.).
- Use Typeform hidden fields to pass customer context (user ID, email, plan) into forms via URL parameters β this data appears in the webhook payload alongside answers without asking users to re-enter it.
- Deploy your Replit app before registering webhooks β use your stable replit.app deployment URL, not the temporary development session URL.
- Use Autoscale deployment for form submission pipelines and Reserved VM only if you need guaranteed zero cold-start for high-priority lead capture forms.
Alternatives
SurveyMonkey has a more traditional multi-question layout and stronger enterprise survey features including skip logic and advanced reporting, making it a better choice for formal research surveys.
HubSpot's native forms automatically create CRM contacts on submission without any API integration, making them simpler to use if you are already using HubSpot as your CRM.
Slack is not a form tool but can collect structured input via slash commands or shortcut forms in channels, making it a good alternative for internal team data collection rather than customer-facing forms.
Frequently asked questions
How do I store my Typeform API token in Replit?
Click the lock icon π in the left sidebar of your Replit project. Add TYPEFORM_TOKEN with your Personal Access Token from your Typeform account settings. If you are using webhook signature verification, also add TYPEFORM_SECRET with your chosen secret string. Access them in Python with os.environ['TYPEFORM_TOKEN'] or in Node.js with process.env.TYPEFORM_TOKEN.
Does the Typeform API work with Replit on the free plan?
Yes. The Typeform API is available on all Typeform plans including free. Replit's free tier supports outbound API calls to Typeform without restrictions. For webhook reception, you need Replit's paid plan for always-on deployment, since free Replit projects sleep when inactive. The Typeform free plan limits responses to 10 per month.
How do I find my Typeform form ID?
Your form ID appears in the URL when you open the form in the Typeform editor: admin.typeform.com/form/{FORM_ID}/create. It is an alphanumeric string like 'AbC1dEf2'. You can also list all form IDs by calling GET https://api.typeform.com/forms with your Bearer token β the response includes all forms with their IDs and titles.
How do I receive Typeform submissions in real time in Replit?
Register a webhook for your form via the Typeform API (POST /forms/{id}/webhooks) or in the Typeform UI under Connect > Webhooks. Point the webhook URL to your deployed Replit server endpoint. Typeform sends a POST request within seconds of each form submission. Your endpoint must respond with HTTP 200 promptly. Use express.raw() in Node.js or request.data in Flask to access the raw body for signature verification.
Why does my Typeform webhook signature verification always fail?
Typeform uses HMAC-SHA256 but encodes the digest as base64, not hex. If your code uses hexdigest() or .digest('hex'), the signature comparison will always fail. Switch to base64 encoding: in Python use base64.b64encode(hmac_obj.digest()).decode(), and in Node.js use .digest('base64'). The Typeform-Signature header format is 'sha256={base64_hash}'.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation