JavaScript (fetch)

Submit forms with the browser's fetch API for AJAX-style submissions: no page reload, and a JSON response you can act on.

How JSON responses work

The endpoint returns JSON instead of a redirect whenever the request asks for it. Send the Accept: application/json header (or post a JSON body) and a successful submission responds with:

{ "ok": true, "submissionId": "sub_abc123" }

An error responds with a non-2xx status and a body like:

{
  "error": "validation_error",
  "message": "One or more fields are invalid.",
  "fieldErrors": { "email": "Enter a valid email address." }
}

fieldErrors is only present for validation failures, and maps each rejected field name to a message you can render next to the input.

Posting FormData

The most direct approach is to build a FormData from your existing form and post it. Don't set Content-Type yourself — the browser adds the correct multipart boundary for you. Keep the hidden _honey field in your markup; it's captured along with everything else.

const form = document.querySelector("#contact");

form.addEventListener("submit", async (event) => {
  event.preventDefault();

  const response = await fetch(
    "https://api.formcrest.com/v1/submit/abc123",
    {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(form),
    },
  );

  const result = await response.json();

  if (response.ok && result.ok) {
    // Success — result.submissionId is available.
    console.log("Submitted:", result.submissionId);
    form.reset();
  } else {
    // Failure — result.error and result.message describe what went wrong.
    console.error(result.error, result.message);
    if (result.fieldErrors) {
      for (const [field, message] of Object.entries(result.fieldErrors)) {
        console.warn(field, message);
      }
    }
  }
});

Posting JSON

You can also send a JSON body directly. Set Content-Type: application/json and the response is JSON automatically.

const response = await fetch(
  "https://api.formcrest.com/v1/submit/abc123",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
    },
    body: JSON.stringify({
      email: "test@example.com",
      message: "Hello",
    }),
  },
);

const result = await response.json();

Idempotency-Key for safe retries

If you retry a submission after a network hiccup, you can avoid creating a duplicate by sending an Idempotency-Key header. Reuse the same key for the retry and we dedupe within a 24-hour window, returning the original result instead of recording a second submission.

const idempotencyKey = crypto.randomUUID();

await fetch("https://api.formcrest.com/v1/submit/abc123", {
  method: "POST",
  headers: {
    Accept: "application/json",
    "Idempotency-Key": idempotencyKey,
  },
  body: new FormData(form),
});

Access key

If the form requires an access key, include it as an access_key field in your FormData or JSON body.

Next steps

  • Submit API reference — the full endpoint contract, headers, and response shapes.
  • Error codes — every error value and what it means.
  • React & Next.js — the same flow wrapped in the @formcrest/sdk package and the useFormSubmit hook.