# Custom one-page form (/docs/embeds/one-page)



One-page mode connects familiar HTML forms—contact, lead, registration, and feedback forms—to Pollaris. It submits every field in one atomic request.

## Requirements [#requirements]

The published form must be strictly linear:

* Each question flows unconditionally to the next question.
* The final question ends the form.
* There are no branches, backward edges, or quiz scoring rules.
* Each question has a unique stable field key.
* **Enable atomic one-page submissions** is turned on in **Share → Embed**.

If the form does not qualify, use the [headless step-by-step SDK](headless.md).

## Stable field keys [#stable-field-keys]

Your HTML input names must match Pollaris question keys:

```html
<input name="name">
<input name="email" type="email">
<textarea name="message"></textarea>
```

Keys match `^[a-z][a-z0-9_]{0,63}$` and are unique within the form. Changing a prompt or moving a question does not change its key. Changing a key is a breaking change for your website integration.

## Submit your HTML form [#submit-your-html-form]

```html
<form id="contact-form">
  <label>
    Name
    <input name="name" autocomplete="name">
  </label>
  <label>
    Email
    <input name="email" type="email" autocomplete="email" required>
  </label>
  <label>
    Message
    <textarea name="message" required></textarea>
  </label>
  <button type="submit">Send</button>
  <p id="contact-status" role="status"></p>
</form>

<script type="module">
  import { createPollarisClient } from 'https://cdn.example.com/embed/v1/index.js';

  const client = createPollarisClient({
    form: 'FORM_SLUG',
    apiBase: 'https://api.example.com'
  });
  const form = document.querySelector('#contact-form');
  const status = document.querySelector('#contact-status');

  form.addEventListener('submit', async (event) => {
    event.preventDefault();
    const button = form.querySelector('button[type="submit"]');
    button.disabled = true;

    try {
      const values = Object.fromEntries(new FormData(form));
      await client.submit(values, {
        context: {
          utm_source: new URLSearchParams(location.search).get('utm_source') ?? undefined,
          language: navigator.language,
          screen_w: window.innerWidth
        }
      });
      form.reset();
      status.textContent = 'Your message was sent.';
    } catch (error) {
      status.textContent = error.code === 'VALIDATION_FAILED'
        ? error.message
        : 'Your message could not be sent. Please try again.';
    } finally {
      button.disabled = false;
    }
  });
</script>
```

## Manifest and version changes [#manifest-and-version-changes]

`client.submit()` first loads `/v1/embed/FORM_SLUG/manifest`, validates keys and values locally, and submits the manifest version. Pollaris rejects stale versions so answers are never silently applied to a changed schema. The SDK manages this flow for you.

Unknown fields are rejected. Missing optional values are normalized to `null`; missing required values fail validation. The server remains authoritative even if browser validation is bypassed.

## Idempotency [#idempotency]

The SDK generates an idempotency key for each submission. To safely retry an application-managed submission, generate a key once and reuse it only for the identical values:

```js
const idempotencyKey = crypto.randomUUID();

await client.submit(values, { idempotencyKey });
```

Never reuse one key for a different submission. Pollaris rejects a reused key when the values differ.

## Field-value notes [#field-value-notes]

* `FormData` produces strings. For rating, star, or NPS fields, convert the selected value to a number.
* Multiple-choice fields expect an array, so collect repeated/checkbox values with `formData.getAll(key)`.
* A contact question expects an object containing its configured contact fields; separate text questions are often easier for a custom one-page form.
* Client-side validation is for immediate feedback only; always handle server validation errors.
