# Website JavaScript

> Read the buyer's account and sell from your own code with window.Toolaby, in plain JavaScript, React or Next.js.

`toolaby-web.js` defines `window.Toolaby`: the buyer's account as state you can read and subscribe to, and the actions the [website components](https://toolaby.app/docs/web-components.md) perform, as methods.

This page is the reference for `window.Toolaby`, then React and Next.js. It assumes the script is on the page; see [Your website](https://toolaby.app/docs/own-site.md).

## `window.Toolaby`

`window.Toolaby` exists once the script has run. The `toolaby:ready` event on `window` fires when the first answer arrives; `event.detail` is `Toolaby.state`.

```js
window.addEventListener('toolaby:ready', () => {
  const { signedIn, email, tools } = Toolaby.state;
  if (signedIn && tools['word-count']) showProFeatures();
});
```

| Member                                                              | Returns                   |                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Toolaby.state`                                                     | object                    | The last answer: `ready`, `mode` (`cookie`, `proxy`, `dev` or `link`), `signedIn`, `email`, `name`, `image`, and `tools`, where `tools[id]` is `true` for each tool the buyer holds, under both of its ids: the short one and the full one.                                                                                                                                    |
| `Toolaby.onChange(fn)`                                              | function                  | Calls `fn(state)` now and after every change. Returns a function that stops it.                                                                                                                                                                                                                                                                                                |
| `Toolaby.refresh()`                                                 | `Promise<state>`          | Asks Toolaby again.                                                                                                                                                                                                                                                                                                                                                            |
| `Toolaby.entitlements()`                                            | `Promise<object>`         | `{ signedIn, email, name, image, tools }`, straight from Toolaby.                                                                                                                                                                                                                                                                                                              |
| `Toolaby.session()`                                                 | `Promise<object \| null>` | The signed-in account, or `null`. On your domain, the session as the sign-in library returns it (`{ session, user }`). Through a proxy or in development the page reaches only the site's endpoints, so it is `{ user: { email, name, image } }`.                                                                                                                              |
| `Toolaby.signIn({ email, next })`                                   | `Promise`                 | With `email`, on your domain, sends a sign-in link to it. Without, or through a proxy or in development, opens the sign-in page. `next` is where the buyer lands; the default is this page.                                                                                                                                                                                    |
| `Toolaby.signInUrl(next)`                                           | string                    | The sign-in page's address, returning to `next` or this page.                                                                                                                                                                                                                                                                                                                  |
| `Toolaby.accountUrl()`                                              | string                    | The account page's address.                                                                                                                                                                                                                                                                                                                                                    |
| `Toolaby.signOut()`                                                 | `Promise`                 | Signs the buyer out, then refreshes.                                                                                                                                                                                                                                                                                                                                           |
| `Toolaby.checkout({ tool, plan, email, ext, seats, feature, web })` | `Promise`                 | Opens Stripe Checkout for the tool. `seats` buys a team licence or subscription on a plan sold per seat. `feature` is what opened your paywall, for [Analytics](https://toolaby.app/docs/analytics.md#web-apps). `web: true` counts the purchase as your web app's; a page that checked in has it already. A subscription signed out signs in first and continues.                                   |
| `Toolaby.startTrial({ tool, ext })`                                 | `Promise`                 | Starts the tool's free trial for the signed-in buyer, then opens the after-purchase page. The trial lasts as long as the longest trial a plan offers and unlocks that plan's features: see [Website components](https://toolaby.app/docs/web-components.md#buttons). Signed out, signs in first and continues. Refused, it rejects with the reason: no trial, an unverified or a disposable address. |
| `Toolaby.portal({ subscriptionId })`                                | `Promise`                 | Opens the Stripe customer portal.                                                                                                                                                                                                                                                                                                                                              |
| `Toolaby.token({ tool })`                                           | `Promise<string \| null>` | A site token for your own server: it names the signed-in buyer, the tool and this page. Lasts five minutes and is reused until shortly before it ends. `null` signed out. See [Your server](#your-server).                                                                                                                                                                     |
| `Toolaby.checkIn({ tool, version })`                                | `Promise<boolean>`        | Counts the signed-in buyer's use of your web app in [Analytics](https://toolaby.app/docs/analytics.md#web-apps): their day, and `version`, dotted numbers such as the day the app was deployed. Call it when a page of the app opens. `false` signed out, or when it was not taken.                                                                                                                  |
| `Toolaby.paywallShown({ tool, opened })`                            | `Promise<boolean>`        | Counts one view of your web app's paywall, by what opened it: a feature's key, `_limit` or `_paid`.                                                                                                                                                                                                                                                                            |
| `Toolaby.purchase()`                                                | `Promise<object>`         | On your after-purchase page: `{ status, tool, email, amount, inAccount, devices }` for the purchase Stripe returned the buyer from. `devices` is how many devices the plan bought allows.                                                                                                                                                                                      |

`Toolaby.origin` is where the script sends its requests: your workspace's address, or your proxy's path on your own domain.

A method that fails rejects with an `Error` whose `message` is fit to show, and `status` is the HTTP status when there was one. With no connection (`mode` is `link`), the methods that need the session reject; `checkout()` and `startTrial()` open the checkout page, and `portal()` the account page, instead.

## Selling from code

```js
document.querySelector('#buy').addEventListener('click', async (event) => {
  event.target.disabled = true;
  try {
    await Toolaby.checkout({ tool: 'word-count', plan: 'yearly' }); // the plan's id, as its card under Pricing shows it
  } catch (e) {
    event.target.disabled = false;
    showError(e.message);
  }
});
```

`checkout()` sends the page to Stripe Checkout. A failure rejects with the reason; a subscription asked for signed out goes to the sign-in page first and continues on return. After payment the buyer lands on the after-purchase page. Toolaby's own signs the extension that sent them (`ext`) in to the account the purchase is on: at once, if the buyer paid signed in, or through its sign-in, if they paid signed out. On your own after-purchase page (**Your own pages**), the extension unlocks when the buyer signs in inside it.

## React

Read the state with a hook:

```tsx
'use client';
import { useEffect, useState } from 'react';

type ToolabyState = { ready: boolean; mode: 'cookie' | 'proxy' | 'dev' | 'link' | null; signedIn: boolean; email: string | null; name: string | null; image: string | null; tools: Record<string, true> };
declare global { interface Window { Toolaby?: { state: ToolabyState; onChange(fn: (s: ToolabyState) => void): () => void; checkout(o: { tool: string; plan?: string; ext?: string; seats?: number }): Promise<unknown>; startTrial(o: { tool: string; ext?: string }): Promise<unknown>; signInUrl(next?: string): string; accountUrl(): string } } }

const initial: ToolabyState = { ready: false, mode: null, signedIn: false, email: null, name: null, image: null, tools: {} };

export function useToolaby(): ToolabyState {
  const [state, setState] = useState<ToolabyState>(initial);
  useEffect(() => {
    let stop = () => {};
    const attach = () => { stop = window.Toolaby!.onChange(setState); };
    if (window.Toolaby) attach(); else window.addEventListener('toolaby:ready', attach, { once: true });
    return () => { stop(); window.removeEventListener('toolaby:ready', attach); };
  }, []);
  return state;
}
```

Render nothing for the signed-in part until `ready` is `true`: before it, the state is unknown.

```tsx
export function ProBadge() {
  const { ready, tools } = useToolaby();
  if (!ready || !tools['word-count']) return null;
  return <span className="badge">Pro</span>;
}
```

The account button works in JSX as it is. For TypeScript, declare it once:

```ts
declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'toolaby-user': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & { 'sign-in-url'?: string; 'account-url'?: string; 'after-sign-out-url'?: string; 'sign-in-label'?: string; theme?: 'light' | 'dark'; align?: 'start' | 'end' };
    }
  }
}
```

## Next.js

Load the script once, in the root layout:

```tsx title="app/layout.tsx"
import Script from 'next/script';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script src="https://accounts.example.com/client/toolaby-web.js" strategy="afterInteractive" />
      </body>
    </html>
  );
}
```

The buyer's account is known in the browser, not on your server: `useToolaby()` belongs in client components. A server component renders the signed-out shape and the client fills it in. Through a proxy, add the route the dashboard shows as `app/__toolaby/[...path]/route.ts` and load the script from `/__toolaby/client/toolaby-web.js`.

To decide on your server who holds a tool — to render private content, or to answer an API of your own — ask Toolaby from the server, with an API key: see [Sync customers](https://toolaby.app/docs/sync.md). For the buyer making a request, use a site token: see [Your server](#your-server).

## Your server

A site token tells your server which buyer sent a request, and, with your workspace's secret key, lets it count that buyer's uses of your tool. Use it for a web app that does its work on your server: it can check who is asking, unlock a buyer, and count free uses as your extension counts them.

Your extension can send the same server a token too, with [`siteToken()`](https://toolaby.app/docs/reference/client.md#sitetoken): the buyer signed in on that device. An extension that does its work through your server spends the same count as the page, and an edited copy has no count of its own to skip.

### 1. Send a token with each request

```js
const token = await Toolaby.token({ tool: 'invoices' });
if (!token) return Toolaby.signIn(); // signed out
await fetch('/api/export', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body });
```

### 2. Check it on your server

The token is an ES256 JWT signed with your workspace's key. Check it with the keys Toolaby publishes for the tool, which follow every key rotation. With [jose](https://github.com/panva/jose):

```js
import { createRemoteJWKSet, jwtVerify } from 'jose';

