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

# SDK Configuration

> Complete reference for Portfolio Connect SDK props, config, events, and framework integration.

This page covers `@cas-parser/connect` v2.x. For the underlying APIs the widget calls, see the [API Reference](/api-reference/introduction).

## Authentication

<Warning>
  Never expose your raw API key to the browser. Mint a short-lived **access token** (`at_*`) on your backend via [`POST /v1/token`](/api-reference/endpoint/access-tokens) and pass that to the widget instead.
</Warning>

The widget accepts either credential — both are sent as the `x-api-key` header. At least one of `accessToken` / `apiKey` is required.

```python theme={null}
# Backend — Python (Flask)
import os, requests
from flask import jsonify

@app.route('/api/casparser/token')
def get_token():
    res = requests.post(
        'https://api.casparser.in/v1/token',
        headers={'x-api-key': os.environ['CASPARSER_API_KEY']},
        json={'expiry_minutes': 30},
    )
    return jsonify(res.json())
```

Access tokens:

* Prefix: `at_`
* TTL: 1–60 minutes (default: 60)
* Drop-in replacement for `x-api-key` on `/v4/*` endpoints
* Cannot mint other tokens

***

## Props

| Prop          | Type                                             | Required         | Description                                                                                                                                   |
| ------------- | ------------------------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `accessToken` | `string`                                         | One of these two | Short-lived token from `POST /v1/token`. Recommended for browser embeds.                                                                      |
| `apiKey`      | `string`                                         | One of these two | Raw API key. For server-side or trusted-environment use only.                                                                                 |
| `apiBaseUrl`  | `string`                                         | No               | Override API base URL (for dedicated / self-hosted instances). Defaults to `https://api.casparser.in`.                                        |
| `config`      | `PortfolioConnectConfig`                         | No               | Feature flags, branding, prefill, theme, etc. See [Config](#config).                                                                          |
| `onSuccess`   | `(data, metadata) => void`                       | **Yes**          | Called when an import completes successfully.                                                                                                 |
| `onError`     | `(error) => void`                                | No               | Called on any structured error.                                                                                                               |
| `onExit`      | `() => void`                                     | No               | Called when the widget closes (regardless of success/cancel).                                                                                 |
| `onEvent`     | `(event, metadata) => void`                      | No               | Fires for every widget event. Useful for analytics.                                                                                           |
| `onSubmit`    | `(input, password, onProgress?) => Promise<any>` | No               | **v2.1+** — replaces the default parse API call across all intercept-capable flows. See [`onSubmit`](#onsubmit-collect-pdfs-without-parsing). |
| `children`    | `(renderProps) => ReactNode`                     | **Yes** (React)  | Render-prop. Receives `{ open, isReady, isOpen }`.                                                                                            |

***

## Config

```typescript theme={null}
interface PortfolioConnectConfig {
  // Import methods
  enableGenerator?: boolean;     // MF statement via email (default: false)
  enableCdslFetch?: boolean;     // CDSL OTP fetch (default: false)
  enableInbox?: boolean;         // Gmail OAuth import (default: false)
  enableInboundEmail?: boolean;  // Forward-email flow (default: false)

  // Restrict portfolio types surfaced in the UI
  allowedTypes?: ('CAMS_KFINTECH' | 'CDSL' | 'NSDL')[];

  // Home-screen layout
  homeLayout?: 'actions' | 'asset-type' | 'unified';
  // 'actions'    (default) — three action cards: File / Email / Fetch
  // 'asset-type' — two tiles: Mutual Funds / Stocks & Demat (advisor-friendly)
  // 'unified'    — single drop zone with secondary chips below

  // Demat broker picker
  showBrokerPicker?: boolean;    // default: true
  brokers?: BrokerInfo[];        // override the default broker list
  showShortcuts?: boolean;       // email-search shortcuts (default: true)
  showPortalLinks?: boolean;     // links to CAS generation portals (default: true)

  // Pre-fill user details (used across CDSL OTP, MF email, inbound forms)
  prefill?: {
    pan?: string;
    email?: string;
    phone?: string;
    boId?: string;               // CDSL BO ID (16 digits)
    dob?: string;                // YYYY-MM-DD
  };

  // Sub-flow configs
  generator?: {
    fromDate?: string;           // YYYY-MM-DD (default: 2000-01-01)
    toDate?: string;             // YYYY-MM-DD (default: today)
    password?: string;           // PDF password (default: 'Abcdefghi12$')
  };
  inbox?: {
    redirectUri: string;         // REQUIRED — OAuth callback (must call SDK's handleInboxCallback)
    casTypes?: ('cdsl' | 'nsdl' | 'cams' | 'kfintech')[];
    startDate?: string;          // YYYY-MM-DD (default: 30 days ago)
    endDate?: string;            // YYYY-MM-DD (default: today)
  };
  inboundEmail?: {
    callbackUrl?: string;        // optional webhook; SDK still receives the file in-widget
    existingId?: string;         // inject an existing inbound email — SDK won't create or delete
    email?: string;              // pair with existingId to skip a network call
    allowedSources?: ('cdsl' | 'nsdl' | 'cams' | 'kfintech')[];
    reference?: string;          // your internal user/account ID
    metadata?: Record<string, string>;  // max 10 keys
    pollIntervalMs?: number;     // default: 3000 (3s)
    sessionTimeoutMs?: number;   // default: 1800000 (30 min, matches backend TTL)
  };

  // Branding
  logoUrl?: string;
  title?: string;                // default: "Import Your Investments"
  subtitle?: string;              // default: "Mutual Funds, Stocks, Bonds — all in one place"

  // Theme — emits CSS variables on the modal root
  theme?: {
    mode?: 'light' | 'dark' | 'auto';
    primary?: string;
    primaryHover?: string;
    primaryForeground?: string;
    accent?: string;
    radius?: number;
    fontFamily?: string;
  };

  // Success screen
  successBehavior?: 'summary' | 'close';  // default: 'close'
  successAutoCloseMs?: number;            // default: 10000 (10s) — used when behavior === 'summary'
  successCta?: {
    label: string;
    onClick: () => void;
  };

  // Misc
  hideFooter?: boolean;          // default: false. Please keep visible unless co-branded.
}
```

### Inbound email config

When `enableInboundEmail: true`, the widget creates a one-time forwarding address via `POST /v4/inbound-email`, polls `GET /files`, and parses the first file it receives. This works without a `callback_url`.

You can also pass an `existingId` (and optionally `email`) to inject an inbound email you've already created on your backend — useful when you want a stable address per user, or when you're driving the API yourself and only need the SDK for the polling/UI.

The `callbackUrl` here is the underlying API's `callback_url` — set it if you want **both** webhook delivery to your backend **and** in-widget reception. Omit for a pure-frontend integration.

***

## Events

### `onSuccess`

```typescript theme={null}
onSuccess: (data: ParsedData, metadata: ParseMetadata) => void;

interface ParsedData {
  cas_type: 'CAMS_KFINTECH' | 'CDSL' | 'NSDL';
  status: 'success' | 'failed';
  investor_info?: { name: string; email?: string; mobile?: string; pan?: string; address?: string };
  folios?: any[];
  holdings?: any[];
  summary?: { total_value: number; as_on_date: string };
  raw_response?: any;            // full API response
}

interface ParseMetadata {
  filename: string;
  parser_type: 'CAMS_KFINTECH' | 'CDSL' | 'NSDL';
  parse_duration_ms: number;
}
```

Fires once per successful import. The signature is **two arguments**, not a single payload object.

### `onError`

```typescript theme={null}
onError: (error: PortfolioConnectError) => void;

interface PortfolioConnectError {
  code: string;                  // see codes below; type widened to string for forward compat
  message: string;
  title?: string;                // short headline suitable for a banner
  remediation?: string;          // what the user can try next
  retryable?: boolean;
  details?: any;                 // raw API payload when available
}
```

Error codes the SDK emits:

| Code                  | When it fires                                       |
| --------------------- | --------------------------------------------------- |
| `INVALID_PASSWORD`    | API response message explicitly mentions "password" |
| `AUTHENTICATION`      | HTTP 401/403                                        |
| `RATE_LIMITED`        | HTTP 429                                            |
| `NETWORK`             | Network failure / timeout                           |
| `PARSE_ERROR`         | `/v4/*/parse` endpoint failed                       |
| `GENERATOR_ERROR`     | KFintech mailback failed                            |
| `CDSL_FETCH_ERROR`    | CDSL OTP / fetch flow failed                        |
| `INBOX_ERROR`         | Gmail inbox flow failed                             |
| `INBOUND_EMAIL_ERROR` | Inbound email provisioning / polling failed         |
| `UNKNOWN`             | Catch-all                                           |

The type is widened to `| string`, so future codes won't break exhaustive switches.

### `onExit`

```typescript theme={null}
onExit: () => void;
```

Fires whenever the widget closes — after success, after error, or on user cancel. No argument is passed; combine with state set in `onSuccess` / `onError` if you need to distinguish.

### `onEvent`

```typescript theme={null}
onEvent: (event: PortfolioConnectEvent | string, metadata: EventMetadata) => void;
```

Fires for every widget event. Useful for analytics integrations. Selected events:

* **Lifecycle**: `WIDGET_OPENED`, `WIDGET_CLOSED`, `MODE_SWITCHED`, `ASSET_SELECTED`, `BROKER_SELECTED`
* **Upload**: `FILE_SELECTED`, `FILE_REMOVED`, `UPLOAD_STARTED`, `UPLOAD_PROGRESS`, `PARSE_STARTED`, `PARSE_SUCCESS`, `PARSE_ERROR`
* **Generator**: `GENERATOR_STARTED`, `GENERATOR_SUCCESS`, `GENERATOR_ERROR`
* **CDSL fetch**: `CDSL_FETCH_STARTED`, `CDSL_OTP_SENT`, `CDSL_OTP_VERIFIED`, `CDSL_FETCH_SUCCESS`, `CDSL_FETCH_ERROR`
* **Gmail inbox**: `INBOX_CONNECT_STARTED`, `INBOX_CONNECTED`, `INBOX_FILES_LOADED`, `INBOX_FILE_SELECTED`, `INBOX_DISCONNECTED`, `INBOX_ERROR`
* **Inbound email**: `INBOUND_EMAIL_CREATED`, `INBOUND_EMAIL_COPIED`, `INBOUND_EMAIL_POLLING`, `INBOUND_EMAIL_FILE_RECEIVED`, `INBOUND_EMAIL_TIMEOUT`, `INBOUND_EMAIL_ERROR`
* **Cross-flow handoffs** — measure how often the widget's nudges convert: `GENERATOR_TO_INBOUND_HANDOFF`, `INBOX_TO_GENERATOR_HANDOFF`, `INBOUND_TO_UPLOAD_HANDOFF`, `UPLOAD_TO_INBOUND_HANDOFF`

The `metadata` object always includes `timestamp` and may include event-specific fields.

***

## `onSubmit` — collect PDFs without parsing

**v2.1+.** When `onSubmit` is provided, the SDK routes every intercept-capable flow through it instead of calling the parse API itself. Useful for:

* Forwarding PDFs to your own backend without burning parse credits
* Custom retry / queueing logic
* Storing a copy of the PDF before parsing

```typescript theme={null}
type SubmitInput =
  | { kind: 'file'; file: File; filename: string; source: 'UPLOAD' }
  | {
      kind: 'url';
      pdfUrl: string;            // presigned, valid 24–48h
      filename: string;
      source: 'INBOX' | 'INBOUND_EMAIL' | 'CDSL_FETCH';
      metadata?: Record<string, unknown>;  // cas_type, message_id, received_at, etc.
    };

onSubmit={async (input, password, onProgress) => {
  if (input.kind === 'file') {
    // User uploaded the PDF directly
    const form = new FormData();
    form.append('file', input.file);
    form.append('password', password);
    return fetch('/api/portfolio/upload', { method: 'POST', body: form }).then(r => r.json());
  }

  // input.kind === 'url' — fetched from inbox / inbound email / CDSL
  return fetch('/api/portfolio/from-url', {
    method: 'POST',
    body: JSON.stringify({
      pdfUrl: input.pdfUrl,
      password,
      source: input.source,
    }),
  }).then(r => r.json());
}}
```

The MF generator (KFintech mailback) flow stays out of scope — the CAS is delivered to the investor's email out of band, so the SDK never sees a PDF.

The return value is forwarded to `onSuccess`. Throw to trigger `onError`.

***

## Framework integration

### React / Next.js

```tsx theme={null}
import { PortfolioConnect } from '@cas-parser/connect';

function ImportButton({ accessToken }: { accessToken: string }) {
  return (
    <PortfolioConnect
      accessToken={accessToken}
      config={{
        enableCdslFetch: true,
        enableInboundEmail: true,
        homeLayout: 'asset-type',
        theme: { primary: '#10B981', mode: 'auto' },
      }}
      onSuccess={(data, metadata) => {
        console.log(`Total: ₹${data.summary?.total_value} (${metadata.parser_type})`);
      }}
      onError={(err) => console.error(err.code, err.message)}
      onExit={() => console.log('widget closed')}
    >
      {({ open, isReady }) => (
        <button onClick={open} disabled={!isReady}>
          Import Portfolio
        </button>
      )}
    </PortfolioConnect>
  );
}
```

For **Next.js App Router**, mark the parent component `'use client'` and fetch the access token from a server route.

### Vanilla JS / Angular / Vue (Imperative API)

The standalone bundle ships its own React copy — no host-page React required.

```html theme={null}
<script src="https://unpkg.com/@cas-parser/connect/dist/portfolio-connect.standalone.min.js"></script>

<button id="import-btn">Import Portfolio</button>

<script>
  document.getElementById('import-btn').onclick = async () => {
    // open() resolves with a discriminated result — never rejects on close.
    const result = await PortfolioConnect.open({
      accessToken: 'at_xxxxx',
      config: { enableCdslFetch: true },
    });

    if (result.status === 'success') {
      console.log('Portfolio:', result.data);
    } else if (result.status === 'closed') {
      console.log('User cancelled');
    } else {
      console.error('Error:', result.error);
    }
  };
</script>
```

If your page already has React 18+, use the lighter UMD bundle:

```html theme={null}
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@cas-parser/connect/dist/portfolio-connect.umd.min.js"></script>
```

**Bundle sizes (gzipped):**

* Standalone: \~71 KB (includes React)
* UMD: \~27 KB (requires React 18+ on the page)

The imperative `open()` accepts the same `accessToken` / `apiKey` / `apiBaseUrl` / `config` / `onSubmit` / `onEvent` as the React props, plus an optional `onOpen` lifecycle hook. The success / error / cancel callbacks are intentionally dropped — the returned `OpenResult` covers all three outcomes.

If you need manual control (open the widget later, dispose without opening), use `PortfolioConnect.create(config)` which returns a `{ open, destroy }` handle.

### React Native / Flutter

Both are supported via WebView. See the [`@cas-parser/connect` examples](https://github.com/CASParser/cas-connect/tree/main/examples) for working setups.

***

## Theming

The recommended way to brand the widget is the `config.theme` object — it emits CSS variables on the modal root and derives the full palette (hover, soft, surface, border, text) from your primary colour automatically.

```tsx theme={null}
config={{
  theme: {
    mode: 'auto',           // 'light' | 'dark' | 'auto' (follows prefers-color-scheme)
    primary: '#10B981',
    primaryHover: '#059669', // optional — derived if omitted
    primaryForeground: '#FFFFFF',
    accent: '#F59E0B',
    radius: 12,             // base radius in px; sm/md/lg/xl/pill are derived
    fontFamily: 'Inter, system-ui, sans-serif',
  },
}}
```

### CSS variables

If you need to override individual tokens beyond what `theme` exposes, set the CSS variables directly. The full list emitted on the modal root:

```css theme={null}
/* Primary palette */
--pc-color-primary
--pc-color-primary-hover
--pc-color-primary-foreground
--pc-color-primary-soft        /* low-alpha tint of primary */
--pc-color-primary-soft-border
--pc-color-accent

/* Surfaces & borders (driven by mode) */
--pc-color-surface, --pc-color-surface-subtle, --pc-color-surface-muted, --pc-color-surface-strong
--pc-color-border, --pc-color-border-strong

/* Text */
--pc-color-text, --pc-color-text-muted, --pc-color-text-faded

/* Status */
--pc-color-success, --pc-color-success-bg, --pc-color-success-border
--pc-color-error,   --pc-color-error-bg,   --pc-color-error-border
--pc-color-warning, --pc-color-warning-bg, --pc-color-warning-border

/* Geometry & font */
--pc-radius-sm, --pc-radius-md, --pc-radius-lg, --pc-radius-xl, --pc-radius-pill
--pc-font-family
--pc-shadow-card, --pc-shadow-button
```

`theme.mode: 'auto'` follows the user's `prefers-color-scheme` media query.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Inbound Email Guide" icon="envelope" href="/guides/inbound-email">
    Forward-email flow used by `enableInboundEmail`
  </Card>

  <Card title="API Reference" icon="book" href="/api-reference/introduction">
    Endpoints the widget calls under the hood
  </Card>
</CardGroup>
