Skip to content
Insy
For developers
Esc
navigateopen⌘Jpreview
On this page

Login widget

Add Sign in with Insy to any page with a single script tag — a public OAuth client that runs PKCE in the browser and returns tokens to your code.

The login widget is a drop-in script tag that renders a Sign in with Insy button and completes the whole OAuth flow in the browser. It is a public client and uses PKCE, so it needs no client secret and no backend on your side.

Button builder

Pick a look and copy the tag. The preview uses the widget’s real styles; it does not sign anyone in.

Theme
Corners
Size
Label
Language
Scopes
<script async src="https://insy.io/oauth/widget.js"
        data-client-id="oac_your_client_id"
        data-scope="memberships.read"
        data-on-auth="onInsyAuth"></script>

Copy and paste

The button renders inline where the script tag sits — place the tag where you want it.

<script
  async
  src="https://insy.io/oauth/widget.js"
  data-client-id="oac_9f2c41e7a83b06d5c4e19f27"
  data-scope="memberships.read"
  data-lang="en"
  data-size="md"
  data-on-auth="handleInsyAuth"
></script>

<script>
  function handleInsyAuth(result) {
    if (!result.ok) {
      console.warn("Insy sign-in failed:", result.error, result.errorDescription);
      return;
    }
    // Send the token to your own backend. Do not trust the browser with privileged work.
    fetch("/api/insy/session", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ accessToken: result.accessToken }),
    });
  }
</script>

Attributes

Attribute Type Default Description
data-client-id string Required. Your public client ID, in the form oac_ plus 24 hex characters.
data-scope string Space-separated scopes. Always set this; the server requires a scope.
data-size sm | md | lg md Button size. small, medium and large are also accepted.
data-lang pl | en pl Button label language.
data-text string Overrides the button label entirely. Ignores data-lang.
data-radius number Corner radius in CSS pixels.
data-on-auth string The name of a function on window that receives the result.

data-on-auth is a name, not an expression

The value is resolved as a dotted path against window, so data-on-auth="myApp.onInsyAuth" finds window.myApp.onInsyAuth. It is never evaluated as JavaScript, which is what lets the widget run under a strict Content Security Policy. Anything that is not a plain dotted path — a call, an arrow function, an inline statement — will not resolve.

<script src="https://insy.io/oauth/widget.js" data-on-auth="myApp.onInsyAuth" ...></script>
<script src="https://insy.io/oauth/widget.js" data-on-auth="onInsyAuth(event)" ...></script>

Receiving the result

There are two ways to get the result, and both fire for every attempt. Pick whichever fits your codebase — you do not need both.

Define a function on window and name it in the attribute.

<script>
  window.handleInsyAuth = function (result) {
    if (result.ok) {
      console.log("scope granted:", result.scope);
      console.log("expires in:", result.expiresIn, "seconds");
    } else {
      console.warn(result.error, result.errorDescription);
    }
  };
</script>

<script
  async
  src="https://insy.io/oauth/widget.js"
  data-client-id="oac_9f2c41e7a83b06d5c4e19f27"
  data-scope="memberships.read products.read"
  data-on-auth="handleInsyAuth"
></script>

The widget also dispatches a bubbling CustomEvent named insy-auth. Its detail is the same object the callback receives. Use this if you would rather not attach anything to window — it suits frameworks where a module-scoped listener is cleaner.

document.addEventListener("insy-auth", async (event) => {
  const result = event.detail;

  if (!result.ok) {
    showError(result.error);
    return;
  }

  await fetch("/api/insy/session", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ accessToken: result.accessToken }),
  });
});

Because the event bubbles, a listener on document catches results from every widget on the page. Attach the listener before the widget script loads so you cannot miss an early result.

The success object

{
  ok: true,
  state: "…",           // the state value tied to this attempt
  accessToken: "…",     // opaque; send it as Authorization: Bearer <token>
  refreshToken: "…",    // opaque; 30 days, sliding
  expiresIn: 3600,      // seconds until the access token expires
  scope: "memberships.read",
  tokenType: "Bearer"
}

