# Your own UI

Send answers from a form you draw yourself, with question and option ids from the builder or the API.

You can skip the form OneStar draws and send answers from your own UI: a thumbs up under an article, a rating in your app's own style, a poll built into a settings screen. `onestar.submit` sends the answers to a published form, and they are counted in Results like any other.

Answers sent this way are checked against the form's live version exactly like answers from the widget, and the form's allowed sites still apply.

## A first answer

```html index.html
<script src="https://embed.onestar.sh/v1/loader.js" async></script>
<button id="loved-it" type="button">Loved it</button>
<script>
  document.getElementById("loved-it").addEventListener("click", () => {
    if (!window.onestar || typeof window.onestar.submit !== "function") return;
    window.onestar
      .submit("f_hemsz66yupbd76d2", { b3f9a12c: 5 })
      .then((receipt) => console.log(receipt.thanks.title))
      .catch((err) => console.warn(err.code, err.message, err.fields));
  });
</script>
```

The guard skips a click that comes before the loader has run. When your code must wait instead, wait for the loader as shown on the [JavaScript API](https://onestar.sh/docs/feedback/javascript-api.md) page. `submit` loads the form itself, so no `<onestar-feedback>` element is needed.

The form's **Install** tab has a **Your own UI** option that writes this call for your form, with its real question ids and an example answer for each question. It reads the draft, so publish first if you added or removed questions since the last version.

## Question and option ids

`answers` is keyed by question id. Each id is `b` followed by 8 hex characters, such as `b3f9a12c`, and stays the same when you change the question's words.

- In the form builder's **Build** tab, each question shows its **Question id** with a copy button, and each option of a choice or ranking shows its **Option id**.
- On Max, `GET /v1/owner/forms/{id}` returns the live version's questions in order, each with its id, type and options. See the [REST API](https://onestar.sh/docs/feedback/rest-api.md).

Option ids stay the same when you rename an option, so code that sends them keeps working. The **Other** option of a choice question has the id `other`.

## Answer shapes

Each question takes one shape of answer:

| Question | Send | Example |
|---|---|---|
| Rating, stars | A whole number from 1 to 5 | `4` |
| Rating, thumbs | `1` for down or `5` for up | `5` |
| Rating, faces | A whole number from 1 to 3, or 1 to 5, as the question has | `3` |
| Rating, scale | A whole number from 1 to the scale's points | `7` |
| Rating, slider | A whole number from 0 to 100 | `80` |
| NPS | A whole number from 0 to 10 | `9` |
| Choice, one answer | One option id | `"maps"` |
| Choice, several answers | A list of option ids, up to the question's maximum | `["maps", "search"]` |
| Choice with Other | The ids with `other`, and the person's words | `{ "choices": ["other"], "other_text": "Offline mode" }` |
| Text | A string | `"The export is slow"` |
| Ranking | Every option id, most important first | `["speed", "price", "support"]` |

- Leave out a question the person skipped. A `null` counts as left out.
- A ranking must list every option once. A partial ranking is refused.
- A written answer longer than the question's limit is cut to it. One much longer is refused. Other text is kept up to 500 characters.
- A question with a condition ("show only if") takes an answer only when the condition is met by the other answers you send. An answer to a question the person would not have seen is refused.

## Options

```ts
onestar.submit(form: string, answers: Record<string, unknown>, opts?: SubmitOptions): Promise<Receipt>

interface SubmitOptions {
  contact?: string;
  display_name?: string;
  updates_opt_in?: boolean;
  final?: boolean;
  user_token?: string;
  api?: string;
}
```

- `contact`: the person's email, or the identifier the form asks for (such as a Player ID). Kept only when the form asks for a contact, and required when the form is set to **Anyone who leaves a contact**. Sealed and shown only to you.
- `display_name`: the name to show beside a quotable answer, when the form asks for one.
- `updates_opt_in`: true when the person ticked that they may be emailed about updates. Kept only with an email, on a form that offers it.
- `final`: whether the person finished. Default `true`.
- `user_token`: a signed token for this call only. `onestar.identify(token)` sets it for every call; see [Signed in users](https://onestar.sh/docs/feedback/identify-users.md).
- `api`: only for running OneStar locally.

## The receipt

```ts
interface Receipt {
  id: string;
  state: "received";
  test: boolean;
  thanks: { title: string; body: string | null };
  write: string;
}
```

`thanks` holds the thank you you set on the form, to show in your UI. `test` is true when the answer was kept as a test (sent from `localhost`, or with the form in test mode), so it will not count. `id` names the answer; `write` is used by the widget itself and you can ignore it.

After each answer is saved, `submit` fires `onestar:submit` on `window` with `{ form, final }`. When a finished answer goes to a form that allows one answer per person, it also notes in the browser that this person answered, so an element for the same form shows its thank you instead of asking again. On such a form, any later answer from the same browser is refused with `already_answered`, whether the earlier one was finished or not.

## Partial answers

Each call to `submit` sends one new answer. `final: false` stores it as started but not finished: Results counts it under **Started** and not **Finished**, and required questions are not enforced.

A later `submit` never adds to an earlier answer. On a form that allows one answer per person (the default), a second `submit` from the same browser is refused with `already_answered`, even when the first was sent with `final: false`. Only a form that allows several answers per person stores a second one.

So send everything in one call when the person is done. Use `final: false` only for an answer you know will not be continued, such as a one tap reaction where the follow up question was dismissed. The widget's own step by step saving is not available from `submit`.

> Test answers from `localhost` do not count toward the one answer per person check, so a second answer that works on your machine can be refused on your live site. Check this flow on the live site before you rely on it.

## Errors

`submit` rejects with an object in plain words:

```ts
interface WidgetError {
  code: string;
  message: string;
  fields: Record<string, string>;
}
```

`fields` holds a message for each question id that needs another look, and for `contact` or `display_name` when those were the problem:

```json error
{
  "code": "invalid_answer",
  "message": "Some answers need another look.",
  "fields": { "b3f9a12c": "Pick one of the answers.", "contact": "Check your email address." }
}
```

| Code | Meaning |
|---|---|
| `invalid_answer` | An answer, the contact or the display name does not fit; see `fields`. With no answers at all, the message is "Answer at least one question." |
| `already_answered` | This browser or signed in user already answered a form that allows one answer per person, finished or not. |
| `closed` | The form is closed, paused, not published, or the id is unknown. `message` is the form's closed message. |
| `origin_refused` | The page's site is not in the form's allowed sites. |
| `identity_required` | The form is for signed in users and no valid token was sent. |
| `version_changed` | A new version was published between loading the form and sending. Send again. |
| `stale_form`, `too_fast` | Rare here, since `submit` loads the form itself and waits a moment before sending. Send again. |
| `invalid_form` | The form id is not shaped like one. |
| `invalid_request` | The request could not be read, for example an answers object with more than 20 questions. |
| `network`, `server`, `rate_limited` | The send failed. It is safe to call again with the same answers. |
| `load_failed` | The widget code could not load, so nothing was sent. |

## Where these answers show

Answers from your own UI arrive with the page's path and context like any other ([Context and placements](https://onestar.sh/docs/feedback/context-and-placements.md)), and show in Results under their site. They have no loads, so they stay out of the "Seen to answered" funnel. To show replies to them in your UI, use `onestar.threads()` and `onestar.reply()`.