const keys = createRemoteJWKSet(new URL('https://northwind.toolaby.app/api/tools/northwind--invoices/keys'));

const { payload } = await jwtVerify(token, keys, { algorithms: ['ES256'], audience: 'https://www.northwind.example' });
if (payload.use !== 'site' || payload.tool !== 'northwind--invoices') throw new Error('Not a site token for this tool');
```

A token your extension sent names it in `azp`. Check it against the request's `Origin` header, and keep what only that device may use bound to its `ins`:

```js
const fromExtension = typeof payload.azp === 'string';
if (fromExtension && request.headers.get('origin') !== payload.azp) throw new Error('Not this extension');
```

| Claim  | Type      | Description                                                                                             |
| ------ | --------- | ------------------------------------------------------------------------------------------------------- |
| `use`  | `'site'`  | Always `site`. A licence token never has it.                                                            |
| `sub`  | string    | The buyer's account id in your workspace.                                                               |
| `tool` | string    | The tool's full id.                                                                                     |
| `aud`  | string    | The origin of the page the token was made for.                                                          |
| `ent`  | boolean   | Whether the buyer held the tool when the token was made: bought, subscribed, on a trial or granted.     |
| `feat` | string\[] | The features the buyer's plan holds. Absent when the plan holds everything, or the buyer holds nothing. |
| `azp`  | string    | Made for an extension: its origin, `chrome-extension://<id>`. Absent in a page's token.                 |
| `ins`  | string    | Made for an extension: the device's install id, 64 hex characters. Absent in a page's token.            |
| `exp`  | number    | When it ends, five minutes after `iat`.                                                                 |

