# Client

> The methods of toolaby.js, what they return, and whether they make a request.

`toolaby.js` is the client the wiring writes beside your code, typed by `toolaby.d.ts`. It has fifteen methods and two events. Everything it answers comes from a token Toolaby signed for this device and the client verified with the workspace's public key; a method marked *no request* answers from the device alone.

```js
import { toolaby } from './toolaby.js';
```

The same import works in the background service worker, the popup, a side panel and a content script. In the background the methods run; elsewhere they are forwarded to the background over `chrome.runtime.sendMessage` and answer the same.

## toolaby.config.js

The whole configuration, written by the wiring beside `toolaby.js`.

| Field           |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key`           | The tool key: `tk_test_…` while you try it, `tk_live_…` when it ships, `tk_dev_…` on a development server. It carries the platform's address, the tool, the public key your tokens are verified against, and what the Free plan held when it was written — what a new install shows until Toolaby's first word, never what it decides a use by. It works only from this extension's id. From the tool's Set up page, or `npx -y toolaby@latest wire`.                                                          |
| `appPopup`      | Your own popup, which the platform's opens on to once the device may. `''` means none stands in front — either you have no popup, or your popup draws the panel itself. `upgrade` keeps it as it is.                                                                                                                                                                                                                                                                                                           |
| `appSidePanel`  | Your own side panel, the same way. See [Surfaces](https://toolaby.app/docs/surfaces.md).                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `countOpens`    | Whether opening the Toolaby popup spends a free use. `false` where your code calls `toolaby.gate()` at the paid action: the popup then opens yours at once unless the person may not use the extension yet. `true` where your code calls no `gate()`, so the open is the use, except in an extension moved from ExtensionPay, whose ExtPay calls say what is paid: there it is always `false`. `wire` and `upgrade` write it from your code; absent, as in a file written before 3 October 2026, it is `true`. |
| `uninstallPage` | Whether Toolaby sets the page Chrome opens once the extension is removed ([Analytics](https://toolaby.app/docs/analytics.md#the-uninstall-page)). `true`: Toolaby sets it while the tool's uninstall page is on, and clears only one it set. `false`, where your code calls `chrome.runtime.setUninstallURL()` itself, or absent, as in a file written before 16 October 2026: Toolaby never sets or clears it. `wire` and `upgrade` write it from your code.                                                                        |
| `manifestKey`   | The extension's identity — the `key` the manifest carries, so every copy has the same id, which the tool key is bound to. Made once by the platform, or adopted from a manifest that already had one. Keep it; changing it changes the extension's id.                                                                                                                                                                                                                                                         |
| `accountBar`    | Whether the [account bar](https://toolaby.app/docs/toolaby-popup.md#the-account-bar) sits at the foot of your popup and side panel. `true`: in a plain extension `wire` and `upgrade` write it into your pages, between Toolaby's markers; in a WXT project the module docks it as it builds. `false` keeps it out, and the next `upgrade` takes Toolaby's block out of your pages. `upgrade` keeps what you set; absent, it is `true`.                                                                                              |

Swapping the key for the live one is the whole of shipping to the store; nothing else in the file changes. See [Test and Live](https://toolaby.app/docs/test-and-live.md).

## startBackground()

```ts
startBackground(options?: BackgroundOptions): void
```

Starts the client in the service worker: the message listeners the other contexts use, the handoffs from the sign-in and checkout pages, and the version check. Call it once, synchronously, at the top level of `background.js`. Throws outside the background.

| Option                 | Type                                           |                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ---------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `handlers`             | `Record<string, (message, sender) => unknown>` | Your `{ action }` messages, by action name. The return value is the reply.                                                                                                                                                                                                                                                                                                                                                                                             |
| `externalHandlers`     | same                                           | Messages from web pages (`externally_connectable`).                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `claimUnknown`         | `boolean`, default `true`                      | `false` leaves actions you do not handle to another `onMessage` listener of yours.                                                                                                                                                                                                                                                                                                                                                                                     |
| `skip`                 | `string[]`                                     | Startup checks to skip, by name.                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `contentScriptStorage` | `boolean`, default `false`                     | `true` lets your content scripts read and write `chrome.storage.local`. By default only the extension's pages and its background can, since the account link and the licence key are kept there: a content script asks the background for what it needs. Chrome 102 and later; other browsers are unchanged.                                                                                                                                                           |
| `contentScriptActions` | `boolean`, default `false`                     | `true` lets your content scripts ask for the account's actions: `activateLicense`, `signOut`, `resendLicense`, `listActivations`, `deactivate`, `capability`. By default only the extension's own pages can (the popup, the side panel, an options page), since a content script runs in the web page's own process; asked from anywhere else, each answers `{ ok: false }`. Reading (`getUser`, `has`, `policy`) and `gate()` are open to content scripts either way. |

The sign-in and purchase handoffs are taken only from a page of Toolaby's own address, never from another extension.

```js
toolaby.startBackground({
  handlers: {
    exportSheet: async ({ tab }) => {
      const permit = await toolaby.gate();
      if (!permit.allowed) return permit;
      return exportTableFrom(tab);
    },
  },
});
```

A handler runs inside the message that called it. An API that needs the click that sent the message, such as `chrome.sidePanel.open()`, works when the handler calls it before its first `await`.

## ensureBackground()

```ts
ensureBackground(): void
```

`startBackground()` unless something already called it. The Toolaby background the wiring puts in front of yours (`toolaby.background.js`), and the WXT module it writes (`modules/toolaby.wxt.ts`), call this after your own code has run, and so does ExtPay's `startBackground()` once [switched](https://toolaby.app/docs/extensionpay.md). A background that calls `startBackground({ handlers })` itself keeps its handlers, and one that never mentions Toolaby still starts it. When `ensureBackground()` ran first, as when ExtPay starts in a module your background imports, your own `startBackground()` takes over with its options. Not something you call.

## getUser()

```ts
getUser(): Promise<User>
```

What the device knows now. No request. `user.token` says when the answer was issued. See [User](https://toolaby.app/docs/reference/objects.md#user).

## getPlans()

```ts
getPlans(): Promise<ToolPlan[]>
```

The plans the tool sells, in the order **Pricing** shows them: each a [Plan](https://toolaby.app/docs/reference/objects.md#plan) with its `tier` and its `trialDays`. Asks Toolaby at most once a minute, also when the service worker stops and starts again in between; a page of the extension asks its background. Rejects when Toolaby cannot be reached, in every context.

## refresh()

```ts
refresh(): Promise<User>
```

Asks Toolaby, then answers as `getUser()` does. Call it when the popup opens, not on every action. Offline, answers what the device has.

| Case                                                                                                                          | Request |
| ----------------------------------------------------------------------------------------------------------------------------- | ------- |
| Toolaby was asked under five minutes ago on Live, or ten seconds ago on Test, whatever it answered: unpaid, or nothing at all | none    |
| The person has been to the checkout page, the sign-in page or the account page since                                          | one     |
| Otherwise                                                                                                                     | one     |

A purchase, a sign-in and a licence key are asked about when they happen, not at the next `refresh()`. Signing in as someone else, signing out or putting a licence key in forgets Toolaby's last answer, so the five minutes never hold one account to what Toolaby said about another.

## gate()

```ts
gate(arg?: number | { count?: number; feature?: string }): Promise<Permit>
```

May this device do the thing, now? Call it before the action, not when the popup opens.

| Argument  |                                                                |
| --------- | -------------------------------------------------------------- |
| `count`   | Uses to spend: a whole number from `1` to `1000`. Default `1`. |
| `feature` | A feature key from Pricing.                                    |

| Case                                                                                                                                                                                             | Answer                                                                                                                                                                                                                | Request                                                                           |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| A paid plan, trial or grant                                                                                                                                                                      | `{ allowed: true, isPremium: true, unlimited: true }`                                                                                                                                                                 | none                                                                              |
| A feature the plan does not hold                                                                                                                                                                 | `{ allowed: false, reason: 'feature', feature, needs }`. `needs` lists the ids of the plans that sell it.                                                                                                             | none on the Free plan; one on a paid plan, in case it changed                     |
| Free plan, uses remaining                                                                                                                                                                        | `{ allowed: true, isPremium: false, remaining, limit, used }`                                                                                                                                                         | one, to count the use                                                             |
| Free plan, uses spent                                                                                                                                                                            | `{ allowed: false, reason: 'limit', remaining: 0, limit }`                                                                                                                                                            | none                                                                              |
| Free plan requires sign-in, or holds nothing                                                                                                                                                     | `{ allowed: false, reason: 'sign_in' }` or `'paid'`                                                                                                                                                                   | none                                                                              |
| A `count` that is not a whole number from 1 to 1000                                                                                                                                              | `{ allowed: false, reason: 'count' }`. Nothing is spent, online or off.                                                                                                                                               | none                                                                              |
| Toolaby refuses the request: an extension that is not the tool's, too many at once, a device it does not know                                                                                    | `{ allowed: false, reason: 'refused' }`. Only when Toolaby does not answer does the device count on its own. An empty 429, Toolaby's word for a device past its own pace, is no answer: the device counts on its own. | one                                                                               |
| An answer that allows a paid plan's use without a token Toolaby signed for this device, or with one whose plan does not hold the feature                                                         | `{ allowed: false, reason: 'refused' }`. The device unlocks nothing.                                                                                                                                                  | one                                                                               |
| An answer with no token that allows a use the Free plan does not give, by Toolaby's latest signed word: a feature a plan sells, a use past the free uses, or any use before Toolaby's first word | `{ allowed: false, reason: 'refused' }`. The device counts nothing.                                                                                                                                                   | one, then the version check before refusing (on Live, at most every five minutes) |
| Build below the minimum version                                                                                                                                                                  | `{ allowed: false, blocked: true, message }`                                                                                                                                                                          | none                                                                              |
| A new install that has not heard from Toolaby yet                                                                                                                                                | The first call waits for Toolaby's word, a few seconds at most, then answers as above. Offline: `{ allowed: false, reason: 'limit', offline: true, unknown: true }`: nothing is given on a guess.                     | one                                                                               |

Offline, the device counts and answers with `offline: true`. See [Permit](https://toolaby.app/docs/reference/objects.md#permit).

## has()

```ts
has(feature: string): Promise<boolean>
```

Whether the device holds the feature: always for a Free plan feature, otherwise when the last token lists it. A tool with no features holds everything. No request, no use spent.

For display — the button drawn, greyed or badged. It stops nothing by itself: enforce a feature with `gate({ feature })` at the action.

## signOut()

```ts
signOut(): Promise<void>
```

Signs this device out and fires `onChange`. Its link is revoked on the account, so the account page no longer lists the device, and the entitlement and any site tokens go from the device. The account is untouched. Offline, the device signs out and the link lapses on its own.

## siteToken()

```ts
siteToken(options: { audience: string; fresh?: boolean }): Promise<string | null>
```

A site token for one of your own pages: the buyer signed in on this device, for that page's server. Use it when your extension does its work through your web app's server: the server checks who is asking and counts the buyer's uses with the token, as it does for the page. See [Your server](https://toolaby.app/docs/web-javascript.md#your-server).

| Argument   |                                                                                                                            |
| ---------- | -------------------------------------------------------------------------------------------------------------------------- |
| `audience` | The origin of one of your own pages, exactly as listed under Your own pages: `https://app.example.com`. The token's `aud`. |
| `fresh`    | `true` asks Toolaby again whatever is kept: for a server that refused the token it was sent. Default `false`.              |

