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

# Build a headless form

> Submit your own HTML or React form to Nautilus without an iframe

Render a form on your website and send its answers directly to Nautilus. You control the layout, styling, and confirmation. Nautilus saves the submission and runs the form's configured contact-list, automation, fleet-intake, notification, and attribution processing.

## Get your submission endpoint

1. Create or open a contact form in Nautilus.
2. Configure its fields, connected contact list, consent text, and notifications.
3. Publish the form.
4. Open **Share**, then **Use your own form**.
5. Copy the endpoint and generated HTML. You can also copy the optional JavaScript for an in-page confirmation.

Use the exact field IDs and endpoint from the sharing panel. Field labels such as `Email` are not submission keys unless they are also the saved field IDs.

```text theme={null}
POST https://app.nautilus.co/api/forms/FORM_ID/submissions
```

Publishing enables submissions. Unpublishing stops accepting them. The existing hosted link and iframe embed remain available.

## Use native HTML

Start with the HTML from **Share** so you keep the correct fields, required values, and consent text. A minimal example looks like this:

```html theme={null}
<form action="https://app.nautilus.co/api/forms/FORM_ID/submissions" method="POST">
  <label for="email">Email</label>
  <input id="email" type="email" name="EMAIL_FIELD_ID" required>

  <div hidden>
    <label for="bot-trap">Leave this field empty</label>
    <input id="bot-trap" type="text" name="_gotcha" tabindex="-1" autocomplete="off">
  </div>

  <!-- Keep the consent text and policy links from the generated sharing code. -->
  <button type="submit">Submit</button>
</form>
```

Replace `FORM_ID` and `EMAIL_FIELD_ID` with your saved IDs. Include every required field from your form. The browser displays the configured success title and message after an accepted submission.

### Return to your website

To send visitors to a thank-you page, add this inside the form:

```html theme={null}
<input type="hidden" name="_redirect" value="https://your-website.example/thank-you">
```

The destination must match the submitting website's origin: the same scheme, host, and port. A native browser submission returns a `303` redirect. Missing `Origin` headers and destinations on a different origin are rejected before saving.

For example, a form on `https://www.example.com/contact` can redirect to `https://www.example.com/thank-you`. It cannot redirect to `https://example.com/thank-you` or `https://another.example/thank-you`.

## Submit with JavaScript or React

The optional sharing script submits the generated HTML with `fetch`, prevents concurrent submissions, captures campaign parameters, and displays errors or a confirmation in your page.

For a custom frontend, send a flat JSON object. Put each answer under its saved field ID; do not wrap answers in a `data` property.

```javascript theme={null}
const response = await fetch(
  "https://app.nautilus.co/api/forms/FORM_ID/submissions",
  {
    method: "POST",
    credentials: "omit",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
    },
    body: JSON.stringify({
      EMAIL_FIELD_ID: "customer@example.com",
      PHONE_FIELD_ID: "+1 (202) 555-0123",
      _gotcha: "",
    }),
  }
);
const result = await response.json();

if (!response.ok || !result.ok) {
  // Display result.error and result.fieldErrors beside the matching inputs.
} else if (result.redirectUrl) {
  window.location.assign(result.redirectUrl);
} else {
  // Display result.title and result.message in your page.
}
```

This example assumes your saved form has both email and phone fields. Remove or replace keys to match your form. In your own submit handler, prevent concurrent clicks and handle network failures separately from validation errors.

You can send `new FormData(form)` instead of JSON. Set `Accept: application/json`, omit the `Content-Type` header, and let the browser supply the multipart boundary. Only text values are supported.

JSON bodies always receive JSON. Other request formats receive JSON unless `Accept` requests HTML without also requesting JSON. With a JSON response, `_redirect` becomes `redirectUrl`; your frontend decides when to navigate.

## Authentication and signup flows

Public submissions do not require an API key or a staff session. The published form ID permits submitting answers. It does not grant access to saved submissions or customer data.

Password-protected forms also require `_password` in the request body. The sharing panel generates an empty password input; it does not embed the saved password.

| Action | Endpoint | Authentication |
| - | - | - |
| Submit answers | `POST /api/forms/{formId}/submissions` | Public form ID; form password when configured |
| Read form configuration | `GET /api/v1/forms/{formId}` | Tenant API key |
| Read saved submissions | `GET /api/v1/forms/{formId}/submissions` | Tenant API key |

Keep tenant API keys on your server. The public submission route supports cross-origin requests and `OPTIONS` preflights without cookies or an `Authorization` header.

You can place a headless form alongside other steps in your own frontend. A submission does not sign the visitor in, create an authentication session, or verify their email or phone. Use the hosted flow for forms that require verification or POS enrollment; this endpoint does not provide separate verification steps that you can combine into a signup flow.

