# Headless step-by-step SDK (/docs/embeds/headless)



Use the headless SDK when you want complete control over markup and CSS. It exposes state and methods but renders nothing. It supports both linear and branching published forms.

## Create a client [#create-a-client]

```js
import { createPollarisClient } from 'https://cdn.example.com/embed/v1/index.js';

const client = createPollarisClient({
  form: 'FORM_SLUG',
  apiBase: 'https://api.example.com',
  redirect: 'manual'
});

const unsubscribe = client.subscribe((state) => {
  renderYourInterface(state);
});

await client.start({
  utm_source: 'website',
  utm_medium: 'embed',
  language: navigator.language,
  screen_w: window.innerWidth
});
```

## Render and answer [#render-and-answer]

When `state.status === 'ready'`, render `state.question` using your own components. Keep the value locally or pass it to `client.setValue(value)`, then submit:

```js
try {
  await client.answer(value);
} catch (error) {
  // The same error is also available as client.getState().error.
  showError(error.message);
}
```

Pollaris returns only the current question. After an answer, the server validates it and resolves the next graph edge. Do not attempt to calculate branching in the browser.

## State [#state]

`client.getState()` and subscription callbacks return:

| Field        | Meaning                                                                    |
| ------------ | -------------------------------------------------------------------------- |
| `status`     | `idle`, `loading`, `ready`, `submitting`, `complete`, `closed`, or `error` |
| `form`       | Public title, description, branding, and close/duplicate settings          |
| `responseId` | Current public response session ID                                         |
| `question`   | Current question, or `null`                                                |
| `value`      | Value most recently passed to `setValue`                                   |
| `progress`   | Approximate value from `0` to `1`                                          |
| `completion` | Completion message, redirect, and optional quiz score                      |
| `error`      | A `PollarisError`, or `null`                                               |

Supported question types are `yesno`, `single`, `multi`, `rating`, `nps`, `text`, `star`, `contact`, `date`, and `time`. Treat `question.config` as type-specific configuration.

## Lifecycle events [#lifecycle-events]

```js
const offReady = client.on('ready', ({ form }) => {});
const offQuestion = client.on('question', ({ key, progress }) => {});
const offComplete = client.on('complete', ({ responseId }) => {});
const offError = client.on('error', ({ code, retryable }) => {});
```

These events intentionally omit answer values. Avoid copying answer data into analytics, console logs, or error-reporting metadata.

## Errors and retry [#errors-and-retry]

Stable public codes include:

```text
FORM_NOT_FOUND        FORM_CLOSED          ORIGIN_NOT_ALLOWED
VALIDATION_FAILED     RATE_LIMITED         SESSION_EXPIRED
NETWORK_ERROR         SERVER_ERROR         PROTOCOL_ERROR
```

Check `error.retryable`. `retry()` is available only when the client has a retryable operation. A validation error should be shown beside the relevant control instead of retried unchanged.

## Resume, reset, and cleanup [#resume-reset-and-cleanup]

The default client storage remembers an in-progress response and duplicate-response status. This is a user-experience feature, not an anti-abuse security boundary.

```js
await client.reset();  // clear this form's saved local state
unsubscribe();
client.destroy();      // abort requests and remove listeners
```

Use `storage: null` when persistence is undesirable, or provide a `StorageLike` object with `getItem`, `setItem`, and `removeItem`.