| Case                                                         | Answer                         | Request                           |
| ------------------------------------------------------------ | ------------------------------ | --------------------------------- |
| Signed in, a token kept with more than 30 seconds left       | the token                      | none                              |
| Signed in                                                    | a new token, five minutes long | one, signed by the device         |
| No account signed in on this device, or its link was revoked | `null`                         | one, or none when no link is kept |
| An `audience` that is not a listed page                      | throws, with Toolaby's message | one                               |
| Toolaby does not answer                                      | throws                         | one                               |

The token carries a page token's claims and two more: `azp`, this extension's origin (`chrome-extension://<id>`), and `ins`, this device's install id. Toolaby reads what the account holds at each token; a device past the account's device limit gets `ent: false`. Signing out or signing in as someone else drops every token kept. Not from a content script.

## noteUses()

```ts
noteUses(usage: UsesAnswer): Promise<boolean>
```

Takes Toolaby's answer from [`/api/v1/tools/{tool}/uses`](https://toolaby.app/docs/reference/api.md#count-a-buyers-uses), as your server passed it on, into this device's meter: `getUser().usage` and the account line then say what is left. Call it after each answer your server sends back from a count, or from a `GET` of the same route when the popup opens.

| Argument |                                                                             |
| -------- | --------------------------------------------------------------------------- |
| `usage`  | The answer as Toolaby gave it: `{ object: 'usage', used, per, window, … }`. |

