# JavaScript API

window.onestar: open, close, identify, set, submit, threads and reply, with the queue that works before the loader.

The loader adds `window.onestar`, a small API for opening forms from code, telling OneStar who is answering and where, sending answers from your own UI, and reading replies. Calls made before the loader has run wait in a queue.

## The queue

The loader is `async`, so your code may run before it. Create a queue and push calls onto it; the loader replays them in order when it runs, and from then on `onestar.q.push` runs a call at once:

```html index.html
<script>
  window.onestar = window.onestar || { q: [] };
  onestar.q.push(["identify", "<your signed token>"]);
  onestar.q.push(["set", { app_version: "2.4.1", platform: "web" }]);
</script>
<script src="https://embed.onestar.sh/v1/loader.js" async></script>
```

Each call is an array: the method's name, then its arguments. A name the API does not have is ignored.

The queue suits calls whose result you do not need: `identify`, `set`, `open` and `close`. A queued `submit`, `threads` or `reply` still runs, but its promise is dropped, so you cannot tell whether it worked. Call those directly once the loader has run:

```js onestar-ready.js
function onestarReady() {
  return new Promise((resolve, reject) => {
    if (window.onestar && typeof window.onestar.submit === "function") return resolve(window.onestar);
    const s = document.querySelector('script[src="https://embed.onestar.sh/v1/loader.js"]');
    if (!s) return reject(new Error("Add the OneStar loader to the page first."));
    s.addEventListener("load", () => resolve(window.onestar), { once: true });
    s.addEventListener("error", () => reject(new Error("The OneStar loader did not load.")), { once: true });
  });
}

onestarReady().then((onestar) => onestar.threads()).then((list) => console.log(list.unread));
```

The **Your own UI** example on the form's **Install** tab queues its `submit` so it works pasted anywhere; use the pattern above when you need the result.

## Methods

```ts
interface OneStarApi {
  open(form?: string): void;
  close(): void;
  identify(token: string | null): void;
  set(context: Record<string, unknown>): void;
  submit(form: string, answers: Record<string, unknown>, opts?: SubmitOptions): Promise<Receipt>;
  threads(form?: string, opts?: ThreadsOptions): Promise<ThreadList>;
  reply(threadId: string, text: string, opts?: { form?: string; api?: string; user_token?: string }): Promise<ThreadMessage>;
  q: { push(call: ArrayLike<unknown>): void };
}
```

### open

```ts
onestar.open(form?: string): void
```

Opens a form for the person and moves focus into it. With no argument it opens the first popover, slide-in or modal on the page (or focuses the first inline form). With a form id it opens that form's element; when the page has no element for that form, it adds a modal for it and opens that. See [Modes and triggers](https://onestar.sh/docs/feedback/modes-and-triggers.md).

### close

```ts
onestar.close(): void
```

Closes every open overlay on the page. Inline forms are not affected.

### identify

```ts
onestar.identify(token: string | null): void
```

Sets the signed token for the person who is signed in to your product, for every form and inbox on the page. Signed tokens work on Pro and Max. The forms draw again with it, keeping any answers in progress. `null` clears it, for example on sign out. A `user-token` attribute on an element wins over `identify` for that element. The token comes from your server; see [Signed in users](https://onestar.sh/docs/feedback/identify-users.md).

### set

```ts
onestar.set(context: Record<string, unknown>): void
```

Adds what the page knows about the person's situation to every answer started after it (later steps of an answer keep the context it started with): `app_version`, `platform`, `locale`, `plan` and `cohort`. Other keys are dropped. Values are strings or numbers, trimmed and cut to 40 characters. Each call adds to or replaces earlier keys; a key cannot be removed once set. The rules for each key are on [Context and placements](https://onestar.sh/docs/feedback/context-and-placements.md).

```js
onestar.q.push(["set", { app_version: "2.4.1", platform: "web", plan: "pro", cohort: "2026-10" }]);
```

### submit

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

Sends answers from a form you draw yourself. `answers` is keyed by question id. The promise resolves with a receipt, or rejects with `{ code, message, fields }`. [Your own UI](https://onestar.sh/docs/feedback/headless.md) covers ids, answer shapes and errors.

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

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

### threads

```ts
onestar.threads(form?: string, opts?: ThreadsOptions): Promise<ThreadList>
```

Returns the person's own conversations about a form: answers they gave that you replied to. With no form id it uses the first form on the page. The person is found by the signed token when one is set, else by this browser's device key. With neither, it resolves with an empty list and sends no request.

```ts
interface ThreadsOptions {
  api?: string;
  scope?: "form" | "all";
  seen?: boolean;
  user_token?: string;
}

interface ThreadList {
  threads: Thread[];
  unread: number;
}

interface Thread {
  id: string;
  form: string;
  title: string;
  product: string;
  answered_at: string;
  answer: { question: string; answer: string }[];
  messages: ThreadMessage[];
  state: "open" | "answered" | "closed";
  can_reply: boolean;
  unread: number;
  by_makers: boolean;
}

interface ThreadMessage {
  id: string;
  by: "maker" | "you";
  text: string | null;
  at: string;
  public: boolean;
}
```

- `scope: "all"` lists conversations on every form you run, not only this one.
- `seen: true` marks the replies read, which you see in OneStar as "Seen in the widget". Pass it when you are showing them.
- `by_makers` is true when you have shown that you make the product, so your UI may say "the makers" rather than "the form's owner".
- `text` is `null` for a message that was deleted. `public` is true for a reply also shown on the form's public page.
- `can_reply` says whether the person may answer back.

### reply

```ts
onestar.reply(threadId: string, text: string, opts?: { form?: string; api?: string; user_token?: string }): Promise<ThreadMessage>
```

Answers back in a conversation, up to 2,000 characters, and resolves with the new message. Call `threads()` first on the same page so the widget knows which form the conversation belongs to, or pass `opts.form`. A reply is sent once and never retried on its own, so a failed one never arrives twice.

It rejects with `{ code, message }`:

| Code | Meaning |
|---|---|
| `thread_closed` | The conversation is closed. |
| `thread_busy` | The person has written a lot in this conversation today. They can write again tomorrow. |
| `message_too_long` | The text is over 2,000 characters. |
| `invalid_message` | The text is empty. |
| `not_found` | The conversation is gone, or `threads()` was not called first and no form was given. |

Network and server failures reject with the codes on [Events and errors](https://onestar.sh/docs/feedback/events.md).

## The api option

`submit`, `threads` and `reply` take `api`, and the element takes an `api` attribute. Both point the widget at another OneStar API origin, which is only for running OneStar locally. Leave them out on your site. When an element on the page has `api` set, calls that name none use it.

## Not for your site: account

The API also has `onestar.account(token)` and the element an `account` attribute. They carry a OneStar session and are used only by the form's own page on OneStar, for forms answered with a OneStar account. On any other site the browser refuses a request that carries one, so leave them out.

## TypeScript

To type `window.onestar`, declare it once in a `.d.ts` file, with the interfaces above:

```ts onestar.d.ts
declare global {
  interface Window {
    /** The queue before the loader has run, the API after. */
    onestar: OneStarApi | { q: unknown[][] };
  }
}

export {};
```

Types for the element's events are on [Events and errors](https://onestar.sh/docs/feedback/events.md), and for the element in JSX on [Frameworks](https://onestar.sh/docs/feedback/frameworks.md).
