# Submit endpoint

Everything the public endpoint reads and returns. Field names are compatible with Web3Forms, so a form moved over keeps working.

## Endpoint

| Property | Value |
| -------- | ----- |
| URL | `https://formwire.io/submit` |
| Key in the path | `https://formwire.io/submit/YOUR-ACCESS-KEY`, for clients that can't add a hidden field |
| Method | `POST`. `OPTIONS` is answered for CORS preflight. |
| Authentication | None. The access key identifies the form and can only create submissions for it. |

## Request body

Send the body as any of these; the field names are the same in each.

| Content type                        | Notes                                                                                                          |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `multipart/form-data`               | What a browser sends for a form with files. The only way to upload files.                                      |
| `application/x-www-form-urlencoded` | A browser's default for a plain form.                                                                          |
| `application/json`                  | Must be an object. Arrays are kept as lists, nested objects are stored as JSON text, null values are dropped. |

A field name ending in `[]`, such as a checkbox group named `topics[]`, is stored as a list under `topics`. A name repeated without the brackets is stored as a list too.

```http title="A JSON request"
POST /submit HTTP/1.1
Host: formwire.io
Content-Type: application/json
Accept: application/json

{
  "access_key": "YOUR-ACCESS-KEY",
  "email": "ada@example.com",
  "message": "Hello",
  "topics": ["pricing", "support"]
}
```

## Option fields

These fields change what Formwire does with a submission and are not stored with it. Everything else you send is kept as the submission's data, shown in the email and the dashboard under its field name.

| Field | What it does | Notes |
| ----- | ------------ | ----- |
| `access_key` | Your form's access key. Required unless it's in the URL path. | A UUID from the form's Settings, General. Example: `a3f1c2…-…`. |
| `subject` | The notification email's subject. | Up to 200 characters. `{field:name}` placeholders are filled in. Example: `New enquiry from {field:name}`. |
| `from_name` | The sender name shown on the notification email. | Up to 80 characters. Example: `Website contact form`. |
| `replyto` | Where replies go instead of the sender's email. | An email address, up to 254 characters. Example: `sales@example.com`. |
| `redirect` | The page people see after a plain HTML form submits. | http or https on one of the form's allowed domains, up to 2048 characters. Example: `https://example.com/thanks`. |
| `fax` | The honeypot. Leave it empty; anything in it marks the submission as spam. | The default name. Rename it in the form's spam settings. |
| `botcheck` | The Web3Forms honeypot, still honoured. | Spam unless empty, "false" or "0". |
| `h-captcha-response` | The hCaptcha token when the form has hCaptcha switched on. | Set by the captcha widget. Example: `P1_eyJ…`. |
| `g-recaptcha-response`, `cf-turnstile-response` | The reCAPTCHA or Turnstile token when the form uses one. | Set by the captcha widget, verified and dropped. |
| `ccemail`, `webhook`, `recaptcha_response` | Web3Forms options. Set CC emails, webhooks and reCAPTCHA in the form's settings instead. | Dropped, so a moved form doesn't show them as data. |

## Recognised fields

These are stored like any other field, and also read for the sender's name, email and phone. Names are matched without case, spaces, dashes or underscores, so `First Name`, `firstName` and `first_name` all work.

| Field | Also accepted | What it does |
| ----- | ------------- | ------------ |
| `name` | full_name, your_name, contact_name | The sender's name, shown in the From column. |
| `first_name + last_name` | fname, given_name, forename; lname, surname, family_name | Joined into the sender's name when there's no name field. |
| `email` | email_address, your_email, mail, contact_email | The sender's email. Replies and the confirmation email go to it. Up to 254 characters. |
| `phone` | phone_number, tel, telephone, mobile, mobile_number, cell, cell_phone, contact_number | The sender's phone number. |
| `topics[]` | any name ending in [] | A checkbox group, stored as one list under topics. |
| `type="file"` | | Uploaded files (multipart only), linked from the notification email and listed under the field's name. |

## Responses

