Form Integration
How to integrate Formlander with HTML and JavaScript forms
Integration Methods
Section titled “Integration Methods”Formlander works with both traditional HTML forms and modern JavaScript frameworks. Choose the method that fits your stack.
For improved reliability under high load, consider using the JavaScript SDK which adds automatic retry with exponential backoff.
No Schema Required
Section titled “No Schema Required”Formlander doesn’t care what fields you send. There’s no form builder, no field configuration, no schema to define. Just send whatever data you need:
<!-- Contact form --><input name="name" /><input name="email" /><textarea name="message"></textarea>
<!-- Job application --><input name="full_name" /><input name="phone" /><input name="linkedin_url" /><select name="position">...</select>
<!-- Survey --><input name="rating" type="range" /><input name="would_recommend" type="checkbox" /><textarea name="feedback"></textarea>Every form works the same way. Formlander stores whatever you send as JSON. Add fields, remove fields, change fields—no backend changes needed.
How it works
Section titled “How it works”- Create a form in the dashboard (just a name and slug)
- Point your HTML form to the endpoint
- Send any fields you want
The submission is stored exactly as received:
{ "name": "Jane Doe", "company": "Acme Inc", "budget": "10k-50k", "timeline": "Q2 2024", "requirements": "We need..."}No field mapping. No configuration. No limits on structure. Change your form anytime—Formlander just stores what you send.
Quick Start Examples
Section titled “Quick Start Examples”<form action="https://your-domain.com/forms/contact/submit?token=YOUR_FORM_TOKEN" method="post"> <label for="name">Name</label> <input type="text" id="name" name="name" required>
<label for="email">Email</label> <input type="email" id="email" name="email" required>
<label for="message">Message</label> <textarea id="message" name="message" rows="4" required></textarea>
<button type="submit">Send Message</button></form>const form = document.getElementById('contact-form');
form.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(form);
const response = await fetch('https://your-domain.com/forms/contact/submit?token=YOUR_TOKEN', { method: 'POST', body: formData });
const result = await response.json(); if (result.ok) { alert('Message sent!'); form.reset(); }});const [formData, setFormData] = useState({ name: '', email: '', message: '' });
const handleSubmit = async (e) => { e.preventDefault(); const body = new FormData(); Object.keys(formData).forEach(k => body.append(k, formData[k]));
const res = await fetch('https://your-domain.com/forms/contact/submit?token=YOUR_TOKEN', { method: 'POST', body });
const result = await res.json(); if (result.ok) alert('Sent!');};await fetch('https://your-domain.com/x/api/v1/submissions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ form_slug: 'contact', token: 'YOUR_TOKEN', })});HTML Forms (Traditional)
Section titled “HTML Forms (Traditional)”The simplest way to integrate - just point your form’s action to your Formlander endpoint.
Basic Example
Section titled “Basic Example”<form action="https://your-domain.com/forms/contact/submit?token=YOUR_FORM_TOKEN" method="post"> <label for="name">Name</label> <input type="text" id="name" name="name" required>
<label for="email">Email</label> <input type="email" id="email" name="email" required>
<label for="message">Message</label> <textarea id="message" name="message" rows="4" required></textarea>
<button type="submit">Send Message</button></form>✅ Pros:
- Works without JavaScript
- Simple and reliable
- Browser handles everything
❌ Cons:
- Page redirects after submit
- Limited error handling
- No custom success message
Custom Redirects
Section titled “Custom Redirects”Control where users go after submitting by adding special hidden fields:
<form action="https://your-domain.com/forms/contact/submit?token=YOUR_FORM_TOKEN" method="post"> <!-- Custom redirect URLs (optional) --> <input type="hidden" name="_success_url" value="https://example.com/thank-you"> <input type="hidden" name="_error_url" value="https://example.com/error">
<label for="name">Name</label> <input type="text" id="name" name="name" required>
<label for="email">Email</label> <input type="email" id="email" name="email" required>
<label for="message">Message</label> <textarea id="message" name="message" rows="4" required></textarea>
<button type="submit">Send Message</button></form>How it works:
_success_url- Redirect here after successful submission_error_url- Redirect here if submission fails (optional)- These fields are NOT saved in your submission data
- Supports both absolute URLs (
https://...) and relative paths (/thank-you) - If using JavaScript/AJAX, you must handle redirects manually
JavaScript Forms (Modern)
Section titled “JavaScript Forms (Modern)”Use fetch() to submit forms asynchronously with full control over the user experience.
Vanilla JavaScript Example
Section titled “Vanilla JavaScript Example”<form id="contact-form"> <label for="name">Name</label> <input type="text" id="name" name="name" required>
<label for="email">Email</label> <input type="email" id="email" name="email" required>
<label for="message">Message</label> <textarea id="message" name="message" rows="4" required></textarea>
<button type="submit">Send Message</button>
<div id="form-status"></div></form>
<script>const form = document.getElementById('contact-form');const status = document.getElementById('form-status');
form.addEventListener('submit', async (e) => { e.preventDefault();
// Show loading state status.textContent = 'Sending...'; status.className = 'loading';
// Get form data const formData = new FormData(form);
try { const response = await fetch('https://your-domain.com/forms/contact/submit?token=YOUR_FORM_TOKEN', { method: 'POST', body: formData });
const result = await response.json();
if (result.ok) { status.textContent = 'Message sent successfully!'; status.className = 'success'; form.reset(); } else { status.textContent = 'Error: ' + result.error; status.className = 'error'; } } catch (error) { status.textContent = 'Failed to send message. Please try again.'; status.className = 'error'; }});</script>
<style>#form-status { margin-top: 1rem; padding: 0.75rem; border-radius: 0.25rem; display: none;}
#form-status:not(:empty) { display: block;}
#form-status.loading { background: #f0f0f0; color: #666;}
#form-status.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb;}
#form-status.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;}</style>React Example
Section titled “React Example”import { useState } from 'react';
export default function ContactForm() { const [formData, setFormData] = useState({ name: '', email: '', message: '' }); const [status, setStatus] = useState({ type: '', message: '' }); const [loading, setLoading] = useState(false);
const handleChange = (e) => { setFormData({ ...formData, [e.target.name]: e.target.value }); };
const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); setStatus({ type: '', message: '' });
const formBody = new FormData(); Object.keys(formData).forEach(key => { formBody.append(key, formData[key]); });
try { const response = await fetch( 'https://your-domain.com/forms/contact/submit?token=YOUR_FORM_TOKEN', { method: 'POST', body: formBody } );
const result = await response.json();
if (result.ok) { setStatus({ type: 'success', message: 'Message sent successfully!' }); setFormData({ name: '', email: '', message: '' }); } else { setStatus({ type: 'error', message: result.error || 'Failed to send message' }); } } catch (error) { setStatus({ type: 'error', message: 'Network error. Please try again.' }); } finally { setLoading(false); } };
return ( <form onSubmit={handleSubmit} className="contact-form"> <div className="form-group"> <label htmlFor="name">Name</label> <input type="text" id="name" name="name" value={formData.name} onChange={handleChange} required /> </div>
<div className="form-group"> <label htmlFor="email">Email</label> <input type="email" id="email" name="email" value={formData.email} onChange={handleChange} required /> </div>
<div className="form-group"> <label htmlFor="message">Message</label> <textarea id="message" name="message" value={formData.message} onChange={handleChange} rows={4} required /> </div>
<button type="submit" disabled={loading}> {loading ? 'Sending...' : 'Send Message'} </button>
{status.message && ( <div className={`status ${status.type}`}> {status.message} </div> )} </form> );}JSON API Example
Section titled “JSON API Example”For headless/API-first integrations, use the JSON endpoint:
async function submitForm(formSlug, token, data) { const response = await fetch('https://your-domain.com/x/api/v1/submissions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ form_slug: formSlug, token: token, data: data }) });
return response.json();}
// Usageconst result = await submitForm('contact', 'YOUR_FORM_TOKEN', { name: 'John Doe', message: 'Hello world'});
if (result.ok) { console.log('Submitted:', result.submission_id);}Response Format
Section titled “Response Format”All submissions return JSON:
Success (200):
{ "ok": true, "submission_id": 123, "received_at": "2025-11-07T10:30:00Z"}Error (400/429/500):
{ "ok": false, "error": "rate limit exceeded"}CORS Configuration
Section titled “CORS Configuration”For cross-origin requests from JavaScript, Formlander allows CORS by default. No additional configuration needed.
Security Best Practices
Section titled “Security Best Practices”- Always use HTTPS in production
- Add Turnstile for bot protection (see Security)
- Enable rate limiting to prevent abuse
- Validate on server - never trust client-side validation alone
JavaScript SDK
Section titled “JavaScript SDK”The optional JavaScript SDK enhances form submissions with automatic retry logic and better error handling. Forms work without it, but the SDK improves reliability under high load or unstable network conditions.
Why Use the SDK?
Section titled “Why Use the SDK?”Without the SDK, if your Formlander server returns a 503 (busy) or the network fails, the form submission is lost. The SDK automatically retries with exponential backoff, significantly improving delivery success rates.
| Without SDK | With SDK |
|---|---|
| Single attempt | 3 retry attempts |
| Network error = lost submission | Auto-retry on failure |
| No loading feedback | Built-in loading states |
| Manual error handling | Automatic error display |
Installation
Section titled “Installation”Include the SDK script on any page with Formlander forms:
<script src="https://your-formlander.com/assets/formlander.js"></script>That’s it! The SDK automatically detects and enhances Formlander forms.
How It Works
Section titled “How It Works”The SDK identifies forms by:
- Action URL — Forms with
actionmatching/forms/{slug}/submit - Data attribute — Forms with
data-formlanderattribute
<!-- Auto-detected by action URL --><form action="https://your-formlander.com/forms/contact/submit?token=XXX" method="post"> <input name="email" type="email" required> <button type="submit">Send</button></form>
<!-- Explicitly marked with attribute --><form action="/custom-endpoint" method="post" data-formlander> <input name="email" type="email" required> <button type="submit">Send</button></form>Retry Behavior
Section titled “Retry Behavior”When a submission fails, the SDK retries automatically:
| Attempt | Delay | Total Wait |
|---|---|---|
| 1st retry | 1 second | 1s |
| 2nd retry | 2 seconds | 3s |
| 3rd retry | 4 seconds | 7s |
The SDK respects the Retry-After header if your server sends one.
Retried errors:
- HTTP 503 (Service Unavailable)
- Network failures (offline, timeout)
Not retried:
- HTTP 400 (Bad Request) — validation errors
- HTTP 401/403 — authentication/authorization errors
- HTTP 404 — form not found
Graceful Degradation
Section titled “Graceful Degradation”If all retries fail, the SDK falls back to a normal form POST, letting the browser handle the submission.
Additional SDK Behavior
Section titled “Additional SDK Behavior”- Loading states: Disables fields and changes button text to “Sending…” during submission
- Auto-inserted messages: Shows success/error messages using Tailwind CSS classes (style
[data-formlander-msg]if not using Tailwind) - Redirects: Respects
_success_urland_error_urlhidden fields (same as standard forms)
File Uploads
Section titled “File Uploads”Formlander supports file uploads with your form submissions. Files are stored on your server alongside your SQLite database.
Supported File Types
Section titled “Supported File Types”| Category | Extensions |
|---|---|
| Images | .jpg, .jpeg, .png, .gif, .webp, .svg |
| Documents | .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx |
| Text | .txt, .csv, .rtf, .odt, .ods, .odp |
Limits
Section titled “Limits”- Maximum file size: 10 MB per file
- Maximum files per field: 5
- Maximum files per submission: 10
HTML Example
Section titled “HTML Example”<form action="https://your-domain.com/forms/contact/submit?token=YOUR_TOKEN" method="post" enctype="multipart/form-data"> <label for="name">Name</label> <input type="text" id="name" name="name" required>
<label for="email">Email</label> <input type="email" id="email" name="email" required>
<label for="resume">Resume (PDF)</label> <input type="file" id="resume" name="resume" accept=".pdf,.doc,.docx">
<label for="photos">Photos</label> <input type="file" id="photos" name="photos" accept="image/*" multiple>
<button type="submit">Submit</button></form>Important: Include enctype="multipart/form-data" on your form tag when uploading files.
JavaScript Example
Section titled “JavaScript Example”const form = document.getElementById('application-form');
form.addEventListener('submit', async (e) => { e.preventDefault();
const formData = new FormData(form); // Files are automatically included in FormData
const response = await fetch( 'https://your-domain.com/forms/job-application/submit?token=YOUR_TOKEN', { method: 'POST', body: formData // Don't set Content-Type header - browser sets it with boundary } );
const result = await response.json(); if (result.ok) { alert('Application submitted!'); }});Storage Location
Section titled “Storage Location”Files are stored at: storage/uploads/{form_id}/{submission_id}/
Each file gets a unique suffix to prevent collisions. Original filenames are preserved in the database for display.
Viewing Uploaded Files
Section titled “Viewing Uploaded Files”Files appear in the submission detail view in the admin dashboard. You can download them directly from there.
Next Steps
Section titled “Next Steps”- Security - Add CAPTCHA and rate limiting
- Deployment - Production setup guide