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

# useChipiSession

> Unified session key management for gasless UX - create, register, execute, and revoke sessions with a single hook.

## Overview

`useChipiSession` provides a unified API for managing session keys, enabling gasless transactions without requiring the owner's signature for each operation.

This hook combines all session-related operations:

* **Session creation** (local keypair generation)
* **Session registration** (on-chain)
* **Transaction execution** (using session key)
* **Session revocation** (on-chain)
* **Status checking** (remaining calls, expiration)

> [!WARNING]
>
> Session keys work with **CHIPI** wallets and with **SHHH** wallets whose signer is STARK or ED25519 (every SHHH wallet the SDK creates, including passkey wallets; SHHH needs 14.14.0 or later in this hook). READY wallets do not support session keys. Check `supportsSession` before offering sessions.

## Prerequisites

Before using `useChipiSession`, you need:

1. A wallet that supports sessions: CHIPI, or SHHH with a STARK / ED25519 signer (check `supportsSession`, `wallet.supportsSessionKeys` or `walletSupportsSessions(wallet)`)
2. The user's **encryption key**: a PIN (`encryptKey`) or a function that resolves it on demand, such as a passkey (`getEncryptKey`, 14.14.0+)
3. An **authentication token** (from Clerk, Firebase, etc.)

## Session Lifecycle

```mermaid theme={null}
stateDiagram-v2
    [*] --> none: No session
    none --> created: createSession()
    created --> active: registerSession()
    active --> active: executeWithSession()
    active --> revoked: revokeSession()
    active --> expired: Time/calls exhausted
```

| State | Description |
| - | - |
| `none` | No session exists |
| `created` | Session created locally, not yet registered on-chain |
| `active` | Session registered and usable |
| `expired` | Session has expired (time or call limit) |
| `revoked` | Session has been revoked on-chain |

## Quick Start

```tsx theme={null}
import { useAuth } from "@clerk/nextjs";
import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs";

function SessionComponent() {
  const { userId, getToken } = useAuth();
  const [pin, setPin] = useState("");
  
  const { wallet } = useChipiWallet({
    externalUserId: userId,
    getBearerToken: getToken,
  });
  
  const {
    session,
    sessionState,
    hasActiveSession,
    remainingCalls,
    createSession,
    registerSession,
    executeWithSession,
    isCreating,
    isRegistering,
    isExecuting,
  } = useChipiSession({
    wallet,
    encryptKey: pin,
    getBearerToken: getToken,
  });

  // Setup session (create + register)
  const handleSetup = async () => {
    await createSession();
    await registerSession();
  };

  // Execute a transfer using the session
  const handleTransfer = async () => {
    await executeWithSession([{
      contractAddress: "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb",
      entrypoint: "transfer",
      calldata: [recipientAddress, amount, "0x0"],
    }]);
  };

  return (
    <div>
      <p>Session: {sessionState}</p>
      {hasActiveSession && <p>Calls remaining: {remainingCalls}</p>}
      
      {!hasActiveSession ? (
        <button onClick={handleSetup} disabled={isCreating || isRegistering}>
          Setup Session
        </button>
      ) : (
        <button onClick={handleTransfer} disabled={isExecuting}>
          Transfer (Gasless)
        </button>
      )}
    </div>
  );
}
```

## Passkey wallets and server-held sessions (14.14.0+)

For an agent that runs on your server, create the session key there and register it from the browser with the user's passkey. The session and its spending caps go in **one** owner-signed transaction, so the user sees one prompt:

```tsx theme={null}
const { registerSession, revokeSession } = useChipiSession({
  wallet,
  // credentialId and prfSupported come from the wallet row (useChipiWallet /
  // getWallet), so this works on any device the passkey synced to. With no
  // argument, getWalletEncryptKey() reads localStorage and fails on a new device.
  getEncryptKey: () =>
    getWalletEncryptKey(wallet?.credentialId ?? undefined, {
      prfSupported: wallet?.prfSupported ?? undefined,
    }),
  getBearerToken: () => getToken({ skipCache: true }),
});

// sessionPublicKey and validUntil come from your server (sdk.sessions.createSessionKey)
await registerSession({
  sessionPublicKey,
  validUntil,
  maxCalls: 500,
  allowedEntrypoints: ["transfer", "approve"],
  spendingPolicies: [
    { token: USDC, maxPerCall: 5_000_000n, maxPerWindow: 20_000_000n, windowSeconds: 86_400 },
  ],
});

// Later, to stop it:
await revokeSession(sessionPublicKey);
```

