Form ID

Gatsby

A Gatsby form that already works in the static HTML before hydration, enhanced with fetch after, plus a Gatsby Function for posts with a secret key.

Gatsby renders every page to HTML at build time. Give the form a real action and it posts before React hydrates, or if the bundle fails to load; once hydrated, the component posts with fetch and shows the result in place.

Post from the browser

  1. Add the form id

    .env.production
    GATSBY_IKIFORM_FORM_ID=frm_xxxxxxxxxxxxxxxx

    Gatsby only exposes variables that start with GATSBY_ to browser code, and it inlines them at build time. Add the same line to .env.development for gatsby develop.

  2. Add the component

    src/components/contact-form.js
    import * as React from "react";
    
    const endpoint = `https://ikiform.com/f/${process.env.GATSBY_IKIFORM_FORM_ID}`;
    
    export default function ContactForm() {
      const [status, setStatus] = React.useState("idle");
      const [message, setMessage] = React.useState("");
    
      async function onSubmit(event) {
        event.preventDefault();
        setStatus("sending");
        try {
          const response = await fetch(endpoint, {
            method: "POST",
            headers: { Accept: "application/json" },
            body: new FormData(event.currentTarget),
          });
          const result = await response.json();
          if (result.ok) return setStatus("sent");
          setMessage(result.error.message);
        } catch {
          setMessage("Couldn't reach the server. Try again.");
        }
        setStatus("error");
      }
    
      if (status === "sent") return <p>Thanks, we got it.</p>;
    
      return (
        <form action={endpoint} method="post" onSubmit={onSubmit}>
          <label>
            Email
            <input name="email" type="email" autoComplete="email" required />
          </label>
          <label>
            Message
            <textarea name="message" rows={5} />
          </label>
          <input type="hidden" name="_redirect" value="https://example.com/thanks/" />
          <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
          {status === "error" ? <p role="alert">{message}</p> : null}
          <button disabled={status === "sending"}>Send</button>
        </form>
      );
    }

    Render it from any page, for example src/pages/contact.js, and add src/pages/thanks.js for the no-JavaScript redirect. For field errors and the SDK's human check, extend it as in the React guide.

Post from a Gatsby Function

A Gatsby Function can forward the post with a secret key, which skips allowed domains and the human check. It only runs on hosts that support Gatsby Functions, such as Netlify with Gatsby's adapter; a purely static host like GitHub Pages can't run it. Store IKIFORM_FORM_ID and a secret key with the submissions:write scope as IKIFORM_SECRET_KEY, without the GATSBY_ prefix so they never reach the browser.

src/api/contact.js
export default async function handler(req, res) {
  if (req.method !== "POST") return res.status(405).end();
  const response = await fetch(`https://ikiform.com/f/${process.env.IKIFORM_FORM_ID}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
    },
    body: JSON.stringify(req.body),
  });
  res.status(response.status).json(await response.json());
}

Gatsby parses JSON and form-encoded bodies into req.body, so the page can post to /api/contact either way. All posts then share your function's IP address for rate limits; see From a server.

Allowed domains

This applies to browser posts. Posts from the Gatsby Function use a secret key and skip the check.

Where the form runsAdd to Allowed domains
gatsby develop and gatsby servelocalhost. It matches http://localhost:8000 and http://localhost:9000: Ikiform compares the host name only, never the port.
Your domainexample.com, which also covers www.example.com
Netlifyyour-site.netlify.app. Deploy previews (deploy-preview-12--your-site.netlify.app) and branch deploys (branch--your-site.netlify.app) are not subdomains of it, so add the exact preview host while you test one.
Cloudflare Pagesyour-project.pages.dev. Preview URLs such as abc123.your-project.pages.dev are subdomains of it, so they already match.
  • Never add netlify.app or pages.dev on its own. Every site on that host would match, including other people's.

Test checklist

  1. Run gatsby build and open public/contact/index.html. The form's action should end in your form id, not undefined.
  2. Run gatsby serve with localhost in allowed domains and submit. The thanks message should replace the form.
  3. Turn JavaScript off, reload and submit. You should see Ikiform's thank-you page locally, or /thanks/ on your https: domain.
  4. If you use the function, post to /api/contact on a deploy preview and confirm the submission appears in Ikiform.

Troubleshooting

What you seeWhyFix
404 not_found, and the action ends in /f/undefinedThe variable isn't prefixed with GATSBY_, or wasn't set at build timeName it GATSBY_IKIFORM_FORM_ID and rebuild.
The function answers 403 forbiddenThe secret key is from another workspace, revoked, or missing submissions:writeCreate a secret key with that scope.
/api/contact returns 404 after deployingThe host serves static files onlyDeploy to a host with Gatsby Functions, or use the browser version.
Submissions arrive under the spam filter with origin_mismatchThe page that posted isn't on the form's allowed domainsAdd the host from the table above. With Reject chosen, the same posts get 403 origin_not_allowed instead.