Returns `true` when the meter took it. The meter only ever counts more uses, never fewer, and takes an answer only for a tool that requires an account, with one signed in, in the period Toolaby last signed. It gives no use: what is allowed stays your server's answer. Not from a content script.

## versionState()

```ts
versionState(options?: { fresh?: boolean }): Promise<VersionState>
```

The version check's last answer, kept across the service worker's restarts. `{ fresh: true }` asks Toolaby again: on Test at every call, on Live when the answer is five minutes old or more. See [VersionState](https://toolaby.app/docs/reference/objects.md#versionstate).

## openPaymentPage()

```ts
openPaymentPage(planId?: string): Promise<void>
```

Opens the checkout page in a tab, with the plan preselected when named. A purchase is on the buyer's account, and the device holds it through the account the extension is signed in to. After payment, Toolaby's after-purchase page signs the extension in to that account at once, if the buyer paid signed in. Bought signed out, the page's one button is the sign-in that does it, with the address the purchase was made with. `onPaid` fires when the device unlocks.

## openLoginPage()

```ts
openLoginPage(provider?: 'google'): Promise<void>
```

Opens the sign-in page. Signed in, the page hands the session to the extension. With `'google'` — the popup's *Continue with Google* — the page goes straight to Google instead of asking again. Google sign-in is on for every workspace, through Toolaby's Google client or your own from **Configure → Sign-in**.

## openAccountPage()

```ts
openAccountPage(): Promise<void>
```

Opens the buyer's account page.

## showPaywall()

```ts
showPaywall(permit: Permit | { feature: string; needs?: string[] }): Promise<void>
```

Answers a refused permit with the paywall, from your popup or your side panel. The page gives way to the paywall for that reason, at that surface's width, in your brand and the buyer's language. A **Back** button returns to the page. Once the device may continue (bought, signed in, the feature held), it returns there by itself. Showing it spends nothing.

```js
const permit = await toolaby.gate({ feature: 'export' });
if (!permit.allowed) return toolaby.showPaywall(permit);
```

| Permit              | The paywall shows                                                                                         |
| ------------------- | --------------------------------------------------------------------------------------------------------- |
| `reason: 'feature'` | *Export is in Pro*, and only the plans that hold the feature, `needs` first.                              |
| `reason: 'limit'`   | The free-use meter, then the plans.                                                                       |
| `reason: 'paid'`    | The plans.                                                                                                |
| `reason: 'sign_in'` | The sign-in doors.                                                                                        |
| `deviceLimit` set   | *In use on 2 devices already*, and the way to the account page, where the buyer signs another device out. |
| `blocked: true`     | Your message from Versions.                                                                               |
| `allowed: true`     | Nothing: it returns without showing anything.                                                             |

`{ feature: 'export' }` on its own asks for a feature's plans without calling `gate()` first — for a button drawn locked because `has()` said no.

The paywall opens in the Toolaby popup's own page, `toolaby-popup.html` or `toolaby-sidepanel.html`, which `wire` and `create` put beside every popup and side panel. An extension without it — a build that left it out — gets the checkout page in a new tab instead, never a missing page. A side panel of your own says it is one with `<html data-toolaby-surface="sidepanel">`, or the paywall opens at popup width.

From a page of the extension open in a tab, such as an editor or a report, navigating away would lose its work. There it opens the checkout page in a new tab instead: the sign-in page for `sign_in`, nothing for `blocked`. A content script, whose page is not the extension's, does the same. The purchase reaches the extension as any other does: `onPaid` fires, and `has()` answers yes at the page's next call. In the background it throws, since there is no page to show it in: return the permit to the page and call it there. See [Gate your extension](https://toolaby.app/docs/gating.md).

## onPaid, onChange

```ts
onPaid: { addListener(cb: (user: User) => void): void; removeListener(cb): void }
onChange: { addListener(cb: (user: User) => void): void; removeListener(cb): void }
```

Background only. `onPaid` fires when the device goes from unpaid to paid. `onChange` fires on every change of `user.paid`. Neither fires for the first observation after the worker wakes. A popup reads `getUser()` when it opens instead.

## ExtensionPay's calls

`toolaby.js` also exports `ExtPay`, which answers ExtensionPay's calls (`getUser()`, `openPaymentPage()`, `onPaid` and the rest) in ExtensionPay's shapes, for an extension moving from it. See [Move from ExtensionPay](https://toolaby.app/docs/extensionpay.md).

## Errors

No method throws on a network failure; each answers from the device and marks the answer `offline` where it matters. `startBackground()` throws outside the background; `Toolaby({ key })` throws at startup when `key` is not a tool key. A method called from a page rejects with *Toolaby is not running in this extension's background* when nothing started it there. See [Troubleshoot an extension](https://toolaby.app/docs/troubleshooting.md).
