EMBEDDING

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

  1. Your page collects values in a normal HTML form.
  2. On submit, you POST them as JSON to /v1/forms/<publicKey>/submissions.
  3. Contact validates the values against your schema and the request against your allowed origins.
  4. On success it returns 202 and 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.

Always send 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.