> ## 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.

# jQuery

> jQuery sends a form to formcarry with one $.ajax call from the submit handler.

It builds a `FormData` from the form, posts it and shows formcarry's answer, and the same request carries files, a spam blocker token and validation errors.

## 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 request

To send a form with jQuery, build a `FormData` from the form element, post it with `$.ajax`, and send `Accept: application/json` so the answer comes back as JSON:

```html theme={null}
<form class="formcarryForm" action="https://formcarry.com/s/AbC123xyz" method="POST">
  <input type="text" name="name">
  <input type="email" name="email">
  <textarea name="message"></textarea>
  <button type="submit">Send</button>
</form>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script>
  $(function () {
    $(".formcarryForm").submit(function (e) {
      e.preventDefault();

      $.ajax({
        type: "POST",
        url: $(this).attr("action"),
        data: new FormData(this),
        dataType: "json",
        processData: false,
        contentType: false,
        headers: { Accept: "application/json" },
        success: function (response) {
          alert(response.message); // We received your submission
        },
        error: function (xhr) {
          alert(xhr.responseJSON.message);
        }
      });
    });
  });
</script>
```

Every field needs a `name`, because `FormData` reads the fields from the elements and formcarry stores each value under its name. The `email` field is the visitor's address, so it becomes the reply to address of your notification and the recipient of the auto response.

Set `processData: false` and `contentType: false` rather than leaving the defaults, otherwise jQuery tries to serialise the `FormData` itself and the request fails before it is sent.

A stored submission answers with `code: 200`. jQuery routes every status outside `2xx` to `error`, so a refused submission's answer is in `xhr.responseJSON`, with the reason in `message`, see [What every form needs](/docs/what-every-form-needs).

Without a file input, `$(this).serialize()` posts the same fields as `application/x-www-form-urlencoded`, with no `processData` or `contentType` to set:

```js theme={null}
$.ajax({
  type: "POST",
  url: $(this).attr("action"),
  data: $(this).serialize(),
  dataType: "json",
  headers: { Accept: "application/json" },
  // success and error as above
});
```

`serialize()` leaves file inputs out, so a form that uploads keeps the `FormData` request.

## JSON

To send JSON instead, collect the fields into an object, `JSON.stringify` it, and set `contentType: "application/json"`:

```js theme={null}
var fields = Object.fromEntries(new FormData(this));

$.ajax({
  type: "POST",
  url: $(this).attr("action"),
  data: JSON.stringify(fields),
  contentType: "application/json",
  dataType: "json",
  headers: { Accept: "application/json" },
  // success and error as above
});
```

The keys are the field names formcarry stores.

## Files

A `FormData` built from the form carries its file inputs, so the first request uploads them as is. To take one file in one field and several in another, add the inputs to the form; the same `$.ajax` options send them:

```html theme={null}
<input type="file" name="attachment">
<input type="file" name="attachments" multiple>
```

Keep `contentType: false` rather than setting `multipart/form-data` yourself, otherwise the multipart boundary is missing and the upload fails.

For `File` objects from elsewhere, such as a Dropzone with its own upload turned off, append each one under its own name:

```html theme={null}
<script src="https://unpkg.com/dropzone@5/dist/min/dropzone.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/dropzone@5/dist/min/dropzone.min.css">

<!-- inside the form -->
<div id="attachments" class="dropzone"></div>

<script>
  Dropzone.autoDiscover = false;

  $(function () {
    var dropzone = new Dropzone("#attachments", {
      url: "/dummy",
      autoProcessQueue: false,
      uploadMultiple: true,
      parallelUploads: 10,
      maxFiles: 10,
      addRemoveLinks: true
    });

    $(".formcarryForm").submit(function (e) {
      e.preventDefault();

      var formData = new FormData(this);
      var files = dropzone.getAcceptedFiles();
      for (var i = 0; i < files.length; i++) {
        formData.append("file-" + i, files[i]);
      }

      $.ajax({
        type: "POST",
        url: $(this).attr("action"),
        data: formData,
        dataType: "json",
        processData: false,
        contentType: false,
        headers: { Accept: "application/json" },
        // success and error as above
      });
    });
  });
</script>
```

`autoProcessQueue: false` keeps Dropzone from posting to `url` itself, and `getAcceptedFiles()` returns what the visitor dropped.

For a small file inside the JSON request, read it as a data URL and give it its own key, never an array:

```js theme={null}
var form = this;
var fields = Object.fromEntries(new FormData(form));
var reader = new FileReader();

reader.onload = function () {
  fields.attachment = reader.result;
  $.ajax({
    type: "POST",
    url: $(form).attr("action"),
    data: JSON.stringify(fields),
    contentType: "application/json",
    dataType: "json",
    headers: { Accept: "application/json" },
    // success and error as above
  });
};
reader.readAsDataURL(form.elements.attachment.files[0]);
```

