splitforms.com
All articles/ TUTORIALS8 MIN READPublished May 1, 2026

Send an HTML form to email without PHP

PHP mail() is brittle, blocked by most hosts, and ends up in spam. Three modern alternatives that work on Vercel, Netlify, Cloudflare Pages, and S3.

✶ Written by
splitforms.com / blog

Founder of splitforms — the form backend API for developers. Writes about form UX, anti-spam, and shipping web apps without backend code.

Why PHP mail() is brittle

PHP mail() looks like a one-liner:

<?php
mail('you@yourdomain.com', 'New contact', $_POST['message']);

Underneath, that's a system call to sendmail(or postfix if your host is fancy). The shell-out has no concept of:

  • SPF or DKIM signing — outgoing mail is unauthenticated
  • Bounces, retries, or queueing — if the receiving server is down, the message just dies
  • Reply-To headers — you have to set them manually as raw header strings
  • Reputation — most shared-hosting IPs are on every major spam blocklist
  • Logging — when mail goes missing, you have no visibility

In practice: you ship the form, it "works" in dev (you sent one to yourself, it arrived), then you go live and silently lose 60–95% of submissions to spam folders. We've had splitforms customers migrate from PHP mail() who gained 4x more leads the day they switched, just because messages started arriving in the inbox instead of spam.

Plus: most modern hosts (Vercel, Netlify, Cloudflare Pages, GitHub Pages, S3 + CloudFront) don't run PHP at all. If you're reading this on a static-site host, PHP isn't even an option.

Option 1: Hosted form backend

The fastest path. Point your form's action at a hosted backend, drop in an access key, ship.

<form action="https://splitforms.com/api/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
  <input type="hidden" name="subject" value="New lead from yoursite.com" />
  <input type="hidden" name="redirect" value="https://yoursite.com/thanks" />

  <label>Name <input name="name" required /></label>
  <label>Email <input name="email" type="email" required /></label>
  <label>Message <textarea name="message" required></textarea></label>

  <input name="botcheck" type="checkbox" style="display:none" tabindex="-1" />
  <button type="submit">Send</button>
</form>

That's the entire integration. splitforms receives the POST, runs spam filtering (honeypot + AI scoring), authenticates the outgoing email with SPF/DKIM on our domain, sets Reply-To to the visitor's submitted email, sends through Postmark, and 302-redirects the visitor to your /thanks page.

Time to first email: ~90 seconds. Cost: $0 for the first 1,000 submissions/month; $10/mo for 5,000 after that. Other hosted backends in this class: Formspree, Web3Forms, Basin, Getform, Formcarry, Netlify Forms.

Option 2: Serverless function + transactional API

You want full ownership. Write a tiny function, call a transactional email API.

Vercel Route Handler with Resend:

// app/api/contact/route.ts
import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(req: Request) {
  const data = await req.formData();

  if (data.get('botcheck')) return new Response('OK');

  const name = String(data.get('name') ?? '');
  const email = String(data.get('email') ?? '');
  const message = String(data.get('message') ?? '');

  const { error } = await resend.emails.send({
    from: 'forms@yourdomain.com',
    to: 'you@yourdomain.com',
    replyTo: email,
    subject: `Contact form: ${name}`,
    text: `From: ${name} <${email}>\n\n${message}`,
  });

  if (error) return new Response('Send failed', { status: 500 });
  return Response.redirect(new URL('/thanks', req.url), 303);
}

Cloudflare Worker version (no Resend SDK, raw fetch):

export default {
  async fetch(req, env) {
    if (req.method !== 'POST') return new Response('Nope', { status: 405 });
    const data = await req.formData();
    if (data.get('botcheck')) return new Response('OK');

    const r = await fetch('https://api.resend.com/emails', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer ' + env.RESEND_API_KEY,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        from: 'forms@yourdomain.com',
        to: 'you@yourdomain.com',
        reply_to: data.get('email'),
        subject: 'New contact form submission',
        text: data.get('message'),
      }),
    });

    if (!r.ok) return new Response('Failed', { status: 500 });
    return Response.redirect('https://yoursite.com/thanks', 303);
  },
};

Time to ship: 30–60 minutes including DNS record propagation. Cost: $0 (Resend free tier: 3,000 emails/month; Postmark free trial: 100/month). You own deliverability — verify your sending domain in the provider, add their SPF/DKIM records to DNS, monitor bounces.

Option 3: Node + third-party SMTP

You have a Node service somewhere (Express, Fastify, Hono). Ship through SMTP using nodemaileragainst a real provider — not your VPS's default sendmail.

import express from 'express';
import nodemailer from 'nodemailer';

const app = express();
app.use(express.urlencoded({ extended: true }));

const transport = nodemailer.createTransport({
  host: 'smtp.postmarkapp.com',
  port: 587,
  auth: {
    user: process.env.POSTMARK_TOKEN,
    pass: process.env.POSTMARK_TOKEN,
  },
});