The endpoint answers in JSON when the request says it wants JSON: an `Accept` header containing `application/json`, or a JSON body. Otherwise it answers a plain form post with a redirect.

### JSON

A stored submission returns `200` with the new submission's id and the fields as stored. A spam submission caught by the honeypot gets the same response, so a bot learns nothing.

```http title="Success"
HTTP/1.1 200 OK
Content-Type: application/json

{
  "success": true,
  "message": "Submission received.",
  "data": {
    "submission_id": "7c1e9b2a-4f3d-4e8a-9b1c-2d5f6a7b8c9d",
    "email": "ada@example.com",
    "message": "Hello",
    "topics": ["pricing", "support"]
  }
}
```

```http title="Error"
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "success": false,
  "message": "The access key is missing or invalid.",
  "error": {
    "code": "invalid_access_key",
    "message": "The access key is missing or invalid.",
    "details": null
  }
}
```

### Redirect

A plain form post gets a `303` to the `redirect` field if it's present, otherwise to the redirect URL in the form's settings, otherwise to a hosted thank-you page at `/submit/success`. Only http and https URLs are followed. On an error the browser is sent to `/submit/error?code=…`, a hosted page that explains the code; nothing else from the request appears on it.

```http
HTTP/1.1 303 See Other
Location: https://example.com/thanks
```

## Errors

Every error carries `success: false`, a readable `message` and an `error.code` to branch on.

| Status | Code | When |
| ------ | ---- | ---- |
| 400 | `invalid_request` | The body couldn't be read, or a JSON body isn't an object. |
| 400 | `invalid_access_key` | The access key is missing or isn't a valid key. |
| 403 | `form_paused` | The form is paused in the dashboard. |
| 403 | `form_suspended` | The form was suspended by Formwire. |
| 403 | `domain_not_allowed` | The page posting the form isn't in the form's allowed domains. |
| 403 | `file_uploads_unavailable` | The form doesn't accept files. |
| 403 | `storage_limit_reached` | The organization's file storage is full. |
| 404 | `not_found` | The path isn't /submit. |
| 413 | `payload_too_large` | The body is over the size cap, or the stored fields are over 64 KB. |
| 415 | `file_type_not_allowed` | A file's type isn't accepted. |
| 429 | `rate_limited` | Too many posts from this IP or to this key, or the form's abuse ceiling was reached. |
| 500 | `internal_error` | Something failed on Formwire's side. Retry with the same Idempotency-Key. |

## Retries and idempotency

Send an `Idempotency-Key` header to make a retry safe. A second post to the same form with the same key returns the submission stored the first time instead of creating another.

```bash
curl https://formwire.io/submit \
  -H "Accept: application/json" \
  -H "Idempotency-Key: order-8831-contact" \
  -F access_key=YOUR-ACCESS-KEY \
  -F email=ada@example.com
```

## Rate limits and size caps

- 30 posts a minute from one IP address, and 120 a minute to one access key. Over either, the answer is `429 rate_limited`.
- Your plan's submission allowance is a soft quota: you're told when you pass it and delivery continues. Only unusual volume far past it is stopped.
- A body of up to 128 KB, plus files on a multipart post: up to 10 files, 10 MB each and 25 MB together, on forms that accept uploads.
- Up to 200 fields with names of up to 100 characters; the stored fields together fit in 64 KB.
- The page URL (from `Referer`) is kept to 2048 characters and the user agent to 512.

## CORS

The endpoint allows any origin, so you can call it with `fetch` from a browser on any site. Preflight permits `POST` with the `Content-Type`, `Accept` and `Idempotency-Key` headers and is cached for a day. To limit which sites can post to a form, use [allowed domains](/docs/spam#allowed-domains): the check reads the request's `Origin` (or `Referer`) and refuses the rest with `403 domain_not_allowed`.

## What's stored with a submission

Besides the fields: the page URL the post came from, the sender's IP address and user agent, the time, and the delivery options (`from_name`, `replyto`). The honeypot field and the option fields above are never stored.