Function names in `allowedEntrypoints` are converted to selectors. The whole flow, server side included, is in [Agent with a session key and a passkey](/sdk/guides/agent-sessions).

## Configuration Options

```typescript theme={null}
interface UseChipiSessionConfig {
  // Required
  wallet: SessionWallet | null | undefined;
  getBearerToken: () => Promise<string | null | undefined>;
  // One of these two
  encryptKey?: string;                                        // a PIN, or a key you already hold
  getEncryptKey?: () => Promise<string | null | undefined>;  // e.g. a passkey (14.14.0+)
  
  // Optional
  storedSession?: SessionKeyData | null;
  onSessionCreated?: (session: SessionKeyData) => void | Promise<void>;
  defaultDurationSeconds?: number;  // Default: 21600 (6 hours)
  defaultMaxCalls?: number;         // Default: 1000
  autoCheckStatus?: boolean;        // Default: true
}
```

| Option | Type | Default | Description |
| - | - | - | - |
| `wallet` | `SessionWallet` | Required | CHIPI, or SHHH with a STARK / ED25519 signer |
| `encryptKey` | `string` | - | PIN (or a key you already hold). Set this or `getEncryptKey` |
| `getEncryptKey` | `() => Promise<string>` | - | Resolves the key only when an action runs, e.g. `() => getWalletEncryptKey(wallet.credentialId ?? undefined, { prfSupported: wallet.prfSupported ?? undefined })` (pass the wallet row's values: with no argument it relies on `localStorage` and fails on a new device). Called **before** `getBearerToken`, so the token is fresh after the passkey prompt. Every action resolves it again, so `executeWithSession` prompts too; execute server-held sessions on your server for prompt-free runs. Ignored when `encryptKey` is set (14.14.0+) |
| `getBearerToken` | `() => Promise<string>` | Required | Auth token function |
| `storedSession` | `SessionKeyData` | `null` | Previously stored session |
| `onSessionCreated` | `(session) => void` | - | Callback to persist new session |
| `defaultDurationSeconds` | `number` | `21600` | Session duration (6 hours) |
| `defaultMaxCalls` | `number` | `1000` | Max calls per session |
| `autoCheckStatus` | `boolean` | `true` | Auto-fetch on-chain status |

## Return Values

### Session Data

| Property | Type | Description |
| - | - | - |
| `session` | `SessionKeyData \| null` | Current session data |
| `sessionStatus` | `SessionDataResponse` | On-chain session status |
| `sessionState` | `SessionState` | Lifecycle state |
| `hasActiveSession` | `boolean` | Whether session is usable |
| `isSessionExpired` | `boolean` | Whether session has expired |
| `remainingCalls` | `number \| undefined` | Calls left in session |
| `supportsSession` | `boolean` | Whether wallet supports sessions |

### Actions

| Method | Returns | Description |
| - | - | - |
| `createSession(config?)` | `Promise<SessionKeyData>` | Create new session locally |
| `registerSession(config?)` | `Promise<string>` | Register on-chain (returns txHash). Pass `sessionPublicKey` + `validUntil` to register a key created elsewhere (e.g. on your server), and `spendingPolicies` to set its caps in the same transaction (14.14.0+) |
| `revokeSession(sessionPublicKey?)` | `Promise<string>` | Revoke on-chain (returns txHash). Defaults to the current session; pass a key to revoke one created elsewhere (14.14.0+) |
| `executeWithSession(calls)` | `Promise<string>` | Execute calls (returns txHash) |
| `clearSession()` | `void` | Clear local session state |
| `refetchStatus()` | `Promise<void>` | Refresh on-chain status |

### Loading States

| Property | Description |
| - | - |
| `isCreating` | Creating session locally |
| `isRegistering` | Registering session on-chain |
| `isRevoking` | Revoking session on-chain |
| `isExecuting` | Executing transaction |
| `isLoadingStatus` | Fetching on-chain status |

## Persisting Sessions

`SessionKeyData` contains an `encryptedPrivateKey`. Where you store it matters — the SDK does not persist sessions itself; this is your responsibility.

> [!WARNING]
>
> **Do not store `SessionKeyData` in Clerk `unsafeMetadata`.** `unsafeMetadata` is client-writable and embedded in the Clerk session JWT, which means the encrypted session key is exposed to the browser on every session refresh. Combined with a low-entropy user PIN (e.g. 6 digits = 10⁶ combinations), the ciphertext is brute-forceable offline by anyone who obtains a valid session token.
>
> Use a server-side route that writes to Clerk **`privateMetadata`** (server-only, not in the JWT), or store the session in your own database. See the recommended pattern below.

### Recommended: server-side route + `privateMetadata`

Persist `SessionKeyData` from a server route after verifying the user's auth token. Chipi's backend SDK ships JWKS verification (`iss` + `aud` validation) for Clerk, Firebase, BetterAuth, and generic providers — use it to authenticate the route. See the [Gasless setup guides](/sdk/nextjs/gasless-clerk-setup) for JWKS configuration.

**1. Server route — `app/api/chipi/session/route.ts`:**

```ts theme={null}
import { auth, clerkClient } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import type { SessionKeyData } from "@chipi-stack/types";

// GET — return the stored session (server-only, never exposed in JWT)
export async function GET() {
  const { userId } = await auth();
  if (!userId) return NextResponse.json({ error: "unauthorized" }, { status: 401 });

  const client = await clerkClient();
  const user = await client.users.getUser(userId);
  const session = user.privateMetadata?.chipiSession as SessionKeyData | undefined;
  return NextResponse.json({ session: session ?? null });
}

// POST — persist a newly created session
export async function POST(req: Request) {
  const { userId } = await auth();
  if (!userId) return NextResponse.json({ error: "unauthorized" }, { status: 401 });

  const body = await req.json().catch(() => null);
  if (!body || typeof body !== "object" || !("session" in body)) {
    return NextResponse.json({ error: "invalid_session_payload" }, { status: 400 });
  }
  const { session } = body as { session: SessionKeyData };

  // updateUserMetadata performs a deep merge — using updateUser() here would
  // replace the entire privateMetadata object and clobber any other keys.
  const client = await clerkClient();
  await client.users.updateUserMetadata(userId, {
    privateMetadata: { chipiSession: session },
  });
  return NextResponse.json({ ok: true });
}
```

**2. Client — load and persist via the route:**

```tsx theme={null}
"use client";

import { useEffect, useState } from "react";
import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs";
import type { SessionKeyData } from "@chipi-stack/types";

function PersistentSession() {
  const { wallet } = useChipiWallet({ ... });
  const [storedSession, setStoredSession] = useState<SessionKeyData | null>(null);

  // Hydrate from server on mount
  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch("/api/chipi/session");
        if (!r.ok) return;
        const d = (await r.json()) as { session?: SessionKeyData | null };
        if (!cancelled) setStoredSession(d.session ?? null);
      } catch {
        if (!cancelled) setStoredSession(null);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, []);

  const { session, hasActiveSession, createSession, registerSession } = useChipiSession({
    wallet,
    encryptKey: pin,
    getBearerToken: getToken,
    storedSession,
    onSessionCreated: async (newSession) => {
      const r = await fetch("/api/chipi/session", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ session: newSession }),
      });
      if (!r.ok) throw new Error(`Failed to persist session: ${r.status}`);
    },
  });

  // Session is restored on mount — and never leaks via the JWT
}
```

### Reduce the brute-force surface

Even with server-side storage, the encryption strength of `encryptedPrivateKey` depends on the encryption key:

* **PIN-only wallets** — a 6-digit PIN has \~20 bits of entropy; an attacker who obtains the ciphertext can crack it offline in seconds. Treat the encrypted session key as sensitive even at rest.
* **Passkey wallets (recommended)** — the encryption key is hardware-backed via WebAuthn PRF and never leaves the device's secure element. This is the production-grade default. See the [PIN → passkey migration guide](/sdk/guides/pin-to-passkey-migration).

### Minimal session whitelist

When registering a session, restrict `allowedEntrypoints` to the smallest set your flow actually needs. Avoid including broad approval entrypoints (`approve`, `set_approval_for_all`) unless required — a session with these permissions can drain tokens within the validity window if compromised.

## Executing Transactions

The `executeWithSession` method accepts an array of Starknet calls:

```typescript theme={null}
// Single transfer
await executeWithSession([{
  contractAddress: USDC_CONTRACT,
  entrypoint: "transfer",
  calldata: [recipient, amount, "0x0"],
}]);

// Batch multiple calls
await executeWithSession([
  {
    contractAddress: USDC_CONTRACT,
    entrypoint: "approve",
    calldata: [spender, amount, "0x0"],
  },
  {
    contractAddress: DEX_CONTRACT,
    entrypoint: "swap",
    calldata: [...],
  },
]);
```

## Custom Session Configuration

### Custom Duration

```typescript theme={null}
// Create session valid for 1 hour
await createSession({ durationSeconds: 3600 });
```

### Custom Max Calls

```typescript theme={null}
// Register with 500 max calls
await registerSession({ maxCalls: 500 });
```

### Allowed Entrypoints

Restrict which contract methods the session can call:

```typescript theme={null}
await registerSession({
  allowedEntrypoints: [
    "0x..." // Only allow specific function selectors
  ],
});
```

## Error Handling

```tsx theme={null}
const { error, session } = useChipiSession({...});

// Check for errors
if (error) {
  console.error("Session error:", error.message);
}

// Handle specific errors
const handleSetup = async () => {
  try {
    await createSession();
    await registerSession();
  } catch (err) {
    if (err.message.includes("does not support sessions")) {
      alert("Please use a CHIPI wallet for sessions");
    } else if (err.message.includes("expired")) {
      alert("Session has expired, please create a new one");
    } else {
      alert(`Error: ${err.message}`);
    }
  }
};
```

## Complete Example

```tsx theme={null}
"use client";

import { useState, useEffect } from "react";
import { useAuth } from "@clerk/nextjs";
import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs";
import type { SessionKeyData } from "@chipi-stack/types";

export function GaslessTransfers() {
  const { getToken, userId } = useAuth();
  const [pin, setPin] = useState("");
  const [recipient, setRecipient] = useState("");
  const [amount, setAmount] = useState("");

  const { wallet, formattedBalance } = useChipiWallet({
    externalUserId: userId,
    getBearerToken: getToken,
  });

  // Hydrate session from a server-side route that reads Clerk privateMetadata
  // (NOT unsafeMetadata — see the "Persisting Sessions" section above).
  const [storedSession, setStoredSession] = useState<SessionKeyData | null>(null);
  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch("/api/chipi/session");
        if (!r.ok) return;
        const d = (await r.json()) as { session?: SessionKeyData | null };
        if (!cancelled) setStoredSession(d.session ?? null);
      } catch {
        if (!cancelled) setStoredSession(null);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, []);

  const {
    session,
    sessionState,
    hasActiveSession,
    remainingCalls,
    supportsSession,
    createSession,
    registerSession,
    executeWithSession,
    revokeSession,
    isCreating,
    isRegistering,
    isExecuting,
    isRevoking,
    error,
  } = useChipiSession({
    wallet,
    encryptKey: pin,
    getBearerToken: getToken,
    storedSession,
    onSessionCreated: async (newSession) => {
      const r = await fetch("/api/chipi/session", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ session: newSession }),
      });
      if (!r.ok) throw new Error(`Failed to persist session: ${r.status}`);
    },
  });

  if (!wallet?.supportsSessionKeys) {
    return <p>Sessions require a CHIPI wallet</p>;
  }

  return (
    <div className="space-y-6">
      {/* PIN Input */}
      <input
        type="password"
        placeholder="Enter PIN"
        value={pin}
        onChange={(e) => setPin(e.target.value)}
        maxLength={4}
      />

      {/* Session Status */}
      <div className="p-4 bg-gray-100 rounded">
        <p>State: <strong>{sessionState}</strong></p>
        {hasActiveSession && (
          <p>Remaining calls: {remainingCalls}</p>
        )}
      </div>

      {/* Session Setup */}
      {!hasActiveSession && (
        <button
          onClick={async () => {
            await createSession();
            await registerSession();
          }}
          disabled={isCreating || isRegistering || !pin}
        >
          {isCreating ? "Creating..." : isRegistering ? "Registering..." : "Setup Session"}
        </button>
      )}

      {/* Transfer Form */}
      {hasActiveSession && (
        <div className="space-y-4">
          <input
            placeholder="Recipient address"
            value={recipient}
            onChange={(e) => setRecipient(e.target.value)}
          />
          <input
            type="number"
            placeholder="Amount"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
          />
          <button
            onClick={async () => {
              await executeWithSession([{
                contractAddress: "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb",
                entrypoint: "transfer",
                calldata: [recipient, amount, "0x0"],
              }]);
            }}
            disabled={isExecuting}
          >
            {isExecuting ? "Sending..." : "Send USDC (Gasless)"}
          </button>
        </div>
      )}

      {/* Revoke Session */}
      {hasActiveSession && (
        <button
          onClick={revokeSession}
          disabled={isRevoking}
          className="text-red-600"
        >
          {isRevoking ? "Revoking..." : "Revoke Session"}
        </button>
      )}

      {/* Error Display */}
      {error && (
        <p className="text-red-500">{error.message}</p>
      )}
    </div>
  );
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.