SimplyForms Documentation
Complete guide to working with the SimplyForms API
What is SimplyForms?
SimplyForms is a Form Backend as a Service (FBaaS). It lets you process submissions from any HTML form without writing or hosting backend code: you set the form's action attribute to a SimplyForms endpoint, and every submission is validated, optionally screened for spam, and forwarded to the form owner's email inbox. Submissions are never written to a database β SimplyForms is a stateless email relay, so there is no stored form data to export, leak, or hand over, and GDPR compliance is structural rather than policy-based. Integrating it is a one-line change to an existing form, the free plan requires no credit card, and all infrastructure runs in the EU on SimplyForms' own mail servers.
Quick start
Start using SimplyForms in 2 minutes β just change the action attribute of your form.
Sign up
Create an account and get your unique Form ID
Change the action URL
Replace the action attribute in your form
Done!
Your form now works and sends emails
<form action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID" method="POST">
<input type="email" name="email" required />
<button type="submit">Odeslat</button>
</form>Wire up with Claude Code
If you use Claude Code (Anthropic's official CLI), you can wire your form in a single prompt. Our official open-source skill walks Claude through the whole flow β it locates the form in your project, repoints submissions to the SimplyForms API, embeds the spam-protection widget, and runs through end-to-end verification with you. Works with plain HTML, React, Vue/Nuxt, and other framework forms.
Install the skill (one-time)
git clone https://github.com/simplyforms/public-claude-skill ~/.claude/skills/simplyformsExample prompt
Here's my contact form at ./about.html. Wire it up to SimplyForms.
My form_id is YOUR_FORM_ID, I'm on the FREE plan.
Show me an inline thank-you message on success.MIT-licensed, open source, semver-versioned. Issues and pull requests welcome. Update locally with `cd ~/.claude/skills/simplyforms && git pull`.
github.com/simplyforms/public-claude-skillBasic form
A simple contact form with name, email and message.
<form action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID" method="POST">
<label for="name">Jmeno:</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">Zprava:</label>
<textarea id="message" name="message" rows="5" required></textarea>
<button type="submit">Odeslat zpravu</button>
</form>Multi-step form
Split a long form into multiple steps for better UX.
<form id="multi-step-form" action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID" method="POST">
<!-- Krok 1: Osobni udaje -->
<div class="step step-1 active">
<h3>Krok 1: Osobni udaje</h3>
<input type="text" name="firstName" placeholder="Jmeno" required />
<input type="text" name="lastName" placeholder="Prijmeni" required />
<button type="button" onclick="nextStep()">Dalsi</button>
</div>
<!-- Krok 2: Kontakt -->
<div class="step step-2">
<h3>Krok 2: Kontaktni udaje</h3>
<input type="email" name="email" placeholder="Email" required />
<input type="tel" name="phone" placeholder="Telefon" />
<button type="button" onclick="prevStep()">Zpet</button>
<button type="button" onclick="nextStep()">Dalsi</button>
</div>
<!-- Krok 3: Zprava -->
<div class="step step-3">
<h3>Krok 3: Vase zprava</h3>
<textarea name="message" placeholder="Zprava" rows="5" required></textarea>
<button type="button" onclick="prevStep()">Zpet</button>
<button type="submit">Odeslat</button>
</div>
</form>
<script>
let currentStep = 1;
const totalSteps = 3;
function nextStep() {
if (currentStep < totalSteps) {
document.querySelector(`.step-${currentStep}`).classList.remove('active');
currentStep++;
document.querySelector(`.step-${currentStep}`).classList.add('active');
}
}
function prevStep() {
if (currentStep > 1) {
document.querySelector(`.step-${currentStep}`).classList.remove('active');
currentStep--;
document.querySelector(`.step-${currentStep}`).classList.add('active');
}
}
</script>
<style>
.step { display: none; }
.step.active { display: block; }
</style>File upload
Add the ability to upload files up to 50MB (Extend plan).
<form
action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID"
method="POST"
enctype="multipart/form-data">
<label for="name">Jmeno:</label>
<input type="text" id="name" name="name" required />
<label for="email">Email:</label>
<input type="email" id="email" name="email" required />
<label for="file">Nahrat soubor:</label>
<input
type="file"
id="file"
name="attachment"
accept=".pdf,.doc,.docx,.jpg,.png"
required />
<p class="file-info">
Podporovane formaty: PDF, DOC, DOCX, JPG, PNG<br>
Maximalni velikost: 50MB (Extend plan)
</p>
<button type="submit">Odeslat s prilohou</button>
</form>Special form fields
A few field names have a special meaning for the delivered e-mail. Works for every plan with no dashboard configuration β just add the field to the form. The subject field becomes the e-mail's Subject: header, ccemail CCs extra recipients (max 5, semicolon-separated), and any field whose name starts with an underscore (_*) is kept out of the e-mail body.
<form action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID" method="POST">
<!-- subject -> Subject: header of the delivered e-mail -->
<input type="text" name="subject" placeholder="Email subject" required />
<!-- ccemail -> extra recipients, max 5, semicolon-separated -->
<input type="hidden" name="ccemail" value="manager@company.com; hr@company.com" />
<!-- Regular fields (appear in the e-mail body) -->
<input type="text" name="name" placeholder="Your name" required />
<input type="email" name="email" placeholder="Your e-mail" required />
<textarea name="message" placeholder="Your message" required></textarea>
<!-- _-prefixed -> kept out of the e-mail body (use for hidden context) -->
<input type="hidden" name="_source" value="homepage-hero" />
<button type="submit">Send</button>
</form>EXTEND plans can override the subject from the Email Template Studio with Jinja against the submitted fields. This value, for example, uses the submitted subject and falls back to static text when it is absent:
{{ subject or "Lead received" }}CAPTCHA protection
Protect your form against spam with Cloudflare Turnstile or Google reCAPTCHA (Extend plan). Free plans rely on rate-limiting; SimplyForms Protection is available from the Standard plan up.
Cloudflare Turnstile (Extend plan)
<form action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID" method="POST">
<input type="text" name="name" placeholder="Jmeno" required />
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Zprava" required></textarea>
<!-- Cloudflare Turnstile -->
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Odeslat</button>
</form>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>Google reCAPTCHA v3 (Extend plan)
<form action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID" method="POST">
<input type="text" name="name" placeholder="Jmeno" required />
<input type="email" name="email" placeholder="Email" required />
<!-- reCAPTCHA v3 token -->
<input type="hidden" name="g-recaptcha-response" id="recaptchaResponse" />
<button type="submit">Odeslat</button>
</form>
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
<script>
grecaptcha.ready(function() {
document.querySelector('form').addEventListener('submit', function(e) {
e.preventDefault();
grecaptcha.execute('YOUR_SITE_KEY', {action: 'submit'}).then(function(token) {
document.getElementById('recaptchaResponse').value = token;
e.target.submit();
});
});
});
</script>SimplyForms Protection (privacy-first, no registration)
Protect your form against spam with SimplyForms Protection β a privacy-first, self-hosted proof-of-work challenge available from the Standard plan up. No third-party account required. SimplyForms Protection is built on the open-source ALTCHA library (MIT License, Β© 2023 Daniel Regeci).
Self-hosted widget (primary)
The widget JS is served directly from the SimplyForms API, so no external CDN request is made from your visitors' browsers.
<form action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID" method="POST">
<input type="text" name="name" placeholder="Jmeno" required />
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Zprava" required></textarea>
<!-- SimplyForms Protection widget β loads JS from the SimplyForms API (self-hosted) -->
<script type="module" src="https://api.simplyforms.app/sf/widget.js"></script>
<sf-captcha challengeurl="https://api.simplyforms.app/sf/challenge?form_id=YOUR_FORM_ID"></sf-captcha>
<button type="submit">Odeslat</button>
</form>Submitting the form more than once
Every verified token is single-use β the server marks it as consumed on first acceptance. Cloudflare Turnstile and Google reCAPTCHA work the same way. If your form can be submitted more than once per page-load (comment forms, retry after a validation error), you must reset the widget between submissions β otherwise the second submit returns 400 ALTCHA_REPLAY.
Option A β automatic reset (autoreset attribute)
Easiest path: add the autoreset attribute to the sf-captcha tag. After each verification the widget schedules a self-reset (200 ms after the dispatched submit), so the next click on the Send button starts a fresh proof-of-work. The user's field values are left untouched.
<form action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID" method="POST">
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<script type="module" src="https://api.simplyforms.app/sf/widget.js"></script>
<!-- auto="onsubmit" defers PoW until the user clicks Send;
autoreset clears the verified payload after every submit. -->
<sf-captcha
challengeurl="https://api.simplyforms.app/sf/challenge?form_id=YOUR_FORM_ID"
auto="onsubmit"
autoreset
></sf-captcha>
<button type="submit">Send</button>
</form>Option B β manual reset from your code
If you'd rather drive the widget lifecycle yourself (custom fetch, custom error UI), dispatch a synthetic 'reset' event on the parent form element. Reset on every response (use finally): the server consumed the token whether it accepted or rejected the submission, so a retry without a fresh challenge would hit ALTCHA_REPLAY. The synthetic event resets only the widget β your form fields stay intact.
<form id="contactForm" action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID" method="POST">
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<script type="module" src="https://api.simplyforms.app/sf/widget.js"></script>
<sf-captcha
challengeurl="https://api.simplyforms.app/sf/challenge?form_id=YOUR_FORM_ID"
auto="onsubmit"
></sf-captcha>
<button type="submit">Send</button>
</form>
<script>
const form = document.getElementById('contactForm');
form.addEventListener('submit', async (e) => {
e.preventDefault();
try {
const res = await fetch(form.action, { method: 'POST', body: new FormData(form) });
if (res.ok) {
// ...show your own success UI here
} else {
// ...show your own error UI here
}
} finally {
// Always reset β the server consumed the token whether it accepted
// or rejected the submission, so a re-submit would hit ALTCHA_REPLAY
// without a fresh challenge. Dispatching the event resets ONLY the
// captcha widget; form fields stay intact (form.reset() would wipe
// them).
form.dispatchEvent(new Event('reset'));
}
});
</script>Complete contact form
A professional contact form with validation and advanced features.
<form
id="contact-form"
action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID"
method="POST"
class="contact-form">
<div class="form-group">
<label for="fullName">Cele jmeno *</label>
<input
type="text"
id="fullName"
name="fullName"
required
minlength="2" />
</div>
<div class="form-row">
<div class="form-group">
<label for="email">Email *</label>
<input type="email" id="email" name="email" required />
</div>
<div class="form-group">
<label for="phone">Telefon</label>
<input type="tel" id="phone" name="phone" pattern="[0-9]{9,}" />
</div>
</div>
<div class="form-group">
<label for="subject">Predmet *</label>
<select id="subject" name="subject" required>
<option value="">Vyberte predmet</option>
<option value="general">Obecny dotaz</option>
<option value="support">Technicka podpora</option>
<option value="sales">Obchodni dotaz</option>
<option value="other">Jine</option>
</select>
</div>
<div class="form-group">
<label for="message">Zprava *</label>
<textarea
id="message"
name="message"
rows="6"
required
minlength="10"></textarea>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" name="newsletter" value="yes" />
Chci odebirat newsletter
</label>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" name="gdpr" required />
Souhlasim se zpracovanim osobnich udaju *
</label>
</div>
<button type="submit" class="btn-submit">
Odeslat zpravu
</button>
<div class="form-success" style="display: none;">
Zprava byla uspesne odeslana!
</div>
</form>
<script>
document.getElementById('contact-form').addEventListener('submit', function(e) {
e.preventDefault();
const form = this;
const formData = new FormData(form);
fetch(form.action, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
form.style.display = 'none';
document.querySelector('.form-success').style.display = 'block';
})
.catch(error => {
alert('Chyba pri odesilani formulare. Zkuste to prosim znovu.');
});
});
</script>Webhooks
Send form data to your own endpoints in real time. Ideal for integrating with your systems, CRM or automation tools. Available on the Extend plan and up.
How webhooks work
On every submission SimplyForms sends a POST request to your URL with the form data as JSON, signed with an HMAC-SHA256 signature.
Configuring the webhook
In the SimplyForms dashboard open Integrations and configure your webhook:
1. Sign in to the SimplyForms dashboard
2. Open "Integrations" in the sidebar
3. In the "Webhook" section, enable it and enter your URL
4. (Optional) toggle "Include metadata" β off by default
5. Save β a signing secret is generated automaticallyExample webhook payload
{
"formId": "YOUR_FORM_ID",
"timestamp": 1736519400,
"data": {
"name": "Jan Novak",
"email": "jan.novak@example.com",
"message": "Dobry den, mam zajem o vas produkt."
}
// "metadata": { ipAddress, userAgent, referrer }
// is ONLY included if you explicitly opt in per form.
}Handling the webhook (Node.js)
// Express.js priklad
const express = require('express');
const app = express();
app.use(express.json());
app.post('/api/webhook', async (req, res) => {
try {
const { formId, submissionId, data, metadata } = req.body;
// Validace signature (doporuceno)
const signature = req.headers['x-simplyforms-signature'];
if (!verifySignature(signature, req.body)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Zpracovani dat
console.log('Novy formular od:', data.email);
// Ulozeni do databaze
await db.submissions.create({
formId,
submissionId,
email: data.email,
name: data.name,
message: data.message,
receivedAt: new Date()
});
// Notifikace do Slacku, CRM, etc.
await sendToSlack(data);
// Vzdy odpovezte status 200
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal error' });
}
});
function verifySignature(signature, body) {
const crypto = require('crypto');
const secret = process.env.SIMPLYFORMS_WEBHOOK_SECRET;
const hash = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(body))
.digest('hex');
return signature === hash;
}
app.listen(3000);Testing the webhook
For local development use a tool like ngrok to expose your localhost URL:
# Instalace ngrok
npm install -g ngrok
# Spusteni tunelu
ngrok http 3000
# Pouzijte vygenerovanou URL (napr. https://abc123.ngrok.io)
# jako webhook URL v Simply Forms dashboarduRetry logic
If your endpoint does not return 200, SimplyForms retries delivery:
- Attempt 1: immediately
- Attempt 2: after 5 minutes
- Attempt 3: after 30 minutes
- Attempt 4: after 2 hours
Custom validation
Add advanced form validation on both the client and the server for maximum control over the data you accept.
Client-side validation (JavaScript)
HTML5 validation attributes are great, but sometimes you need more control:
<form id="validated-form" action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID" method="POST">
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required />
<span class="error-message" id="email-error"></span>
</div>
<div class="form-group">
<label for="phone">Telefon (CZ format):</label>
<input type="tel" id="phone" name="phone" required />
<span class="error-message" id="phone-error"></span>
</div>
<div class="form-group">
<label for="age">Vek (18+):</label>
<input type="number" id="age" name="age" required />
<span class="error-message" id="age-error"></span>
</div>
<button type="submit">Odeslat</button>
</form>
<script>
const form = document.getElementById('validated-form');
const validators = {
email: (value) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!regex.test(value)) {
return 'Zadejte platnou emailovou adresu';
}
const blocked = ['tempmail.com', 'throwaway.email'];
const domain = value.split('@')[1];
if (blocked.includes(domain)) {
return 'Pouzijte prosim trvalou emailovou adresu';
}
return null;
},
phone: (value) => {
const regex = /^(\+420)?[0-9]{9}$/;
if (!regex.test(value.replace(/\s/g, ''))) {
return 'Zadejte platne ceske telefonni cislo';
}
return null;
},
age: (value) => {
const age = parseInt(value);
if (isNaN(age) || age < 18) {
return 'Musite byt starsi 18 let';
}
if (age > 120) {
return 'Zadejte prosim realny vek';
}
return null;
}
};
Object.keys(validators).forEach(fieldName => {
const field = document.getElementById(fieldName);
const errorSpan = document.getElementById(`${fieldName}-error`);
field.addEventListener('blur', () => {
const error = validators[fieldName](field.value);
if (error) {
errorSpan.textContent = error;
field.classList.add('invalid');
} else {
errorSpan.textContent = '';
field.classList.remove('invalid');
}
});
});
form.addEventListener('submit', (e) => {
e.preventDefault();
let isValid = true;
Object.keys(validators).forEach(fieldName => {
const field = document.getElementById(fieldName);
const errorSpan = document.getElementById(`${fieldName}-error`);
const error = validators[fieldName](field.value);
if (error) {
errorSpan.textContent = error;
field.classList.add('invalid');
isValid = false;
}
});
if (isValid) {
form.submit();
} else {
alert('Opravte prosim chyby ve formulari');
}
});
</script>
<style>
.error-message {
color: #e53e3e;
font-size: 14px;
display: block;
margin-top: 5px;
}
input.invalid {
border-color: #e53e3e;
background-color: #fff5f5;
}
</style>Server-side validation (SimplyForms dashboard)
In the SimplyForms dashboard you can define validation rules that are evaluated before the submission is accepted:
{
"validationRules": {
"email": {
"required": true,
"type": "email",
"blockedDomains": ["tempmail.com", "guerrillamail.com"]
},
"age": {
"required": true,
"type": "number",
"min": 18,
"max": 120
},
"phone": {
"required": false,
"pattern": "^(\\+420)?[0-9]{9}$"
},
"message": {
"required": true,
"minLength": 10,
"maxLength": 1000
}
}
}Async validation (duplicate check)
Check whether an email is already in your database before submitting the form:
const emailInput = document.getElementById('email');
const emailError = document.getElementById('email-error');
emailInput.addEventListener('blur', async () => {
const email = emailInput.value;
clearTimeout(emailInput.debounceTimer);
emailInput.debounceTimer = setTimeout(async () => {
try {
const response = await fetch('/api/check-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
const data = await response.json();
if (data.exists) {
emailError.textContent = 'Tento email je jiz zaregistrovan';
emailInput.classList.add('invalid');
} else {
emailError.textContent = '';
emailInput.classList.remove('invalid');
}
} catch (error) {
console.error('Validation error:', error);
}
}, 500);
});Email Templates
Customise the design and content of notification emails to match your brand.
Default template
SimplyForms uses a professional default template that includes all submitted fields.
Predmet: Novy formular z example.com
Dobry den,
obdrzeli jste novy formular od:
Jmeno: Jan Novak
Email: jan.novak@example.com
Telefon: +420777123456
Zprava:
Dobry den, mam zajem o vas produkt.
---
Odeslano pres Simply Forms
ID: sub_xyz789
Cas: 10.1.2025 14:30Ready-made preset templates
Six built-in templates are available in the dashboard Email Templates page. Select one with a single click β a sandboxed live preview is shown before you save.
minimalβ Minimal β simple left-aligned title, no color bandclean-cardβ Clean Card β light bg, purple top accent barbold-headerβ Bold Header β full-width dark header with white titlenotificationβ Notification β inline NEW badge, compact paddingeditorialβ Editorial β serif fonts, warm paper tonesdark-firstβ Dark First β dark-mode-first with light media query
Live preview
The Email Templates page in the dashboard shows a sandboxed live preview rendered by the backend with sample data. Available for all authenticated users β no plan gate on preview.
Custom HTML template (EXTEND only)
On the EXTEND plan you can write a fully custom HTML body in the dashboard. The custom body is sent as the complete email β it is not wrapped in the SimplyForms notification shell.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f3f4f6; margin: 0; padding: 32px 12px; }
.card { background: #fff; border-radius: 12px; max-width: 600px;
margin: 0 auto; overflow: hidden; }
.header { background: #111827; color: #fff; padding: 28px 32px; }
.header h1 { margin: 0; font-size: 20px; }
.body { padding: 28px 32px; }
.field-label { font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: .05em; color: #6b7280; padding-top: 12px; }
.field-value { font-size: 15px; color: #111827; padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6; }
.footer { background: #fafafa; padding: 16px 32px; font-size: 12px;
color: #9ca3af; text-align: center; }
</style>
</head>
<body>
<div class="card">
<div class="header">
<h1>New form submission</h1>
</div>
<div class="body">
<!-- Reference individual fields by name attribute -->
<div class="field-label">Name</div>
<div class="field-value">{{ name }}</div>
<div class="field-label">Email</div>
<div class="field-value">{{ email }}</div>
<div class="field-label">Message</div>
<div class="field-value">{{ message }}</div>
</div>
<div class="footer">Sent via SimplyForms</div>
</div>
</body>
</html>Available template variables
| Variable | Description | Example |
|---|---|---|
{{ fieldName }} | Value of a submitted form field β use the exact name attribute of the field. Fields prefixed with _ and file-upload fields are excluded. | {{ email }}, {{ name }}, {{ message }} |
fields | Dict of all submitted fields (label to value). Use with a for-loop to render every field automatically regardless of field names. | {% for label, value in fields.items() %} |
Iterating over all fields
The fields variable is a dict mapping each submitted field name to its value. Use a Jinja2 for-loop to render every field automatically β useful in presets and any form with dynamic fields.
<!-- Loop over all submitted fields (any form) -->
{% for label, value in fields.items() %}
<div class="field-label">{{ label }}</div>
<div class="field-value">{{ value }}</div>
{% endfor %}Autoresponder
Send an automatic confirmation message to the person who submitted the form.
HTML autoresponder template
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2>Dekujeme za Vasi zpravu!</h2>
<p>Mily/a {{name}},</p>
<p>Dekujeme za vyplneni formulare. Vasi zpravu jsme obdrzeli a odpovime Vam do 24 hodin.</p>
<div style="background: #f7fafc; padding: 20px; border-radius: 8px; margin: 20px 0;">
<p><strong>Shrnuti Vasi zpravy:</strong></p>
<p>{{message}}</p>
</div>
<p>Pokud mate nalehavou otazku, kontaktujte nas na <a href="tel:+420777123456">+420 777 123 456</a>.</p>
<p>S pozdravem,<br>
Vas tym</p>
<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 30px 0;">
<p style="color: #718096; font-size: 14px;">
Tento email byl odeslan automaticky. Neodpovidejte na nej.
</p>
</body>
</html>Plain text fallback
Always add a plain text version for email clients that do not support HTML:
Dekujeme za Vasi zpravu!
Mily/a {{name}},
Dekujeme za vyplneni formulare. Vasi zpravu jsme obdrzeli a odpovime Vam do 24 hodin.
Shrnuti Vasi zpravy:
{{message}}
Pokud mate nalehavou otazku, kontaktujte nas na +420 777 123 456.
S pozdravem,
Vas tym
---
Tento email byl odeslan automaticky. Neodpovidejte na nej.API Reference
Complete SimplyForms API documentation.
Endpoint
POST https://api.simplyforms.app/v1/forms/{FORM_ID}The canonical route is /v1/forms/. The legacy /submit/ path keeps working as a permanent alias.
Headers
| Header | Value | Required |
|---|---|---|
Content-Type | application/x-www-form-urlencoded, multipart/form-data, application/json | Yes |
Response
{
"success": true
}
// Submissions are relayed, never stored β there is no submissionId.
// If the monthly email limit is reached you also get:
// "warning": "...", "email_limit": { "used", "limit", "reset_date" }Error response
{
"ok": false,
"code": "INVALID_FORM_ID",
"message": "Invalid or inactive form ID"
}Frequently asked questions
Does SimplyForms store my form submissions?
No. SimplyForms is a stateless email relay: each submission is validated, optionally checked for spam, forwarded to your inbox, and then discarded. The form payload and the visitor's IP address are never written to a database, so there is no stored submission data to export, breach, or be compelled to disclose.
Is SimplyForms GDPR compliant?
Yes. Because submissions are never stored, the data-protection surface is minimal by design. All infrastructure is hosted in the EU on SimplyForms' own mail servers β there is no third-party email processor such as SendGrid or Mailgun. A Data Processing Agreement (DPA), privacy policy, and sub-processor list are published at simplyforms.app/legal.
What is included in the free plan?
The free plan covers 50 submissions per day and 250 emails per month, with a 1 MB file-upload limit and one allowed domain. It needs no credit card. On the free plan spam is handled by rate limiting; CAPTCHA options (ALTCHA, Turnstile, reCAPTCHA) are available on paid plans.
How is SimplyForms different from Formspree or Netlify Forms?
The core difference is storage: SimplyForms never stores submissions, whereas most form backends keep them in a dashboard. SimplyForms relays each submission to your email and forgets it. It is also EU-hosted and sends through its own mail servers rather than a third-party email provider, which keeps the sub-processor surface small.
How do I add spam protection to my form?
Every plan includes automatic rate limiting. From the Standard plan you can enable SimplyForms Protection β a privacy-first, self-hosted ALTCHA proof-of-work widget that needs no third-party account. The Extend plan adds Cloudflare Turnstile and Google reCAPTCHA v3. The free plan relies on rate limiting only.
Terms of Service
By using SimplyForms you agree to use the service lawfully and not to relay spam, malware or unlawful content. Paid plans are billed via Stripe; you can cancel anytime and cancellation takes effect at the end of the current billing period. The Enterprise plan is billed by separate agreement. The service is provided "as is" without warranty; we are not liable for indirect or consequential damages. We may update these terms β material changes will be announced on this page.
Privacy Policy
All SimplyForms infrastructure runs in the European Union. API, database, and SMTP relay process data exclusively in EU regions β no submission ever leaves the EU. SimplyForms is a stateless email-relay: form submissions are never stored β they are relayed directly to the form owner's inbox. No form payload, no submitter IP and no user-agent are persisted. We keep only an anonymous per-submission metric (form id, timestamp, whether an email was sent, country code, referrer domain) with a 90-day retention. Outbound webhook metadata (IP/user-agent/referrer) is sent ONLY if the form owner explicitly opts in per form. You can export your data (GDPR Art. 20) or delete your account (GDPR Art. 17) from the dashboard at any time.
SimplyForms Protection is built on the open-source ALTCHA library (MIT License, Β© 2023 Daniel Regeci).