## Map your answers

Nautilus validates answers against the saved form. Unknown field IDs and nested answer objects are rejected.

| Field | Submitted value |
| - | - |
| Text, names, textarea, license | String |
| Email | Valid email string; trimmed and lowercased |
| Phone | Phone string; normalized using the same rules as hosted forms |
| Number | Finite number or numeric string |
| Date | String in `YYYY-MM-DD` format |
| Dropdown or single choice | One saved option value |
| Multiple checkboxes | String array in JSON, or repeated field names in HTML |
| Standalone checkbox | JSON boolean or native `on` / `true`; an omitted optional checkbox becomes `false` |
| Hidden | String; the saved default applies when omitted |

Phone normalization removes non-digits and keeps the last ten digits when the number is longer. For example, `+1 (202) 555-0123` is saved as `2025550123` in answers and downstream form data.

For a required checkbox group, select at least one option. Do not put native `required` on every checkbox in the group: the browser would require every option. The generated sharing HTML leaves this group check to the server.

Conditional visibility is evaluated on the server. Answers for fields hidden by those conditions are discarded. Your frontend must implement the corresponding show/hide behavior. Hidden input values are still client-controlled; do not use them as proof of identity or authorization.

## Preserve attribution

Add tracking keys alongside your answers in the same flat request body:

`_utm_source`, `_utm_medium`, `_utm_campaign`, `_utm_term`, `_utm_content`, `_gclid`, `_gbraid`, `_wbraid`, `_fbclid`, `_referrer`, and `_landing_url`.

They populate the existing attribution columns. They do not become form answers, and headless intake does not read Nautilus attribution cookies.

Tracking strings are trimmed to 450 characters when stored. Empty or malformed optional values are ignored individually without rejecting valid answers. The complete request must still fit within the 64 KiB body limit.

For example, add the current page and referrer to your request:

```javascript theme={null}
const tracking = {
  _landing_url: window.location.href,
  _referrer: document.referrer,
};
```

Spread these keys into your JSON body, or set them on your `FormData` before submitting.

## Spam protection and limits

* Every request must include `_gotcha` as a string. Keep this hidden bot-trap field empty for real visitors.
* A filled trap receives an ordinary acknowledgment without a `submissionId`. It creates no submission, contact, notification, or automation trigger.
* The complete request body is limited to 64 KiB (65,536 bytes), including multipart overhead.
* File bodies and nested answer objects are not accepted.
* The form's configured per-contact submission limit still applies.

Headless intake uses basic honeypot protection. It does not add rate limiting or CAPTCHA.

## Handle confirmation and retries

A saved submission returns a receipt such as:

```json theme={null}
{
  "ok": true,
  "submissionId": "SUBMISSION_ID",
  "title": "Thank you!",
  "message": "Your form has been submitted successfully."
}
```

The title and message come from the form's settings. The receipt does not return saved answers, contact records, or notification recipients.

A `submissionId` confirms that the submission was saved. Contacts, automation startup, fleet intake, and notifications use the existing processing behavior; the receipt does not guarantee that every downstream action completed or that an email reached an inbox.

Notifications are scheduled after confirmation, so a slow email provider does not hold up your receipt. Network failures can still leave the result unknown. Do not automatically retry: this endpoint has no request idempotency key. Check the form's saved submissions before repeating an uncertain request.

## Forms that need the hosted flow

The endpoint returns `409 hosted_form_required` for forms requiring email verification, POS enrollment, file uploads, or an unsupported saved field definition. Use the hosted link when **Share** indicates that headless submission is unavailable.

Null optional field settings are treated as omitted. For example, `placeholder: null` does not make a form unsupported. Malformed field definitions and comparison rules are still rejected.

The headless receipt does not reproduce hosted completion features such as voucher display, survey-incentive completion, or Meta/Dub browser conversion events. Use the hosted flow when you need those features, or implement the corresponding frontend integration where one is available.

## Handle errors

JSON errors have `ok: false`, a `code`, an `error` message, and optional `fieldErrors` keyed by saved field ID. Native HTML submissions display an error page.

| Status | Meaning |
| - | - |
| `400` | Malformed or missing body |
| `401` | Missing or incorrect form password |
| `404` | Missing or unpublished form |
| `409` | Form requires a hosted flow |
| `413` | Request body exceeds 64 KiB |
| `415` | Unsupported content type or a file body |
| `422` | Invalid answers, missing honeypot, invalid controls, or invalid redirect |
| `500` | Submission processing failed |

See [Submit a form](/api-reference/endpoint/submit-form) for request formats and response examples, or the [API introduction](/api-reference/introduction) for authenticated read access.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.