Sending data
Framework recipes
formto.email is just an HTTP POST endpoint, so any framework works. These are the patterns we recommend.
React
A self-contained controlled form with inline status. Replace YOUR_FORM_ID with your endpoint key.
ContactForm.tsxtsx
import { useState } from "react";
export function ContactForm() {
const [status, setStatus] = useState<"idle" | "sending" | "ok" | "error">("idle");
const [error, setError] = useState<string | null>(null);
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus("sending");
setError(null);
const res = await fetch("https://formto.email/f/YOUR_FORM_ID", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(e.currentTarget),
});
if (res.ok) {
e.currentTarget.reset();
setStatus("ok");
} else {
const data = await res.json().catch(() => ({}));
setError(data.error || "Something went wrong.");
setStatus("error");
}
}
if (status === "ok") return <p>Thanks — we got your message.</p>;
return (
<form onSubmit={onSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button disabled={status === "sending"}>
{status === "sending" ? "Sending…" : "Send"}
</button>
{error && <p role="alert">{error}</p>}
</form>
);
}Next.js (Server Action)
If you want to keep the form ID off the client, post from a Server Action. The endpoint is happy with form-encoded bodies.
app/contact/page.tsxtsx
async function submitContact(formData: FormData) {
"use server";
const res = await fetch(`https://formto.email/f/${process.env.FORMTO_ID}`, {
method: "POST",
headers: { Accept: "application/json" },
body: formData,
});
if (!res.ok) throw new Error("Submission failed");
}
export default function ContactPage() {
return (
<form action={submitContact}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button>Send</button>
</form>
);
}Vue 3
ContactForm.vuevue
<script setup>
import { ref } from "vue";
const status = ref("idle");
async function submit(e) {
status.value = "sending";
const res = await fetch("https://formto.email/f/YOUR_FORM_ID", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(e.target),
});
status.value = res.ok ? "ok" : "error";
if (res.ok) e.target.reset();
}
</script>
<template>
<form @submit.prevent="submit" v-if="status !== 'ok'">
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button>Send</button>
</form>
<p v-else>Thanks — we got your message.</p>
</template>Svelte
ContactForm.sveltesvelte
<script>
let status = "idle";
async function submit(e) {
status = "sending";
const res = await fetch("https://formto.email/f/YOUR_FORM_ID", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(e.target),
});
status = res.ok ? "ok" : "error";
if (res.ok) e.target.reset();
}
</script>
{#if status === "ok"}
<p>Thanks — we got your message.</p>
{:else}
<form on:submit|preventDefault={submit}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button>Send</button>
</form>
{/if}WordPress / Webflow / Squarespace
Any site builder that lets you embed an HTML block works the same as plain HTML — paste a <form> with the right action URL into a code/embed block. You don't need a plugin and you don't need to host anything.
html
<form action="https://formto.email/f/YOUR_FORM_ID" method="POST">
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<input type="hidden" name="_redirect" value="https://yoursite.com/thanks" />
<button type="submit">Send</button>
</form>Don't have a place to put HTML?
Use a hosted form. We give you a public link with a styled form on it — no HTML required.