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.

1

Sign up

Create an account and get your unique Form ID

2

Change the action URL

Replace the action attribute in your form

3

Done!

Your form now works and sends emails

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

bash
git clone https://github.com/simplyforms/public-claude-skill ~/.claude/skills/simplyforms

Example prompt

text
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.
Skill source

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

Basic form

A simple contact form with name, email and message.

html
<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>
Tip
The name attribute determines the field label in the received email.

Multi-step form

Split a long form into multiple steps for better UX.

html
<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).

html
<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>
Important
For file uploads you must use enctype="multipart/form-data".

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.

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

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)

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

html
<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).

Privacy-first
Free, no third-party account required, privacy-first: verification is 100% local β€” no end-user data leaves our server.

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.

html
<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>
Production deployment
The API must be served over HTTPS or the browser will block mixed content. If your site uses a strict CSP, allow the API origin in the script-src and connect-src directives. The widget script must load β€” without it the form will not submit.

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.

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

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

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

Newsletter signup

A simple form for newsletter subscription.

html
<form
  action="https://api.simplyforms.app/v1/forms/YOUR_FORM_ID"
  method="POST"
  class="newsletter-form">

  <h3>Prihlaste se k odberu</h3>
  <p>Ziskejte nejnovejsi tipy a triky primo do emailu</p>

  <div class="newsletter-input">
    <input
      type="email"
      name="email"
      placeholder="vas@email.cz"
      required />
    <button type="submit">Odeslat</button>
  </div>

  <p class="newsletter-note">
    Zadny spam. Odhlasit se muzete kdykoli.
  </p>
</form>

<style>
.newsletter-form {
  max-width: 500px;
  margin: 0 auto;
  padding: 30px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 16px;
  text-align: center;
}

.newsletter-input {
  display: flex;
  gap: 10px;
  margin: 20px 0;
}

.newsletter-input input {
  flex: 1;
  padding: 12px;
  border: none;
  border-radius: 8px;
  font-size: 16px;
}

.newsletter-input button {
  padding: 12px 24px;
  background: white;
  color: #667eea;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
}

.newsletter-note {
  font-size: 14px;
  opacity: 0.9;
}
</style>

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.

Yes
Webhooks are available on the Extend plan and up.

Configuring the webhook

In the SimplyForms dashboard open Integrations and configure your webhook:

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

Example webhook payload

json
{
  "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)

javascript
// 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:

bash
# 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 dashboardu
Important
Always verify the x-simplyforms-signature header to protect against unauthorized requests.

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

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

Enterprise feature
Server-side validation rules are available on the Enterprise plan.
json
{
  "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:

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

text
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:30

Ready-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 band
  • clean-card β€” Clean Card β€” light bg, purple top accent bar
  • bold-header β€” Bold Header β€” full-width dark header with white title
  • notification β€” Notification β€” inline NEW badge, compact padding
  • editorial β€” Editorial β€” serif fonts, warm paper tones
  • dark-first β€” Dark First β€” dark-mode-first with light media query
Live preview
The dashboard renders a live preview of every preset and of your custom HTML using sample data, so you can see the exact email before saving.

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.

Whole-email rendering
When mode is set to custom your template IS the entire email HTML document. The SimplyForms notification shell (subject line header, footer branding) is not added. With mode=default the standard shell is always used.
Availability
Custom email templates (mode=custom) are available on the EXTEND plan only. Preset selection and live preview are available to all authenticated users.
html
<!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

VariableDescriptionExample
{{ 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 }}
fieldsDict 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.

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

html
<!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>
Important
To activate the autoresponder the form must contain a field with name="email" β€” the automatic reply is sent to that address.

Plain text fallback

Always add a plain text version for email clients that do not support HTML:

text
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

bash
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

HeaderValueRequired
Content-Typeapplication/x-www-form-urlencoded, multipart/form-data, application/jsonYes

Response

json
{
  "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

json
{
  "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).