Skip to Content
Form EditorHeadless Forms

Headless Form

You can embed a FlowGenie form inside your own application and handle the UI yourself. The steps are straightforward:

  1. Build the form using the visual form builder inside FlowGenie.
  2. Fetch the configuration with a GET request to https://app.flowgenie.pro/api/headless/forms/{slug}
  3. Render the form in your app by looping through this configuration. Use the type of each question to decide which input to render. Use the question key as the field name when collecting values.
  4. Submit responses back to https://app.flowgenie.pro/api/headless/forms/{slug}/submit with a POST request.

The response contains a JSON object similar to the structure used in the form builder. It has an array of pages, each containing sections, which in turn contain an array of rows of questions.

API Endpoints

  • GET https://app.flowgenie.pro/api/headless/forms/{slug} - Fetch form configuration
  • POST https://app.flowgenie.pro/api/headless/forms/{slug}/submit - Submit form data

Both endpoints require an Authorization: Bearer YOUR_API_KEY header if the form is not public.

Framework Examples

React (JavaScript)

import { useState, useEffect } from 'react'; function FlowGenieForm({ slug }) { const [formConfig, setFormConfig] = useState(null); const [answers, setAnswers] = useState({}); const [loading, setLoading] = useState(true); useEffect(() => { fetch(`https://app.flowgenie.pro/api/headless/forms/${slug}`, { headers: { Authorization: 'Bearer YOUR_API_KEY' } }) .then(res => res.json()) .then(data => { setFormConfig(data); setLoading(false); }); }, [slug]); const handleSubmit = async (e) => { e.preventDefault(); const response = await fetch(`https://app.flowgenie.pro/api/headless/forms/${slug}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer YOUR_API_KEY' }, body: JSON.stringify(answers) }); const result = await response.json(); console.log('Form submitted:', result); }; const renderQuestion = (question) => { const { key, type, label, required, placeholder, options } = question; switch (type) { case 'text': case 'email': case 'password': return ( <div key={key} className="mb-4"> <label className="block mb-1">{label} {required && '*'}</label> <input type={type} placeholder={placeholder} required={required} value={answers[key] || ''} onChange={(e) => setAnswers({ ...answers, [key]: e.target.value })} className="w-full px-3 py-2 border rounded" /> </div> ); case 'multiline': return ( <div key={key} className="mb-4"> <label className="block mb-1">{label} {required && '*'}</label> <textarea placeholder={placeholder} required={required} value={answers[key] || ''} onChange={(e) => setAnswers({ ...answers, [key]: e.target.value })} className="w-full px-3 py-2 border rounded" rows={4} /> </div> ); case 'number': return ( <div key={key} className="mb-4"> <label className="block mb-1">{label} {required && '*'}</label> <input type="number" placeholder={placeholder} required={required} value={answers[key] || ''} onChange={(e) => setAnswers({ ...answers, [key]: e.target.value })} className="w-full px-3 py-2 border rounded" /> </div> ); case 'checkbox': return ( <div key={key} className="mb-4"> <label className="flex items-center"> <input type="checkbox" checked={answers[key] || false} onChange={(e) => setAnswers({ ...answers, [key]: e.target.checked })} className="mr-2" /> {label} </label> </div> ); case 'select': return ( <div key={key} className="mb-4"> <label className="block mb-1">{label} {required && '*'}</label> <select required={required} value={answers[key] || ''} onChange={(e) => setAnswers({ ...answers, [key]: e.target.value })} className="w-full px-3 py-2 border rounded" > <option value="">Select...</option> {options?.map(opt => ( <option key={opt.key} value={opt.value}>{opt.label}</option> ))} </select> </div> ); case 'date': return ( <div key={key} className="mb-4"> <label className="block mb-1">{label} {required && '*'}</label> <input type="date" required={required} value={answers[key] || ''} onChange={(e) => setAnswers({ ...answers, [key]: e.target.value })} className="w-full px-3 py-2 border rounded" /> </div> ); case 'multiselect': return ( <div key={key} className="mb-4"> <label className="block mb-1">{label} {required && '*'}</label> <div className="space-y-2"> {options?.map(opt => ( <label key={opt.key} className="flex items-center"> <input type="checkbox" checked={(answers[key] || []).includes(opt.value)} onChange={(e) => { const current = answers[key] || []; const updated = e.target.checked ? [...current, opt.value] : current.filter(v => v !== opt.value); setAnswers({ ...answers, [key]: updated }); }} className="mr-2" /> {opt.label} </label> ))} </div> </div> ); default: return <div key={key}>Unsupported type: {type}</div>; } }; if (loading) return <div>Loading...</div>; if (!formConfig) return <div>Form not found</div>; return ( <form onSubmit={handleSubmit} className="max-w-md mx-auto p-6"> {formConfig.pages[0]?.sections?.map(section => section.questions?.flatMap(row => row.map(q => renderQuestion(q)) ) )} <button type="submit" className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600" > Submit </button> </form> ); }

Next.js (TypeScript)

'use client'; import { useState, useEffect } from 'react'; interface Question { id: string; key: string; type: string; label?: string; required?: boolean; placeholder?: string; options?: Array<{ key: string; value: string; label: string }>; } interface FormConfig { pages: Array<{ sections: Array<{ questions: Question[][]; }>; }>; } interface FlowGenieFormProps { slug: string; } export default function FlowGenieForm({ slug }: FlowGenieFormProps) { const [formConfig, setFormConfig] = useState<FormConfig | null>(null); const [answers, setAnswers] = useState<Record<string, any>>({}); const [loading, setLoading] = useState(true); useEffect(() => { fetch(`https://app.flowgenie.pro/api/headless/forms/${slug}`, { headers: { Authorization: 'Bearer YOUR_API_KEY' } }) .then(res => res.json()) .then((data: FormConfig) => { setFormConfig(data); setLoading(false); }) .catch(err => { console.error('Error fetching form:', err); setLoading(false); }); }, [slug]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { const response = await fetch(`https://app.flowgenie.pro/api/headless/forms/${slug}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer YOUR_API_KEY' }, body: JSON.stringify(answers) }); const result = await response.json(); console.log('Form submitted:', result); } catch (error) { console.error('Error submitting form:', error); } }; const renderQuestion = (question: Question) => { const { key, type, label, required, placeholder, options } = question; switch (type) { case 'text': case 'email': case 'password': return ( <div key={key} className="mb-4"> <label className="block mb-1">{label} {required && '*'}</label> <input type={type} placeholder={placeholder} required={required} value={answers[key] || ''} onChange={(e) => setAnswers({ ...answers, [key]: e.target.value })} className="w-full px-3 py-2 border rounded" /> </div> ); case 'multiline': return ( <div key={key} className="mb-4"> <label className="block mb-1">{label} {required && '*'}</label> <textarea placeholder={placeholder} required={required} value={answers[key] || ''} onChange={(e) => setAnswers({ ...answers, [key]: e.target.value })} className="w-full px-3 py-2 border rounded" rows={4} /> </div> ); case 'number': return ( <div key={key} className="mb-4"> <label className="block mb-1">{label} {required && '*'}</label> <input type="number" placeholder={placeholder} required={required} value={answers[key] || ''} onChange={(e) => setAnswers({ ...answers, [key]: e.target.value })} className="w-full px-3 py-2 border rounded" /> </div> ); case 'checkbox': return ( <div key={key} className="mb-4"> <label className="flex items-center"> <input type="checkbox" checked={answers[key] || false} onChange={(e) => setAnswers({ ...answers, [key]: e.target.checked })} className="mr-2" /> {label} </label> </div> ); case 'select': return ( <div key={key} className="mb-4"> <label className="block mb-1">{label} {required && '*'}</label> <select required={required} value={answers[key] || ''} onChange={(e) => setAnswers({ ...answers, [key]: e.target.value })} className="w-full px-3 py-2 border rounded" > <option value="">Select...</option> {options?.map(opt => ( <option key={opt.key} value={opt.value}>{opt.label}</option> ))} </select> </div> ); case 'date': return ( <div key={key} className="mb-4"> <label className="block mb-1">{label} {required && '*'}</label> <input type="date" required={required} value={answers[key] || ''} onChange={(e) => setAnswers({ ...answers, [key]: e.target.value })} className="w-full px-3 py-2 border rounded" /> </div> ); case 'multiselect': return ( <div key={key} className="mb-4"> <label className="block mb-1">{label} {required && '*'}</label> <div className="space-y-2"> {options?.map(opt => ( <label key={opt.key} className="flex items-center"> <input type="checkbox" checked={(answers[key] || []).includes(opt.value)} onChange={(e) => { const current = answers[key] || []; const updated = e.target.checked ? [...current, opt.value] : current.filter(v => v !== opt.value); setAnswers({ ...answers, [key]: updated }); }} className="mr-2" /> {opt.label} </label> ))} </div> </div> ); default: return <div key={key}>Unsupported type: {type}</div>; } }; if (loading) return <div>Loading...</div>; if (!formConfig) return <div>Form not found</div>; return ( <form onSubmit={handleSubmit} className="max-w-md mx-auto p-6"> {formConfig.pages[0]?.sections?.map(section => section.questions?.flatMap(row => row.map(q => renderQuestion(q)) ) )} <button type="submit" className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600" > Submit </button> </form> ); }

Vanilla JavaScript

<!DOCTYPE html> <html> <head> <title>FlowGenie Form</title> <style> .form-container { max-width: 600px; margin: 0 auto; padding: 20px; } .form-group { margin-bottom: 20px; } label { display: block; margin-bottom: 5px; font-weight: bold; } input, textarea, select { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; } button { background: #3b82f6; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; } button:hover { background: #2563eb; } </style> </head> <body> <div class="form-container"> <form id="flowgenie-form"> <div id="form-questions"></div> <button type="submit">Submit</button> </form> </div> <script> const slug = 'your-form-slug'; const apiKey = 'YOUR_API_KEY'; let formConfig = null; const answers = {}; // Fetch form configuration async function loadForm() { const response = await fetch(`https://app.flowgenie.pro/api/headless/forms/${slug}`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); formConfig = await response.json(); renderForm(); } // Render form questions function renderForm() { const container = document.getElementById('form-questions'); container.innerHTML = ''; formConfig.pages[0].sections.forEach(section => { section.questions.forEach(row => { row.forEach(question => { const div = document.createElement('div'); div.className = 'form-group'; const label = document.createElement('label'); label.textContent = question.label + (question.required ? ' *' : ''); div.appendChild(label); let input; switch (question.type) { case 'text': case 'email': case 'password': input = document.createElement('input'); input.type = question.type; input.placeholder = question.placeholder || ''; break; case 'multiline': input = document.createElement('textarea'); input.placeholder = question.placeholder || ''; break; case 'number': input = document.createElement('input'); input.type = 'number'; input.placeholder = question.placeholder || ''; break; case 'checkbox': input = document.createElement('input'); input.type = 'checkbox'; break; case 'select': input = document.createElement('select'); const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.textContent = 'Select...'; input.appendChild(defaultOption); question.options?.forEach(opt => { const option = document.createElement('option'); option.value = opt.value; option.textContent = opt.label; input.appendChild(option); }); break; case 'date': input = document.createElement('input'); input.type = 'date'; break; default: return; } input.id = question.key; input.required = question.required || false; input.addEventListener('change', (e) => { if (question.type === 'checkbox') { answers[question.key] = e.target.checked; } else { answers[question.key] = e.target.value; } }); div.appendChild(input); container.appendChild(div); }); }); }); } // Handle form submission document.getElementById('flowgenie-form').addEventListener('submit', async (e) => { e.preventDefault(); const response = await fetch(`https://app.flowgenie.pro/api/headless/forms/${slug}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify(answers) }); const result = await response.json(); console.log('Form submitted:', result); alert('Form submitted successfully!'); }); // Load form on page load loadForm(); </script> </body> </html>

This approach lets you keep complete control over the look and feel while relying on FlowGenie for form management and submission handling.

Last updated on