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:
<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:
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#
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#
onestar.open(form?: string): voidOpens 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.
close#
onestar.close(): voidCloses every open overlay on the page. Inline forms are not affected.
identify#
onestar.identify(token: string | null): voidSets 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.
set#
onestar.set(context: Record<string, unknown>): voidAdds 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.
onestar.q.push(["set", { app_version: "2.4.1", platform: "web", plan: "pro", cohort: "2026-10" }]);submit#
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 covers ids, answer shapes and errors.
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#
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.
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: truemarks the replies read, which you see in OneStar as "Seen in the widget". Pass it when you are showing them.by_makersis true when you have shown that you make the product, so your UI may say "the makers" rather than "the form's owner".textisnullfor a message that was deleted.publicis true for a reply also shown on the form's public page.can_replysays whether the person may answer back.
reply#
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.
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:
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, and for the element in JSX on Frameworks.