A React app that runs in the browser has nowhere to send a contact form unless you build an API for it. You can skip that: post the form straight to your InboxPanda endpoint with fetch, and the message lands in an inbox that keeps it, filters spam and emails you.
1
The component
TSXimport { useState } from "react" export function ContactForm() { const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle") async function onSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() setStatus("sending") const res = await fetch("https://inboxpanda.io/f/your-form-id", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(Object.fromEntries(new FormData(e.currentTarget))), }) setStatus(res.ok ? "sent" : "error") } if (status === "sent") return <p>Thanks. We'll get back to you soon.</p> return ( <form onSubmit={onSubmit}> <label>Name <input name="name" autoComplete="name" required /></label> <label>Email <input name="email" type="email" autoComplete="email" required /></label> <label>Message <textarea name="message" required /></label> <input name="_honey" style={{ display: "none" }} tabIndex={-1} autoComplete="off" /> <button disabled={status === "sending"}>Send</button> {status === "error" && <p>Something went wrong. Please try again.</p>} </form> ) }
To get your own endpoint, type your email on the home page and press Create your inbox: no password, no card. The same component, with your endpoint in it, is under Settings, then Messages in, then React. Open the link we email you to claim the inbox.
2
How it works
new FormData(e.currentTarget)reads every input by itsname, so adding a field is adding an input. The name is the field's label in your inbox.Accept: application/jsonmakes the endpoint answer{"ok": true, "id": "…"}instead of a thank-you page.- The endpoint accepts posts from any origin, so there's no CORS setup.
_honeyis a spam trap: hidden from people, filled in by bots. Fields starting with_are settings, not part of the message.
3
Show the reason when it fails
A refused message answers with a status and
{"ok": false, "error": "…"}, in words you can show as they are:TSXconst data = await res.json() if (!data.ok) setError(data.error) // e.g. "Submission is too large (max 32 KB)."
4
Uploads
To accept a file, send
FormDataitself instead of JSON, and leave outContent-Typeso the browser sets the multipart boundary:TSXconst res = await fetch("https://inboxpanda.io/f/your-form-id", { method: "POST", headers: { Accept: "application/json" }, body: new FormData(e.currentTarget), })
Add
<input type="file" name="attachment" />to the form. PDF, Word, text and image files are accepted, up to 4 MB per message in total, once you've claimed the inbox.5
Several forms
Each inbox has its own endpoint. A careers form and a sales form can post to two inboxes, or both to one inbox with a hidden field telling them apart:
TSX<input type="hidden" name="form" value="careers" />
Panda labels each message by what it says, so one inbox for everything works too.
6
Check it works
Send the form from a different email address than your own. It shows up in your inbox within a few seconds, and you get an email about it. If it lands in Spam, press Not spam: a message that only says "test" can look like spam.