> ## Documentation Index
> Fetch the complete documentation index at: https://docs.custral.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Browser SDK

> Track usage and talk to the Custral API from the browser with a publishable key.

**`@custral/js`** is the browser-facing, **publishable-key** surface, separate
from the secret-key [`@custral/sdk`](/dev/sdks/typescript). It is the protocol
layer the other browser packages build on:

* **`CustralUsage`**: report per-customer usage events against a Custral usage
  module, and read them back.
* **`CustralError` and friends**: one error hierarchy to `catch`, whatever
  failed.
* **`request()`**: the raw `{data, error, reqId}` envelope, unwrapped, if you are
  calling an endpoint the package does not wrap yet.

<Info>
  Looking for the chat widget? It is [`@custral/widget`](/dev/sdks/react), which
  builds on this package. `CustralChat` used to ship here under
  `@custral/ui@1.x`; it moved, because the only thing that ever used it was the
  widget itself.
</Info>

<Warning>
  `@custral/ui@1.x` is **deprecated**. It contained no UI: it held the chat engine
  and the usage client, which now live in `@custral/widget` and `@custral/js`
  respectively. Nothing about the API changed, so migrating is a rename of the
  import. `@custral/ui` 2.0 is a different package: React hooks over your workspace
  data, documented as the [React data SDK](/dev/sdks/ui).
</Warning>

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @custral/js
  ```

  ```bash pnpm theme={null}
  pnpm add @custral/js
  ```
</CodeGroup>

`socket.io-client` is an **optional** peer, needed only if you import the
`@custral/js/socket` subpath. Usage tracking and the error types pull in nothing.

## Usage tracking

`CustralUsage` records that a customer consumed a metered product. It is safe to
call from a browser at the moment of consumption, which is the point: your own
application is the only thing that knows a unit was used.

```ts theme={null}
import {CustralUsage} from "@custral/js";

const usage = new CustralUsage({
  key: process.env.NEXT_PUBLIC_CUSTRAL_PUBLISHABLE_KEY!,
  moduleId: "umod_...",
});

await usage.track({
  productRecordId: "rec_product_basic",
  customerRecordId: "rec_customer_acme",
  quantity: 1,
});

await usage.track({
  event: "report_created",
  customerRecordId: "acct_42", // or a Custral record id
  user: "jane@acme.com",
});
```

<ParamField path="new CustralUsage({key, moduleId, baseUrl?, timeoutMs?})">
  `key` is a publishable (`pk_…`) or secret (`sk_…`) key; `moduleId` is the usage
  module (`umod_…`) events are recorded against. `baseUrl` defaults to
  `https://api.custral.com` and `timeoutMs` to 30 seconds.
</ParamField>

<ParamField path="await usage.track({customerRecordId, productRecordId?, event?, user?, quantity?, occurredAt?, metadata?})">
  Records one event. It needs a customer, plus a product or an `event` name such
  as `report_created`. `user` is the person inside the account: your own user id
  or their email. `quantity` defaults to `1` and `occurredAt` to now. An
  `occurredAt` that cannot be read is refused by name rather than throwing a bare
  `RangeError`. If an identifier matches no record, the result's `unmatched`
  names that side (`["customer"]`). The event is kept, but counts toward no
  record's usage.
</ParamField>

<ParamField path="await usage.getEvents({productRecordId?, customerRecordId?, event?, from?, to?, limit?, offset?})">
  Reads recorded events back, newest first. **Requires a secret key holding
  `usage:read`**; a publishable key is refused with `insufficient_scope`.
</ParamField>

<Note>
  Reading events is paged at 50 by default (200 max). A usage event is written
  per metered call in your own application, so this is the one list whose length
  is not bounded by anything a person does in the workspace. Filter by date to
  total a period rather than paging to the end.
</Note>

The `key` option was called `publishableKey` under `@custral/ui`. That spelling
still works and is deprecated: the same client now takes a secret key too, so the
old name described only half of what it accepts.

## Errors

Every call throws a `CustralError` subclass. Custral's API answers HTTP 200 with
the failure inside the envelope, so status alone is not the signal, and this is
the one place that is decided.

```ts theme={null}
import {CustralUsage, CustralAuthenticationError, CustralError} from "@custral/js";

try {
  await usage.track({productRecordId: "rec_...", customerRecordId: "rec_..."});
} catch (err) {
  if (err instanceof CustralAuthenticationError) {
    // The key is unknown, inactive, or expired.
  } else if (err instanceof CustralError) {
    console.error(err.code, err.statusCode, err.requestId);
  }
}
```

| Class                        | Raised for                                                       |
| ---------------------------- | ---------------------------------------------------------------- |
| `CustralAuthenticationError` | 401: an unknown, inactive, or expired key                        |
| `CustralPermissionError`     | 403: the key lacks the scope                                     |
| `CustralNotFoundError`       | 404                                                              |
| `CustralInvalidRequestError` | 400: a malformed or incomplete payload                           |
| `CustralRateLimitError`      | 429                                                              |
| `CustralAPIError`            | 5xx                                                              |
| `CustralConnectionError`     | The request never got an answer: offline, or the deadline passed |

Every one carries `code`, `statusCode` and `requestId`. Quote the `req_…` when
you contact support: it is what makes a failure findable in our logs.

## Realtime

`@custral/js/socket` is a **subpath**, not part of the base entry, because
`socket.io-client` is browser-only and around 40KB. Import it only where you
actually open a socket.

```ts theme={null}
import {connectSocket} from "@custral/js/socket";

const socket = connectSocket({
  namespace: "/widget",
  auth: {visitorId, orgId},
});
```

<Warning>
  The connection forces the WebSocket transport. Custral runs multiple API
  instances behind a load balancer with no sticky sessions, so HTTP long-polling
  pins a session to one instance and fails whenever a poll lands on another. A
  client that cannot open a WebSocket gets no realtime at all, so anything built
  on this needs an HTTP path to the same data.
</Warning>

## Troubleshooting

<AccordionGroup>
  <Accordion title="`insufficient_scope` from getEvents">
    Reading events needs a **secret** key (`sk_…`) holding `usage:read`. Writing
    one with `track()` is publishable-key authed so it can be called from a
    browser; reading them back is not, or a client-side key would let anyone
    enumerate your workspace's usage.
  </Accordion>

  <Accordion title="`CustralUsage: a key is required`">
    The constructor needs `key` (or the deprecated `publishableKey`). Create one
    in **Settings → Applications**.
  </Accordion>

  <Accordion title="Which package do I want?">
    `@custral/js` in the browser with a **publishable** key for usage tracking.
    [`@custral/widget`](/dev/sdks/react) for the React chat widget.
    [`@custral/ui`](/dev/sdks/ui) for React hooks over records and objects.
    [`@custral/sdk`](/dev/sdks/typescript) on a server with a **secret** key for
    records, objects, and webhooks.
  </Accordion>

  <Accordion title="I was using @custral/ui 1.x">
    `CustralUsage` and the error types are now in `@custral/js`; `CustralChat` is
    in `@custral/widget`. Change the import and nothing else.
  </Accordion>
</AccordionGroup>


## Related topics

- [SDKs](/dev/sdks/overview.md)
- [TypeScript SDK](/dev/sdks/typescript.md)
- [Developer Overview](/dev/overview.md)
- [React SDK](/dev/sdks/react.md)
- [React data SDK](/dev/sdks/ui.md)
