Documentation
Integrate in minutes.
Your website never touches SMTP. It holds two values and calls two endpoints — the service does delivery, spam filtering, and the branded auto-reply.
1The two-value contract
Create a form in your dashboard and copy its ID. Give your site exactly two values:
FORM_API_URLThe service origin — where challenges and submissions go.
FORM_IDOne form's UUID. Each site (or each form) gets its own.
2The copy-paste helper
Works in Astro, Next.js, Nuxt, Vue, Svelte, or a plain <script type="module">.
// form-service.js — drop into any project (framework-agnostic)
const API_URL = import.meta.env.PUBLIC_FORM_API_URL; // or process.env / hardcode
const FORM_ID = import.meta.env.PUBLIC_FORM_ID; // UUID from your dashboard
async function sha256Hex(text) {
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, "0")).join("");
}
export async function submitForm(fields, { lang = "en" } = {}) {
// 1. Get a proof-of-work challenge
const c = await (await fetch(`${API_URL}/api/challenge`)).json();
// 2. Solve it (costs a bot CPU, costs your user ~a blink)
let nonce = 0;
const prefix = "0".repeat(c.difficulty);
while (!(await sha256Hex(`${c.challenge}:${nonce}`)).startsWith(prefix)) nonce++;
// 3. Submit the fields
const res = await fetch(`${API_URL}/api/submit/${FORM_ID}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...fields,
_lang: lang,
_gotcha: "", // honeypot — leave empty
pow_challenge: c.challenge,
pow_timestamp: c.timestamp,
pow_nonce: String(nonce),
}),
});
if (!res.ok) throw new Error((await res.json()).error ?? "Submission failed");
return res.json();
}3Use it
import { submitForm } from "./form-service";
await submitForm({
name: "Ada Lovelace",
email: "ada@example.com", // used for the branded auto-reply
message: "I'd like a quote.",
});email field— if present, the submitter receives your branded auto-reply._lang— "en" or "fr" — selects the auto-reply language._gotcha— hidden honeypot input. Humans leave it empty; bots fill it and get silently dropped.files— send base64 payloads — they are uploaded to storage and arrive as download links.
4Let your AI do it
Using Claude, Cursor, or Copilot? We publish a machine-readable install file at /llm-install.md — paste this prompt and your AI does the whole integration, correctly, on the first try:
Read https://inlett.vercel.app/llm-install.md
and integrate Inlet into this project exactly as it instructs.
My values: FORM_API_URL=https://inlett.vercel.app FORM_ID=<paste yours>Claude Code users: drop our skill file .claude/skills/inlet/SKILL.md and Claude wires forms, honeypots and webhooks by itself in any project.
5Webhooks (optional)
Add a webhook URL to a form and every stored lead is POSTed to your endpoint as submission.created, signed Stripe-style in the X-Inlet-Signature header — verify it and reject replays in five lines:
// Verify Inlet's webhook signature (Node) — read the RAW body first.
import crypto from "node:crypto";
export function verifyInletWebhook(rawBody, sigHeader, secret, toleranceSec = 300) {
const p = Object.fromEntries(sigHeader.split(",").map(s => s.split("=")));
const expected = crypto.createHmac("sha256", secret).update(`${p.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(p.t)) < toleranceSec;
return fresh && crypto.timingSafeEqual(Buffer.from(p.v1), Buffer.from(expected));
}Delivery is fire-and-forget with one retry — a down endpoint can never block or lose a lead. Endpoints must be https.
What happens server-side
Four spam gates. Honeypot, proof-of-work verification, NLP keyword filter, reverse-DNS VPN/cloud blocking. Blocked attempts are logged, never delivered.
Two emails. The owner gets the lead notification; the submitter gets an auto-reply in your client's branding — logo, colors, sender name. SMTP fallback rotation means a failing credential never loses a lead.