URL: https://wattfare.com/docs/client-sdk
# Client SDK

> Import from wattfare/client. Framework-agnostic, runs in the browser, never touches the secret key — it works entirely with session tokens minted by your backend.

## createWattfare(config)

Creates a browser client. It throws immediately if the publishable key is missing or malformed.

```ts
import { createWattfare } from "wattfare/client";

const wf = createWattfare({
  publishableKey: "pk_live_…",        // required, starts with "pk_"
  session: "/api/wattfare-token",     // backend route OR a token function
  baseURL: "https://wattfare.com",    // optional
  consentURL: "https://wattfare.com", // optional popup origin
});
```

| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| publishableKey | string | — | Required. Must start with `pk_`. |
| session | string \| () => string \| Promise<string> | — | Required. Your backend route path, or a function returning a token. |
| baseURL | string | https://wattfare.com | Override the Wattfare service URL. |
| consentURL | string | https://wattfare.com | Override the consent popup origin. |

## The session source

`session` is how the client gets a token without ever seeing your secret key. Pass a string for the easy path, or a function when you need custom headers or token plumbing:

```ts
// Shorthand: a backend route path. The SDK POSTs to it and reads
// { token } (or a raw token string) from the response.
const wf = createWattfare({ publishableKey, session: "/api/wattfare-token" });

// Full control: a function that returns (or resolves) a token string.
const wf = createWattfare({
  publishableKey,
  session: async () => {
    const res = await fetch("/api/wattfare-token", { method: "POST" });
    const { token } = await res.json();
    return token;
  },
});
```

> **Tokens are cached**
>
> The client caches the token until ~30 seconds before it expires, so `status()`, `connect()`, and `disconnect()` don't each round-trip to your backend.

## connect()

Opens the consent popup and resolves once the user approves or denies. On approval it returns the fresh `ConnectionStatus`; on denial (or if they close the popup) it returns the not-connected status.

```ts
// Must be called from a user gesture (click/tap).
connectBtn.onclick = async () => {
  const status = await wf.connect();
  if (status.connected) renderBudget(status.remainingUsd);
};
```

> **Call it inside the click**
>
> The SDK opens a blank popup *synchronously* in the handler, then navigates it once the token resolves — that's what beats popup blockers. If you do your own `await` before calling `connect()`, the popup opens outside the gesture and gets blocked. It throws `WattfareNetworkError` in that case.

## status()

Fetches the current connection and budget snapshot for the session's user. Returns `{ connected: false, … }` when they haven't connected — it never throws for that case, so you can call it on every page load safely.

```ts
const status = await wf.status();
// { connected: true, limits: { monthlyUsd: 10 },
//   usage: { monthlyUsd: 3.2 }, remainingUsd: 6.8 }

if (!status.connected) showConnectButton();
```

## disconnect()

Revokes the connection for the session's user. Their budget is immediately cut off from your app.

```ts
await wf.disconnect();
// Connection revoked. In-flight requests may finish; new ones won't be authorised.
```

## requestGrant(options)

Opens the consent popup for a one-time access grant — no persistent connection needed. See [One-time grants](https://wattfare.com/docs/grants) for the full API, options, and examples.

## WattfareClient

| Method | Returns | Description |
| --- | --- | --- |
| connect() | Promise<ConnectionStatus> | Open the consent popup; resolve on approve/deny. |
| status() | Promise<ConnectionStatus> | Current connection + budget snapshot. |
| disconnect() | Promise<void> | Revoke the connection. |
| requestGrant(options) | Promise<GrantResult \| null> | Request a one-time access grant; resolve on approve/deny. |

> **Using React?**
>
> [wattfare/react](https://wattfare.com/docs/react-sdk) wraps this client in a provider + hook so you get reactive `connected` / `remainingUsd` state for free.