Use data URLs for small files. Using them with larger files bloats the request by about a third and reaches the 50 MB limit sooner.

<Note>
  Files are stored on paid plans only. Free plans store the rest of the submission without them.
</Note>

## Validation errors

A `422` carries `errors`, one entry per failing field, each with a `message`. To show each one next to its field, and every other refusal in an alert:

```js theme={null}
error: function (xhr) {
  var answer = xhr.responseJSON;
  if (answer.code === 422) {
    $.each(answer.errors, function (name, error) {
      var field = $('[name="' + name + '"]');
      field.addClass("fc-field-error");
      $("<span></span>").addClass("fc-field-error-message").text(error.message).insertAfter(field);
    });
  } else {
    alert(answer.message); // 403, 429 and the rest
  }
}
```

Read the `422` in `error` rather than in `success`, otherwise the handler never runs.

To clear the marks before the next attempt, remove them at the top of the submit handler:

```js theme={null}
$(".fc-field-error-message").remove();
$(".fc-field-error").removeClass("fc-field-error");
```

Clear them on every submit rather than only on success, otherwise old messages stay next to fields that now pass.

## Spam blocker

To add a challenge, load the vendor's script, put a `div` with the widget class and `data-sitekey` inside the form, and paste the secret key into the form's settings under Form Security:

| Challenge    | Script                                                    | Widget class   | Token field             |
| ------------ | --------------------------------------------------------- | -------------- | ----------------------- |
| reCAPTCHA v2 | `https://www.google.com/recaptcha/api.js`                 | `g-recaptcha`  | `g-recaptcha-response`  |
| reCAPTCHA v3 | `https://www.google.com/recaptcha/api.js?render=SITE_KEY` | none           | `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` |

With reCAPTCHA v2:

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

<!-- inside the form, before the submit button -->
<div class="g-recaptcha" data-sitekey="SITE_KEY"></div>
```

The widget adds a field named `g-recaptcha-response` to the form, so the `FormData` request carries the token as is, and `Object.fromEntries` puts it into the JSON request too. With reCAPTCHA v3 there is no widget: call `grecaptcha.execute("SITE_KEY", { action: "submit" })` when the visitor submits and put what it returns into a hidden input named `g-recaptcha-response`.

To hold the request until the visitor has passed the challenge, check the token at the top of the submit handler:

```js theme={null}
if (!$(this).find('[name="g-recaptcha-response"]').val()) {
  alert("Complete the challenge first");
  return;
}
```

Refuse to send while the token is empty rather than posting anyway, otherwise formcarry answers `403` for reCAPTCHA or `400` for the others. Add `localhost` to the challenge's allowed domains while you test.

## The submit button

To stop a second submit while the request runs, disable the button before `$.ajax` and enable it again in `complete`:

```js theme={null}
var button = $(this).find('button[type="submit"]').first();
button.prop("disabled", true);

$.ajax({
  // the options above
  complete: function () {
    button.prop("disabled", false);
  }
});
```

`complete` runs after `success` and `error` alike, so the button comes back after a refused submission too. Disable the button rather than trusting one click, otherwise the second click gets a `429`: 1 submission per 15 seconds per form per IP address.

To show that the request is running, swap the button text for a spinner and put the text back in `complete`:

```js theme={null}
var button = $(this).find('button[type="submit"]').first();
var buttonText = button.text();
var loader = $('<svg fill="#fff" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_ajPY{transform-origin:center;animation:spinner_AtaB .75s infinite linear}@keyframes spinner_AtaB{100%{transform:rotate(360deg)}}</style><path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25"/><path d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" class="spinner_ajPY"/></svg>');

button.html(loader);
button.prop("disabled", true);

$.ajax({
  // the options above
  complete: function () {
    button.html(buttonText);
    button.prop("disabled", false);
  }
});
```

Put the text back in `complete` rather than in `success`, otherwise a refused submission leaves the spinner in the button.

## Reset

To clear the fields after a stored submission, reset the form in `success`:

```js theme={null}
var form = $(this);

$.ajax({
  // the options above
  success: function (response) {
    if (response.code === 200) {
      alert(response.message);
      form[0].reset();
    }
  }
});
```

Reset in `success` rather than in `complete`, otherwise a refused submission clears what the visitor typed.

## 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

- [Introduction](/docs/introduction.md)
- [Legacy Submissions API — List and Filter Submissions](/docs/api/legacy/submissions.md)
- [Formcarry API Overview](/docs/api/overview.md)
- [Search submissions across forms](/docs/api-reference/submissions/search-submissions-across-forms.md)
- [Count submissions](/docs/api-reference/submissions/count-submissions.md)