An extension's token carries `ent: false` on a device past the account's device limit, whatever the account holds.

### 3. Count uses

To count free uses, your server asks Toolaby with your workspace's secret key and the buyer's token. Toolaby reads what the buyer holds again, and counts nothing for a buyer who holds the tool. Create the key under **Configure → API**, or with `npx -y toolaby@latest api-keys create --env-file .env`, and keep it on your server.

```
POST https://northwind.toolaby.app/api/v1/tools/northwind--invoices/uses
Authorization: Bearer <your secret key, wk_live_…>
Toolaby-Site-Token: <the buyer's site token>
Idempotency-Key: <made by your server from what it counts>
Content-Type: application/json

{ "count": 12 }
```

| Parameter         | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ----------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `count`           | integer | Uses to count, 1 to 1,000. Refused whole when fewer remain.                                                                                                                                                                                                                                                                                                                                                                                        |
| `Idempotency-Key` | header  | Optional. Names one action: make it on your server from what that action counts, such as a hash of the items, and never pass along a key the browser sends as it is. A retried request with the same key and count answers what the first did and counts nothing more, for 24 hours; the same key with another count is refused. A refusal counts nothing and is not kept: the same key asks again, so a buyer who pays in between is let through. |

```json
{ "object": "usage", "allowed": true, "entitled": false, "used": 3, "remaining": 7, "limit": 10, "per": "ever" }
```