Both tokens are opaque random strings. Do not try to decode or parse them — they carry no claims. scope reflects what was actually granted, which is what you should branch on, not what you asked for.

The failure object

{
  ok: false,
  error: "popup_blocked",
  errorDescription: "…"
}
error Cause What to do
popup_blocked The browser blocked the popup, usually because the click was not treated as a user gesture. Ask the user to allow popups, or trigger the widget directly from a real click rather than from timers or async code.
closed_by_user The user closed the popup before finishing. Treat as a cancel. Leave the UI in its signed-out state and let them retry.
insecure_context The page is not HTTPS and not localhost, so Web Crypto is unavailable and PKCE cannot run. Serve the page over HTTPS. There is no fallback — PKCE is mandatory for public clients.
verifier_lost The PKCE verifier was gone when the code came back, typically because the page reloaded or storage was cleared mid-flow. Start a fresh attempt. Do not navigate or reload the opener while the popup is open.
no_code The callback returned without an authorization code. Retry. If it persists, check that the origin the page runs on is registered.
token_request_failed The code-for-token exchange was rejected. Check the client ID, that the client is registered as public, and that the requested scopes are granted to it.
access_denied The user declined on the consent screen. Treat as a cancel, same as closed_by_user.

error may also carry any other OAuth error returned by the authorize endpoint, such as invalid_scope for a scope your client is not registered for. Handle unknown values with a generic failure path rather than assuming the list above is exhaustive at runtime.

How the popup flow works

The widget prepares a PKCE pair

On click it generates a random code_verifier and derives the S256 code_challenge using Web Crypto. The verifier stays on your page and is never transmitted.

A popup opens the authorize URL

The popup goes to https://api.insy.io/oauth/authorize carrying the challenge, your client ID, the scope and a state value.

The user signs in and consents

Everything credential-related happens on Insy’s own origin. Your page never sees the user’s password.

The popup lands on a callback page Insy hosts

That page posts the authorization code back to the opener — your page — and closes.

Your page exchanges the code

The widget calls POST /oauth/token with the code and the verifier it kept, then delivers the result through data-on-auth and the insy-auth event.

This split is what makes a browser-only client safe: the authorization code is useless without the matching code_verifier, which never leaves your page, so an attacker who intercepts it — from a log, a referrer header, or a malicious extension reading the popup URL — cannot redeem it. The code is also single-use and expires after 60 seconds.

Registering web origins

The widget only runs on origins registered on the client at Account → Developer → OAuth clients. An origin is the scheme, host and port together.

Each entry is either an exact origin or a host with one leading *. label, which covers every subdomain beneath it at any depth — the latter exists for per-branch preview deploys. The scheme and the port are never wildcarded.

You do not register a redirect URI for the widget. The flow always returns to the callback page Insy hosts at https://insy.io/oauth/widget-callback, accepted for any client with at least one web origin registered — your web origins are what gate where the widget runs.

Exact origins

https://shop.example
https://app.shop.example
http://localhost:5173

https://shop.example matches only https://shop.example. It does not cover https://www.shop.example, http://shop.example or https://shop.example:8443 — register each one you actually use.

Wildcard subdomains

https://*.vercel.app
Origin Matches Why
https://my-app-git-main.vercel.app Yes Any host under the suffix matches.
https://preview-42.vercel.app Yes Same.
https://deep.nested.vercel.app Yes The wildcard matches at any depth, not just one label.
https://vercel.app No The apex is not covered by a wildcard label. Register it separately.
http://my-app.vercel.app No The scheme is never wildcarded.
https://my-app.vercel.app:8443 No The port is never wildcarded.

If you need the apex as well as the previews, register both https://vercel.app and https://*.vercel.app.

Every host under the suffix is authorised, at every depth, to run the widget and to call the token endpoint cross-origin. Register the narrowest suffix you control — on a shared preview domain that means asking for exact origins instead.

Security

If your application has a backend that can hold a secret, the server-side flow with a confidential client keeps tokens out of the browser entirely and is the stronger option.

Was this page helpful?