app.post('/contact', async (req, res) => {
  if (req.body.botcheck) return res.sendStatus(200);

  await transport.sendMail({
    from: 'forms@yourdomain.com',
    to: 'you@yourdomain.com',
    replyTo: req.body.email,
    subject: 'New contact form submission',
    text: req.body.message,
  });

  res.redirect(303, '/thanks');
});

app.listen(3000);

The pattern is identical for any SMTP provider — Postmark, SendGrid, Mailgun, Amazon SES, Brevo. Don't use your VPS's default sendmail binary; the deliverability outcomes are the same as PHP mail().

Comparison: which option fits your stack

OptionTime to shipCost (small site)You own deliverability?Best when
splitforms / hosted backend~90 sec$0–10/moNo (we do)Static sites, fast iteration
Serverless + Resend/Postmark30–60 min$0–15/moYesAlready on Vercel/Workers
Node + SMTP1–2 hr$0–15/moYesExisting Node service
PHP mail() (do not use)5 minHidden cost: lost leadsSort ofNever, in 2026

Per-host notes

  • Vercel — no PHP. Use Option 1 or write a Route Handler (Option 2).
  • Netlify— Netlify Forms (free for 100/mo, $19/mo for 1,000) is built in. splitforms' free tier is 5x the volume at the same price.
  • Cloudflare Pages — pair with Workers for Option 2. splitforms also works directly.
  • GitHub Pages — pure static. Hosted backend (Option 1) is your only realistic path.
  • S3 + CloudFront — hosted backend, or API Gateway + Lambda for Option 2.
  • Astro / Hugo / Eleventy / Jekyll — same story; the framework is irrelevant to email delivery.

Deliverability checklist

  1. Send from a domain you own. Never from gmail.com via SMTP relay — DMARC will reject it.
  2. Add SPF and DKIM records to that domain's DNS, copying from your provider's setup page.
  3. Set Reply-To to the visitor's submitted email so you can reply directly from Gmail.
  4. Include a plain-text part, not just HTML. Spam filters reward it.
  5. Test with mail-tester.com before going live. Aim for 9/10 or higher.
  6. Monitor bounces. Most providers have a webhook for it; surface it in your error tracking.

Tech support and troubleshooting

Five issues that account for almost every "no PHP, still no emails" outage:

  • Form submits but no email — check that the access_key is the live one (not a sandbox), and that the destination inbox isn't silently sending the message to a tab/folder.
  • Vercel function returns 405 — your app/api/contact/route.ts only exports GET. Add the POST handler explicitly.
  • Resend returns 403 — sending domain isn't verified. Add the SPF and DKIM CNAME records and wait for DNS propagation.
  • Emails go to spam after a week — DKIM key rotated or SPF record was changed. Re-run mail-tester.com and re-verify the domain in your provider.
  • Reply hits the form provider, not the lead — the Reply-To header is missing. Pass the visitor's email through explicitly when calling the email API.

The full delivery pipeline, retry policy, and signature spec live in the splitforms docs and the API reference; account questions live in the splitforms FAQ.

Frequently asked questions

How can I send HTML form data to email without using PHP?

Three modern options work everywhere: (1) point your form's action attribute at a hosted form backend like splitforms (https://splitforms.com/api/submit), (2) write a tiny serverless function on Vercel/Netlify/Cloudflare Workers that calls a transactional email API like Resend or Postmark, or (3) use third-party SMTP from a JavaScript runtime. Form backend is fastest; serverless gives you full control.

Why is PHP mail() considered unreliable?

PHP mail() invokes the local sendmail binary, which on shared hosting almost always sends from an unauthenticated IP with no SPF or DKIM signing. Gmail and Outlook quietly route those emails to spam or drop them entirely. Many hosts (Vercel, Netlify, Cloudflare Pages, GitHub Pages, S3) don't even support PHP. mail() is fine on a fully-configured mail server with proper DNS, but that's a level of ops work most teams shouldn't take on.

Can I send email from a static HTML site without any backend?

Not directly — there has to be SOMETHING accepting the POST and authenticating to an SMTP server. The trick is to outsource that 'something' to a hosted form backend or a serverless function. From your static site's perspective, it's still just an HTML form with no server code on your end.

What's the cheapest way to send HTML form to email?

splitforms is free for 1,000 submissions/month. If you outgrow that, a serverless function plus Resend's free tier (3,000 emails/month) costs $0. Past that, Postmark and SendGrid start around $15/month. Self-hosted SMTP is technically free but the 'unsubscribe me from your spam folder' bills cost more than $15/month in time.

Do I need a domain to receive form submissions by email?

You need a destination inbox, which can be any email address (Gmail, ProtonMail, your work email). You don't need to own a domain to receive. To send from your own domain (forms@yourbrand.com), you'll need to add SPF and DKIM DNS records — splitforms handles this on their domain by default and lets you BYO domain when you're ready.

Next steps

About the author
✻ ✻ ✻

Get your free contact form API key in 60 seconds.

1,000 free form submissions per month. No credit card. No SDK, no PHP, no plugin. Drop one POST endpoint in your form and submissions land in your inbox.

Generate access key →Read the docs
v0.1 · founders pricing locked in · early access open