# AJAX / fetch

Part of the sendm8 docs: https://sendm8.com/docs#ajax. Index for AI assistants: https://sendm8.com/llms.txt

Want to stay on the page and show your own message? Send the form with `fetch` and an `Accept: application/json` header. Instead of a redirect you get JSON back, with a proper HTTP status code. It works with any framework, or none.

**FormData: `contact.js`**

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

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const res = await fetch("https://sendm8.com/f/k3x9q2m7ab", {
    method: "POST",
    body: new FormData(form),
    headers: { Accept: "application/json" },
  });
  const result = await res.json();
  if (result.ok) form.replaceWith("Thanks! It's on its way.");
  else alert(result.error.message);
});
```

**HTML: `contact.html`**

```html
<form id="contact">
  <label>Your email <input name="email" type="email" required></label>
  <label>Message <textarea name="message" required></textarea></label>
  <button>Send</button>
</form>
```

**JSON body: `contact.js`**

```js
const res = await fetch("https://sendm8.com/f/k3x9q2m7ab", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
  },
  body: JSON.stringify({
    email: "alex@moreno.dev",
    message: "Hiya! Are you free in October?",
  }),
});

const result = await res.json();
// { ok: true, id: "01K53N7XQ2R8M4DE7Q2K9F4X3M" }
```

### What comes back

Success

`200 OK`

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "ok": true,
  "id": "01K53N7XQ2R8M4DE7Q2K9F4X3M"
}
```

Error

`4xx`

```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/json

{
  "ok": false,
  "error": {
    "code": "quota_exceeded",
    "message": "This form has reached its monthly submission limit."
  }
}
```

`ok` is always there, so checking it is enough. The `id` is the submission’s id in your dashboard. On failure, `error.message` is a plain-English sentence you can show to your visitor as it is, and `error.code` (like `quota_exceeded` or `rate_limited`) stays the same for your code to check.

| Status | What it means |
| --- | --- |
| 200 | Stored. A 2xx always means the submission is safely saved. |
| 303 | Browser posts only: redirect to your thank-you page, or to the Turnstile challenge page. |
| 400 | We couldn’t read it: bad JSON, more than 100 fields, or a body over 64 KB without files. |
| 403 | The page it came from isn’t on the form’s allowed domains, or a Turnstile check failed. |
| 404 | No form with that id. Check for a typo in the URL. |
| 410 | The form has been disabled by its owner. |
| 413 | A file is over 5 MB, or the files add up to more than 10 MB. |
| 423 | The form is paused. Nothing is stored until it’s switched back on. |
| 429 | Too many too fast (10 a minute per visitor), or the form’s monthly cap is used up. |

> **Sending files with fetch?**
>
> Pass the `FormData` as the body and don’t set a `Content-Type` header yourself. The browser adds the right multipart header, boundary included.