| Field                        | Description                                                                                                                                             |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allowed`                    | Whether to do the work.                                                                                                                                 |
| `entitled`                   | The buyer holds the tool: nothing was counted. `unlimited` is `true` beside it.                                                                         |
| `used`, `remaining`, `limit` | The free uses counted and left in the period, and how many it gives.                                                                                    |
| `per`                        | The period: `ever`, `day`, `week` or `month`.                                                                                                           |
| `window`, `resetsAt`         | For a day, week or month: when the period began and when it ends, ISO 8601, UTC.                                                                        |
| `reason`                     | When refused: `limit` (the free uses are spent) or `paid` (the tool has no free uses).                                                                  |
| `sharedUses`                 | `true` when the address is disposable or unverified: such accounts share one count.                                                                     |
| `deviceLimit`                | `{ allowed, held }`, for an extension's token: the account holds the tool, and this device is past its device limit. Its uses are counted as free ones. |

To read the count without counting, as a page or an extension opens, ask with `GET`. The answer has the same fields; `allowed` says whether one use would be.

```
GET https://northwind.toolaby.app/api/v1/tools/northwind--invoices/uses
Authorization: Bearer <your secret key>
Toolaby-Site-Token: <the buyer's site token>
```

Pass the answer to your extension's [`noteUses()`](https://toolaby.app/docs/reference/client.md#noteuses) so its account line says what is left.

Free uses are counted per account: by the account's inbox, in the tool's period, on the same count your extension uses when the tool requires an account. Errors are the API's ([API](https://toolaby.app/docs/reference/api.md#errors)): `401` for a missing or wrong key, or a site token that is missing, changed, ended or another tool's; `400` for a count out of range, or `idempotency_error` for an `Idempotency-Key` first used with another count; `409` while a request with the same `Idempotency-Key` is being answered; `429` when a buyer asks too fast.

A key that comes from the browser unchanged lets anyone replay one counted action for other items: Toolaby answers it as the first time and counts nothing. Make the key from the action itself, for example the browser's key and the items' ids hashed together, so that other items are another action.

The secret key is what makes a count your server's. A site token is made for your page, and anything that runs there could read it: a tag manager's script, an extension, a log. Without the key, it cannot count anyone's uses at Toolaby. It can still reach your own server as the buyer for five minutes, so keep third-party scripts off the pages that use it.

**Moving from `/api/me/uses`.** Until 23 October 2026 your server counted with the site token alone, at `POST` and `GET /api/me/uses`. That route still answers, with `Deprecation` and `Sunset` headers, until 1 December 2026, and is refused after it. Send the same requests to `/api/v1/tools/{tool}/uses` with your secret key, and the token as `Toolaby-Site-Token`.

## See also

* [Your website](https://toolaby.app/docs/own-site.md)
* [Website components](https://toolaby.app/docs/web-components.md)
* [Sync customers](https://toolaby.app/docs/sync.md)
