Skip to content

Form Integration

How to integrate Formlander with HTML and JavaScript forms

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.


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.

  1. Create a form in the dashboard (just a name and slug)
  2. Point your HTML form to the endpoint
  3. Send any fields you want

The submission is stored exactly as received:

{
"name": "Jane Doe",
"email": "[email protected]",
"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.


<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>

The simplest way to integrate - just point your form’s action to your Formlander endpoint.

<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

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

Use fetch() to submit forms asynchronously with full control over the user experience.

<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>
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>
);
}

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();
}
// Usage
const result = await submitForm('contact', 'YOUR_FORM_TOKEN', {
name: 'John Doe',
message: 'Hello world'
});
if (result.ok) {
console.log('Submitted:', result.submission_id);
}

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"
}

For cross-origin requests from JavaScript, Formlander allows CORS by default. No additional configuration needed.


  1. Always use HTTPS in production
  2. Add Turnstile for bot protection (see Security)
  3. Enable rate limiting to prevent abuse
  4. Validate on server - never trust client-side validation alone

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.

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 SDKWith SDK
Single attempt3 retry attempts
Network error = lost submissionAuto-retry on failure
No loading feedbackBuilt-in loading states
Manual error handlingAutomatic error display

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.

The SDK identifies forms by:

  1. Action URL — Forms with action matching /forms/{slug}/submit
  2. Data attribute — Forms with data-formlander attribute
<!-- 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>

When a submission fails, the SDK retries automatically:

AttemptDelayTotal Wait
1st retry1 second1s
2nd retry2 seconds3s
3rd retry4 seconds7s

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

If all retries fail, the SDK falls back to a normal form POST, letting the browser handle the submission.

  • 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_url and _error_url hidden fields (same as standard forms)

Formlander supports file uploads with your form submissions. Files are stored on your server alongside your SQLite database.

CategoryExtensions
Images.jpg, .jpeg, .png, .gif, .webp, .svg
Documents.pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx
Text.txt, .csv, .rtf, .odt, .ods, .odp
  • Maximum file size: 10 MB per file
  • Maximum files per field: 5
  • Maximum files per submission: 10
<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.

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!');
}
});

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.

Files appear in the submission detail view in the admin dashboard. You can download them directly from there.