1Create a FormSMTP project
Sign up free and add a project. Pick your email provider so the SMTP host and port fill in, enter your login, and click Test connection. Not sure which provider? Your existing mailbox works: see the Gmail or Zoho guides.
2Lock the project to your site
In Settings → Security, set Allowed websites to https://yoursite.com and Allowed recipients to your own address. Now the key in your page can only email you, only from your site, and only up to your hourly limit.
3Option A: Client-side component
Works with adapter-static, plain Svelte on Vite, or any host. Add PUBLIC_FORMSMTP_KEY=… to .env.
<!-- src/lib/ContactForm.svelte -->
<script>
import { PUBLIC_FORMSMTP_KEY } from "$env/static/public";
let status = $state("idle"); // idle | sending | sent | error
let error = $state("");
async function onsubmit(e) {
e.preventDefault();
status = "sending";
try {
const res = await fetch("https://app.formsmtp.com/api/v1/form/" + PUBLIC_FORMSMTP_KEY, {
method: "POST",
headers: { Accept: "application/json" },
body: new URLSearchParams(new FormData(e.currentTarget)),
});
const data = await res.json();
if (!data.success) throw new Error(data.error);
status = "sent";
} catch (err) {
error = err.message;
status = "error";
}
}
</script>
{#if status === "sent"}
<p>Thanks! We'll be in touch soon.</p>
{:else}
<form {onsubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required></textarea>
<input name="_gotcha" style="display: none" tabindex="-1" autocomplete="off" />
<button disabled={status === "sending"}>{status === "sending" ? "Sending…" : "Send"}</button>
{#if status === "error"}<p role="alert">{error}</p>{/if}
</form>
{/if}Not using SvelteKit? Replace the $env import with import.meta.env.VITE_FORMSMTP_KEY. On Svelte 4, use let variables and on:submit|preventDefault.
4Option B: SvelteKit form action
When your app runs on a server (Node, Vercel, Netlify, Cloudflare), keep the key private. Add FORMSMTP_KEY=… to .env and leave Allowed websites empty, since requests come from your server.
// src/routes/contact/+page.server.js
import { fail } from "@sveltejs/kit";
import { FORMSMTP_KEY } from "$env/static/private";
export const actions = {
default: async ({ request }) => {
const form = await request.formData();
const res = await fetch("https://app.formsmtp.com/api/v1/send", {
method: "POST",
headers: { Authorization: `Bearer ${FORMSMTP_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
to: "you@yourdomain.com",
replyTo: form.get("email"),
subject: `New message from ${form.get("name")}`,
text: String(form.get("message")),
}),
});
const data = await res.json();
if (!data.success) return fail(res.status, { error: data.error });
return { sent: true };
},
};<!-- src/routes/contact/+page.svelte -->
<script>
import { enhance } from "$app/forms";
let { form } = $props();
</script>
{#if form?.sent}
<p>Thanks! We'll be in touch soon.</p>
{:else}
<form method="POST" use:enhance>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required></textarea>
<button>Send</button>
{#if form?.error}<p role="alert">{form.error}</p>{/if}
</form>
{/if}With use:enhance the page doesn’t reload; without JavaScript it still works as a normal form post.
FAQ
Which option should I use?
Use the form action whenever your SvelteKit app runs on a server, since the key never reaches the browser. Use the client-side component for adapter-static builds and plain Svelte.
Why does $env/static/public need the PUBLIC_ prefix?
SvelteKit only lets variables starting with PUBLIC_ into browser code, so private secrets can’t leak by accident. A FormSMTP key locked to your site is fine to make public.
Why did my message land in spam?
Usually the From address and the SMTP account don’t match, or the domain has no SPF/DKIM records. Send from the address you log in with, and check the Deliverability panel in FormSMTP’s Settings.