> ## Documentation Index
> Fetch the complete documentation index at: https://formcarry.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# formcarry.js

> formcarry.js sends a form to formcarry from a plain HTML page with one script tag and one call.

It posts the fields as `FormData` when the visitor submits and passes the answer to your function, and the same post carries files and a spam blocker token.

## Prerequisites

Before you start, you need:

* A formcarry account. [Sign up](https://app.formcarry.com/register) is free.
* A form in the [dashboard](https://app.formcarry.com). Its endpoint is on the form's Setup page. The examples use `https://formcarry.com/s/AbC123xyz`; put yours in its place.

## The script tag

To include the library, add the script tag before the closing `</body>` tag, after the form:

```html theme={null}
<script src="https://carrier.formcarry.com/js/v1.js"></script>
```

The script defines one global function, `formcarry`. It is 3.8 KB gzipped and carries its own `fetch` and `Promise` fallbacks, so it runs in browsers without them. It is not on npm; the script tag is the only way to include it.

## The form

To send a form through the library, give the form an `id`, give every field a `name`, and call `formcarry()` after both the form and the script:

```html theme={null}
<form id="contact">
  <input name="name">
  <input name="email" type="email">
  <textarea name="message"></textarea>
  <button type="submit">Send</button>
  <p id="error"></p>
</form>

<script src="https://carrier.formcarry.com/js/v1.js"></script>
<script>
  formcarry({
    form: "AbC123xyz",
    element: "#contact",
    onSuccess: function (answer) {
      document.querySelector("#contact").innerHTML = "<p>Thanks, we got your message.</p>"
    },
    onError: function (answer) {
      document.querySelector("#error").textContent = answer.message
    }
  })
</script>
```

The `name` of each input is the field name formcarry stores. The `email` field is the visitor's address, so it becomes the reply-to address of your notification email and the recipient of the auto response.

The library sends `Accept: application/json`, so the answer is JSON and the visitor stays on your page; the thank you page and the redirect are not used. The form's Setup page in the dashboard shows this call with your form id filled in.

Call `formcarry()` after the form is in the page rather than in the `<head>`, otherwise `element` matches nothing and the call throws. Give the button `type="submit"` rather than `type="button"` with your own click handler, otherwise the form never fires `submit` and the library sends nothing. The browser's own checks, such as `required` and `type="email"`, still run before `submit` fires.

## Options

* `form` **(required)**. The form id, the part of the endpoint after `/s/`. The library builds the URL from it and ignores the form's `action` and `method`.
* `element` **(required)**. A CSS selector for the `<form>`, such as `#contact`. The first match is used. Point it at the `<form>` itself rather than a wrapper around it, otherwise the library throws a `TypeError` on submit and sends nothing.
* `onSuccess` **(required)**. A function called with the answer when the submission is stored. Without it a stored submission ends in `onError` with a `TypeError`.
* `onError` **(required)**. A function called with the answer when formcarry refuses the submission, or with the `Error` when the request never reaches formcarry. Without it a refused submission is an unhandled rejection in the console and the visitor sees nothing.
* `extraData`. An object whose entries are added to the submission as fields, each stored under its key. The object is read when the visitor submits, so a value set on it after the call is sent too. Defaults to no extra fields.

To add fields the visitor does not type, put them in `extraData`:

```js theme={null}
formcarry({
  form: "AbC123xyz",
  element: "#contact",
  extraData: {
    page: window.location.pathname,
    language: window.navigator.language
  },
  onSuccess: function (answer) {
    document.querySelector("#contact").innerHTML = "<p>Thanks, we got your message.</p>"
  },
  onError: function (answer) {
    document.querySelector("#error").textContent = answer.message
  }
})
```

The submission then has `page` and `language` next to the fields the visitor typed.

## Success and error

The library calls `onSuccess` when the answer's `code` is `200` and `onError` for every other answer. Both receive the answer object: `code`, `status`, `title` and `message`, plus `errors` on a `422`.

When the request never reaches formcarry, or `onSuccess` throws, `onError` receives the thrown `Error` instead, which has a `message` and no `code`. To tell the two apart:

```js theme={null}
onError: function (answer) {
  if (answer.code) {
    document.querySelector("#error").textContent = answer.message
  } else {
    document.querySelector("#error").textContent = "The message did not go through. Try again."
  }
}
```

Check `answer.code` rather than showing `answer.message` as it comes, otherwise a dropped connection shows the browser's own wording to the visitor.

To send the visitor to your own page after a stored submission, set `window.location.href` in `onSuccess`:

```js theme={null}
onSuccess: function (answer) {
  window.location.href = "https://example.com/thanks"
}
```

The library leaves the button enabled while the request is in flight, and a second click within 15 seconds answers `429`, again in `onError`. To disable the button until the answer arrives, add your own `submit` listener before the call and enable the button again in `onError`:

```js theme={null}
const form = document.querySelector("#contact")
const button = form.querySelector("button[type=submit]")

form.addEventListener("submit", function () {
  button.disabled = true
})

formcarry({
  form: "AbC123xyz",
  element: "#contact",
  onSuccess: function (answer) {
    form.innerHTML = "<p>Thanks, we got your message.</p>"
  },
  onError: function (answer) {
    button.disabled = false
    document.querySelector("#error").textContent = answer.message
  }
})
```

Disable the button rather than the inputs, otherwise the browser leaves the disabled inputs out of the `FormData` and the submission arrives without them.

## Files

To send files, add a file input with a `name` to the form:

```html theme={null}
<form id="upload">
  <input name="email" type="email">
  <input name="attachment" type="file" multiple>
  <button type="submit">Send</button>
  <p id="error"></p>
</form>
```

The library posts the form as `FormData`, so the files go with the other fields. A `multiple` input sends every chosen file under its name, and several file inputs each send theirs under their own.

The library sets no `Content-Type`, so the browser adds the multipart boundary itself and the form needs no `enctype`. Files are stored on paid plans; on the free plan the rest of the submission is stored without them.

A `File` or `Blob` in `extraData` is sent as a file too. To send a file the visitor dropped outside the form, set it on the `extraData` object when the drop happens:

```html theme={null}
<div id="drop">Drop a file here</div>

<script>
  const extra = {}
  const drop = document.querySelector("#drop")

  drop.addEventListener("dragover", function (e) { e.preventDefault() })
  drop.addEventListener("drop", function (e) {
    e.preventDefault()
    extra.attachment = e.dataTransfer.files[0]
  })

  formcarry({
    form: "AbC123xyz",
    element: "#upload",
    extraData: extra,
    onSuccess: function (answer) {
      document.querySelector("#upload").innerHTML = "<p>Thanks, we got your file.</p>"
    },
    onError: function (answer) {
      document.querySelector("#error").textContent = answer.message
    }
  })
</script>
```

The dropped file is stored under `attachment`, next to the fields from the form.

## Spam blocker

To add a challenge, load the vendor's script, put the widget's `div` inside the form, and paste the secret key into the form's settings under Form Security. With reCAPTCHA v2:

```html theme={null}
<script src="https://www.google.com/recaptcha/api.js" async defer></script>

<form id="contact">
  <input name="email" type="email">
  <textarea name="message"></textarea>
  <div class="g-recaptcha" data-sitekey="SITE_KEY"></div>
  <button type="submit">Send</button>
  <p id="error"></p>
</form>
```

The widget adds a field named `g-recaptcha-response` to the form, so the `FormData` post carries the token and the call stays as it is. hCaptcha and Turnstile work the same way with their own script, widget class and token field:

| Challenge    | Script                                                  | Widget class   | Token field             |
| ------------ | ------------------------------------------------------- | -------------- | ----------------------- |
| reCAPTCHA v2 | `https://www.google.com/recaptcha/api.js`               | `g-recaptcha`  | `g-recaptcha-response`  |
| hCaptcha     | `https://js.hcaptcha.com/1/api.js`                      | `h-captcha`    | `h-captcha-response`    |
| Turnstile    | `https://challenges.cloudflare.com/turnstile/v0/api.js` | `cf-turnstile` | `cf-turnstile-response` |

An unsolved challenge posts an empty token, and formcarry answers `403` for reCAPTCHA or `400` for the others, in `onError`; show `answer.message` and let the visitor try again. Add `localhost` to the challenge's allowed domains while you test.

The library posts the moment the visitor submits, before `grecaptcha.execute()` returns a reCAPTCHA v3 token, so a v3 post gets `403`. For v3, post with `fetch` as on the [JavaScript](/docs/frameworks/javascript) page.

## Validation errors

A `422` reaches `onError` with `errors`, one entry per failing field, each with a `message`. To show each one next to its field:

```html theme={null}
<input name="email" type="email">
<p data-error-for="email"></p>
```

```js theme={null}
onError: function (answer) {
  if (answer.code === 422) {
    for (const field in answer.errors) {
      const slot = document.querySelector(`[data-error-for="${field}"]`)
      if (slot) slot.textContent = answer.errors[field].message
    }
    return
  }
  document.querySelector("#error").textContent = answer.message
}
```

## What's next

* [Field validations](/docs/features/field-validations): the rules you can set per field.
* [Spam protection](/docs/features/spam-protection): the challenges, the filter and the honeypot.
* [Email notifications](/docs/features/email-notifications): the auto response the visitor gets, keyed on `email`.

Stuck? Write to [help@formcarry.com](mailto:help@formcarry.com). Include the form id.


## Related topics

- [Next.js](/docs/frameworks/nextjs.md)
- [JavaScript](/docs/frameworks/javascript.md)
- [jQuery](/docs/frameworks/jquery.md)
- [v0](/docs/ai/v0.md)
- [Introduction](/docs/introduction.md)
