# JavaScript and fetch

Submit to Nisuform with fetch to keep visitors on the page, then read the JSON response to show success or an error.

Send your fields with `fetch` when you want to handle the submit yourself. Requests that do not ask for `text/html` get a JSON response instead of a thank-you page.

```js
const response = await fetch('https://api.nisuform.com/s/YOUR_FORM_KEY', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
  body: JSON.stringify({ name: 'Jane Doe', email: 'jane@example.com', message: 'Hello!' }),
})
const result = await response.json()
if (!result.ok) {
  throw new Error(result.message)
}
```

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

form.addEventListener('submit', async (event) => {
  event.preventDefault()
  const response = await fetch(form.action, {
    method: 'POST',
    headers: { Accept: 'application/json' },
    body: new FormData(form),
  })
  const result = await response.json()
  form.replaceWith(result.ok ? 'Thanks! We got your message.' : result.message)
})
```

```js
const response = await fetch('https://api.nisuform.com/s/YOUR_FORM_KEY', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
  body: new URLSearchParams({ email: 'jane@example.com', message: 'Hello!' }),
})
```

`FormData` is sent as `multipart/form-data`, which is also how you send files. In that case, don't set the `Content-Type` header yourself. The browser adds it with the right boundary.

## Responses

A stored submission answers `200` with its ID:

```json
{ "ok": true, "submissionId": "3f2c8a61-5d4e-4b7a-9c1f-2a6e8d0b4c17" }
```

When your team has used 95% of its monthly submissions, the response also carries a warning. The submission is still stored:

```json
{ "ok": true, "submissionId": "3f2c8a61-5d4e-4b7a-9c1f-2a6e8d0b4c17", "warning": "near_limit" }
```

A submission caught by the honeypot, the fill-time check or your blocked IPs and countries also answers `200`, but without a `submissionId`. Bots can't tell they were filtered.

```json
{ "ok": true }
```

A rejected submission answers with a matching HTTP status and an error code you can branch on:

```json
{ "ok": false, "error": "rate_limited", "message": "Too many requests. Try again shortly" }
```

The `message` is written for people, so you can show it to the visitor as is. Every code is listed in [Errors](/docs/reference/errors).

## Handle errors

```js
async function submit(data) {
  const response = await fetch('https://api.nisuform.com/s/YOUR_FORM_KEY', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    body: JSON.stringify(data),
  })
  const result = await response.json()
  if (result.ok) {
    return { sent: true }
  }
  if (result.error === 'rate_limited') {
    return { sent: false, message: 'Please wait a minute and try again.' }
  }
  return { sent: false, message: result.message }
}
```

## Send the fill time

Browsers that submit a form faster than a person could are almost always bots. Record when the form was shown and send the difference as `_elapsed`, in milliseconds. Submissions faster than 2 seconds are dropped.

```js
const shownAt = Date.now()

form.addEventListener('submit', async (event) => {
  event.preventDefault()
  const data = new FormData(form)
  data.set('_elapsed', String(Date.now() - shownAt))
  await fetch(form.action, { method: 'POST', headers: { Accept: 'application/json' }, body: data })
})
```

The styled embed from the form builder already does this for you.

## How values are saved

* Every value is saved as text.
* JSON numbers and booleans become text, like `"3"` and `"true"`.
* JSON arrays of plain values are joined with a comma, like `"Design, Development"`.
* Nested objects are saved as JSON text.
* A JSON body must be an object. Anything else is treated as an empty submission.

## CORS

The endpoint sends `Access-Control-Allow-Origin: *` and answers preflight requests, so `fetch` works from any website. Cookies and credentials are not needed, so don't send them.

Only the `Content-Type` and `Accept` request headers are allowed. Adding other headers, like `Authorization`, makes the browser's preflight check fail. The response's `Retry-After` header can't be read from browser JavaScript, so wait a minute after a `429` before retrying. To accept submissions only from your own sites, set [allowed websites](/docs/protection/access-rules).

## Submitting from a server

You can post to the endpoint from your own backend, a serverless function or a script. Keep in mind:

* Requests from a server usually have no `Origin` or `Referer` header, so allowed websites do not apply to them.
* Rate limits are per IP address. Every request from your server shares the server's IP, so a busy relay can hit the limit of 30 requests a minute per IP, and 5 a minute per form. Post from the visitor's browser when you can.
* Nisuform records your server's IP address, user agent and location instead of the visitor's.
