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

# JavaScript SDK

> Reference for the official Bondify JavaScript/TypeScript packages — @bondify/react (browser) and @bondify/node (server).

Bondify ships two official JS/TS packages. Both include full TypeScript definitions — no separate `@types/` package needed.

| Package                            | Use it for                                                                       |
| ---------------------------------- | -------------------------------------------------------------------------------- |
| [`@bondify/react`](#bondify-react) | Browser sign-in: provider, button, modal, QR, hooks, and Next.js server helpers  |
| [`@bondify/node`](#bondify-node)   | Server: proof verification, Express/Next middleware, webhook handlers, admin API |

Current versions: **`@bondify/node` 3.0.2**, **`@bondify/react` 3.0.1** (Node ≥ 18; React 18/19; Next.js 14–16).

<Warning>
  **Upgrading from 1.x or 2.x?** Version 3.0.0 has breaking changes:
  `verifyProof()` is now `async`, and its returned fields are camelCase
  instead of snake\_case. See [SDK 3.0.0 in the changelog](/changelog/changelog#sdk-3-0-0-standardized-identity-shape-async-verification)
  for the full migration guide. `1.x` is deprecated and unsupported — upgrade
  straight to `3.x`.
</Warning>

***

## @bondify/react

```bash theme={null}
npm install @bondify/react
# optional, only for <BondifyQR>:
npm install qrcode
```

### `<BondifyProvider config={…}>`

Wrap your app once. `config` is a `BondifyConfig`:

<ParamField path="projectId" type="string" required>
  Your project ID (`proj_…`). Public.
</ParamField>

<ParamField path="apiUrl" type="string" default="https://api.bondify.dev">
  Override the API base URL.
</ParamField>

<ParamField path="mode" type="'redirect' | 'inline'" default="'redirect'">
  `redirect` opens the Telegram deep link in a new tab on `startAuth`. `inline` leaves opening the deep link to you (e.g. render a QR with `<BondifyQR>`).
</ParamField>

<ParamField path="pollingInterval" type="number" default="1500">
  Status poll interval, in milliseconds.
</ParamField>

<ParamField path="pollingTimeout" type="number" default="120000">
  How long the client polls before giving up (`POLLING_TIMEOUT`), in milliseconds.
</ParamField>

<ParamField path="onSuccess" type="(user: BondifyUser) => void">
  Called when the user confirms.
</ParamField>

<ParamField path="onError" type="(error: BondifyError) => void">
  Called on failure.
</ParamField>

<ParamField path="onCancel" type="() => void">
  Called when the user cancels in Telegram.
</ParamField>

### Hooks

| Hook                   | Returns                                              |
| ---------------------- | ---------------------------------------------------- |
| `useBondifyAuth()`     | Full state + actions (see below)                     |
| `useBondifyUser()`     | `BondifyUser \| null`                                |
| `useBondifyStatus()`   | `AuthStatus`                                         |
| `useBondifyError()`    | `BondifyError \| null`                               |
| `useBondifySession()`  | `{ deeplink, sessionToken, expiresAt, secondsLeft }` |
| `useBondifyActions()`  | `{ startAuth, reset, checkStatus }`                  |
| `useIsAuthenticated()` | `boolean`                                            |

```ts theme={null}
const {
  status, user, error,
  deeplink, sessionToken, expiresAt, secondsLeft,
  startAuth,   // () => Promise<void>
  reset,       // () => void
  checkStatus, // () => Promise<void>
} = useBondifyAuth()
```

### Components

```tsx theme={null}
import { BondifyButton, BondifyModal, BondifyQR } from '@bondify/react'
```

**`<BondifyButton>`** — `label?`, `theme?: 'telegram' | 'dark' | 'light' | 'custom'`, `size?: 'sm' | 'md' | 'lg'`, `showIcon?: boolean`, `className?`. Plus standard `<button>` attributes. Success/error/cancel are handled by the provider's `config` callbacks.

**`<BondifyModal>`** — `open?`, `onOpenChange?`, `title?`, `description?`, `className?`. A dialog that runs the flow with a QR + button.

**`<BondifyQR>`** — `size?` (default 200), `showLink?`, `className?`. Renders a QR for the current session's deep link.

### Types

```ts theme={null}
interface BondifyUser {
  telegramId: string
  telegramName: string
  telegramUsername: string | null
  telegramPhone: string | null
  proof: string            // JWT — verify on your server
  confirmedAt: number      // epoch ms
}

type AuthStatus =
  | 'idle' | 'pending' | 'polling'
  | 'confirmed' | 'expired' | 'cancelled' | 'error'

interface BondifyError {
  code:
    | 'SESSION_EXPIRED' | 'SESSION_CANCELLED' | 'NETWORK_ERROR'
    | 'PROJECT_NOT_FOUND' | 'PROJECT_INACTIVE' | 'PUBLIC_ACCESS_DISABLED'
    | 'RATE_LIMITED' | 'POLLING_TIMEOUT' | 'UNKNOWN_ERROR'
  message: string
  details?: unknown
}
```

### Server helpers — `@bondify/react/server`

Next.js App Router only. `getServerUser` and `requireAuth` read the JWT
secret from an explicit `jwtSecret` argument first; if omitted, they fall
back to environment variables in this order: `BONDIFY_WEBHOOK_SECRET` →
`BONDIFY_JWT_SECRET` → `SERVER_SECRET`. Use `BONDIFY_WEBHOOK_SECRET` unless
you already have one of the other two set for another purpose in your
project.

<Warning>
  **This subpath is `server-only`, enforced at build time, not just by
  convention.** As of v3.0.0, `@bondify/react/server` imports the `server-only`
  package, so calling any of these exports from a file marked `'use client'`
  (or anything only reachable from one) fails the build immediately with a
  clear message. If you need one of these functions in response to a client
  event (e.g. after `<BondifyButton onSuccess>`), don't call it directly —
  wrap it in its own **Server Action** module and call that module from the
  Client Component instead. This was previously just a code comment, and the
  most common report of these helpers "not working" was actually this
  import-location mistake, surfacing as a confusing runtime error from
  `next/headers` instead of a clear build-time one.
</Warning>

| Export               | Signature                                           | Purpose                                 |
| -------------------- | --------------------------------------------------- | --------------------------------------- |
| `saveProofCookie`    | `(proof: string) => Promise<void>`                  | Set the httpOnly `bondify_proof` cookie |
| `clearProofCookie`   | `() => Promise<void>`                               | Sign out                                |
| `getProofFromCookie` | `() => Promise<string \| null>`                     | Read the raw proof                      |
| `getServerUser`      | `(opts?) => Promise<BondifyUser \| null>`           | Verify the cookie's proof (SSR)         |
| `requireAuth`        | `(redirectTo?, jwtSecret?) => Promise<BondifyUser>` | Redirect if not signed in               |

See the [Next.js guide](/guides/nextjs/nextjs) for usage.

***

## @bondify/node

```bash theme={null}
npm install @bondify/node
```

### `new BondifyServer(config)`

<ParamField path="jwtSecret" type="string" required>
  Your project's **webhook secret** (`whsec_…`). Verifies the proof JWT.
</ParamField>

<ParamField path="webhookSecret" type="string" default="= jwtSecret">
  Secret used to verify webhook signatures. It's the same `whsec_…`, so you can omit it.
</ParamField>

<ParamField path="apiUrl" type="string" default="https://api.bondify.dev" />

**Methods**

| Method                                  | Returns                        | Notes                                                                               |
| --------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------- |
| `verifyProof(proof)`                    | `Promise<BondifyUser>`         | **Async** — always `await` it. Throws `BondifyVerificationError` on invalid/expired |
| `safeVerifyProof(proof)`                | `Promise<BondifyUser \| null>` | **Async.** Never throws                                                             |
| `verifyWebhook(rawBody, signature)`     | `WebhookEvent`                 | Synchronous. Throws `BondifyWebhookError` on bad signature                          |
| `safeVerifyWebhook(rawBody, signature)` | `WebhookEvent \| null`         | Synchronous. Never throws                                                           |

<Info>
  **`verifyProof` / `safeVerifyProof` are `async` even though verification is
  fully local** (no network call is made) — this matches the convention used
  by other auth SDKs (Clerk, NextAuth, Passport, …), so `await` and `.then()`
  both behave as expected. `verifyWebhook` / `safeVerifyWebhook` remain
  synchronous.
</Info>

<Info>
  **Proofs expire 5 minutes after issuance.** The JWT's `exp` claim is set 5
  minutes after the user confirms in Telegram. Verify a proof promptly after
  receiving it — a late one (slow network, a user idling on a confirmation
  screen, etc.) throws `BondifyVerificationError` with code `TOKEN_EXPIRED`.
  Handle that case explicitly rather than treating it the same as an invalid
  signature:

  ```ts theme={null}
  try {
    const user = await bondify.verifyProof(proof)
  } catch (e) {
    if (e instanceof BondifyVerificationError && e.code === 'TOKEN_EXPIRED') {
      // Prompt the user to sign in again — the 5-minute window has passed.
    } else {
      // Invalid signature, malformed token, or missing claims — reject.
    }
  }
  ```
</Info>

```ts theme={null}
interface BondifyUser {
  telegramId: string
  telegramName: string
  telegramUsername: string | null
  projectId: string
  sessionToken: string
  confirmedAt: number
  exp: number
  iat: number
}

type WebhookEvent =
  | { event: 'auth.confirmed'; session_token: string; telegram_id: string;
      telegram_name: string; telegram_username: string | null; telegram_phone?: string | null; confirmed_at: number }
  | { event: 'auth.cancelled'; session_token: string; cancelled_at: number }
```

<Note>
  `BondifyUser` (returned by `verifyProof`) is camelCase — the same shape
  `@bondify/react` returns on the client. `WebhookEvent` (from `verifyWebhook`)
  stays snake\_case, since it mirrors the raw JSON Bondify sends over the wire
  for webhook deliveries — a deliberate, separate convention from the
  SDK's own ergonomic surface. The old snake\_case type name
  `BondifyProofPayload` is kept as a deprecated alias for `BondifyUser` so
  existing type-only imports keep compiling.
</Note>

### Middleware — `@bondify/node/middleware`

```ts theme={null}
import { createBondifyMiddleware, verifyNextRequest } from '@bondify/node/middleware'
```

* **`createBondifyMiddleware(server, options?)`** → Express `RequestHandler`. Reads the proof from `Authorization: Bearer …`, a custom getter, or the `bondify_proof` cookie, verifies it (`await`s `verifyProof` internally), and sets `req.bondifyUser`. Options: `cookieName`, `headerName`, `tokenGetter`, `onUnauthorized`.

<Note>The cookie fallback reads `req.cookies`, which Express only populates if you've applied the [`cookie-parser`](https://www.npmjs.com/package/cookie-parser) middleware before this one (`app.use(cookieParser())`). Without it, `req.cookies` is `undefined` and cookie-based auth silently falls through to a 401 — if you're only using the `Authorization: Bearer …` header, you don't need it.</Note>

* **`verifyNextRequest(server, request, cookieName?)`** → `Promise<BondifyUser | null>`. **Async — always `await` it.** For Next.js App Router Route Handlers. Reads the proof from the `Authorization: Bearer …` header first, then falls back to the `bondify_proof` cookie (or the custom `cookieName` you pass); never throws — a missing header/cookie, invalid signature, or expired proof (5-minute lifetime) all resolve to `null`, which you should treat as "unauthorized". It's a thin wrapper around `safeVerifyProof`, so it doesn't distinguish *why* verification failed — call `verifyProof` directly if you need that.

### Webhooks — `@bondify/node/webhooks`

```ts theme={null}
import { createWebhookHandler, createNextWebhookHandler } from '@bondify/node/webhooks'
```

Both verify the HMAC signature automatically and dispatch to your handlers:

```ts theme={null}
interface WebhookHandlers {
  onConfirmed?: (event: WebhookEventConfirmed) => void | Promise<void>
  onCancelled?: (event: WebhookEventCancelled) => void | Promise<void>
  onError?:     (error: BondifyWebhookError, raw: string) => void
}
```

See the [Webhooks guide](/guides/webhooks/webhooks).

### Admin client

```ts theme={null}
import { BondifyAdminClient } from '@bondify/node'

const admin = new BondifyAdminClient({ token: developerJwt }) // JWT from POST /api/v1/dev/login
await admin.getMe()
await admin.listProjects()
await admin.createProject({ name: 'My App', webhook_url: 'https://…' })
```

The admin client wraps the developer REST API — see the [REST API reference](/sdk-reference/rest-api/rest-api).

### Errors

```ts theme={null}
import { BondifyVerificationError, BondifyWebhookError } from '@bondify/node'
// BondifyVerificationError.code: 'INVALID_SIGNATURE' | 'TOKEN_EXPIRED' | 'INVALID_TOKEN' | 'MISSING_FIELDS'
// BondifyWebhookError.code:      'INVALID_SIGNATURE' | 'MISSING_SIGNATURE' | 'PARSE_ERROR'
```
