Embed your form anywhere.
Contact is a form backend, not a form builder UI. You keep full control of the markup and styling and send the values to your form endpoint.
How a submission works
- Your page collects values in a normal HTML form.
- On submit, you POST them as JSON to
/v1/forms/<publicKey>/submissions. - Contact validates the values against your schema and the request against your allowed origins.
- On success it returns
202and queues your delivery rules.
Plain HTML
Works in any site — no build step required. Field name attributes must match your schema keys.
<!-- The form fields. Names must match your saved schema. -->
<form id="contact">
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required></textarea>
<input type="text" name="_website" tabindex="-1" autocomplete="off" hidden aria-hidden="true" />
<button type="submit">Send</button>
</form>
<p id="status" role="status"></p>
<script>
document.getElementById("contact").addEventListener("submit", async (event) => {
event.preventDefault();
const form = event.currentTarget;
const status = document.getElementById("status");
status.textContent = "Sending…";
const response = await fetch("https://contact.kirandhakal.me/v1/forms/YOUR_PUBLIC_KEY/submissions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID()
},
body: JSON.stringify(Object.fromEntries(new FormData(form)))
});
const result = await response.json();
status.textContent = response.ok ? result.message : (result.detail || "Something went wrong.");
if (response.ok) form.reset();
});
</script>Allowed origins
Each form keeps an allowlist of origins. A request from an origin that is not on the list is rejected with 403. Add both your production origin and any development origin, for example https://example.com and http://localhost:3000. Include the scheme and port, and never a path or trailing slash. Server-to-server requests (no browser Origin header) are always allowed, so keep admin keys secret.
Map your fields
Name each input to match the schema you saved. For example an input named email fills the email property. Required fields must be present and valid or the request fails with 422. Add the hidden _website field shown in the examples as a honeypot — real users never fill it, so a value marks the submission as spam.
Content-Type: application/json and a unique Idempotency-Key. A plain HTML form POST (browser navigation) is not supported — submit with JavaScript or from your server.Common mistakes
- Submitting without an
Idempotency-Key→422. - Origin not allowlisted, or it includes a path or trailing slash →
403. - Field names that do not match the schema →
422. - Putting an admin key or integration secret in browser code.
Still stuck? See Troubleshooting & FAQ.