HTML Form Setup
The recommended way to integrate Postbox with an HTML form is using JavaScript fetch(). This gives you full control over the user experience - inline validation errors, success messages, and no page navigation.
Basic setup
Section titled “Basic setup”<form id="contact-form"> <label for="name">Name</label> <input type="text" id="name" name="name" required />
<label for="email">Email</label> <input type="email" id="email" name="email" required />
<label for="message">Message</label> <textarea id="message" name="message"></textarea>
<!-- honeypot field: hidden from users, catches bots --> <div style="position:absolute; left:-9999px; top:-9999px; opacity:0; pointer-events:none;"> <input type="text" name="website" tabindex="-1" autocomplete="off" /> </div>
<button type="submit">Send</button> <div id="form-errors" style="display:none; color:red;"></div> <div id="form-success" style="display:none; color:green;">Submitted successfully!</div></form>
<script> document .getElementById("contact-form") .addEventListener("submit", async (e) => { e.preventDefault(); const errorsEl = document.getElementById("form-errors"); const successEl = document.getElementById("form-success"); errorsEl.style.display = "none"; successEl.style.display = "none";
const data = Object.fromEntries(new FormData(e.target));
try { const res = await fetch("https://usepostbox.com/api/.../f/contact", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), });
if (res.ok) { successEl.style.display = "block"; e.target.reset(); } else { const body = await res.json(); if (body.error?.details) { const messages = Object.entries(body.error.details) .map(([field, errs]) => `${field}: ${errs.join(", ")}`) .join("\n"); errorsEl.textContent = messages; } else { errorsEl.textContent = body.error?.message || "Something went wrong."; } errorsEl.style.display = "block"; } } catch (err) { errorsEl.textContent = "Network error. Please try again."; errorsEl.style.display = "block"; } });</script>Why fetch instead of form action?
Section titled “Why fetch instead of form action?”A <form action> submission triggers a full page navigation. The browser leaves your page, and you lose the ability to:
- Show inline per-field validation errors (Postbox returns
error.detailswith field-level messages) - Keep the form state so users can correct and resubmit
- Show a success message in context
With fetch(), you get the full response object, can parse errors, and give users a smooth experience.
Postbox endpoints have CORS enabled. You can submit from any origin - no proxy needed.
- Match field names to your Postbox form schema (the
nameattributes in your HTML must match) - Use HTML validation (
required,type="email", etc.) for client-side checks - Postbox validates server-side too - Hidden fields can pass metadata (e.g., page URL, referrer) alongside user input
- Honeypot fields should use robust CSS hiding (not just
display:nonewhich some bots detect)