JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 139
  • Score
    100M100P100Q87213F
  • License MIT

Portfolio Connect - Lightweight widget for importing Indian investment statements (MF, CDSL, NSDL). Works with React, Angular, Vue, and vanilla JS.

Package Exports

  • @cas-parser/connect
  • @cas-parser/connect/package.json

Readme

Portfolio Connect SDK

Drop-in widget for importing Indian investment statements (Mutual Funds, CDSL, NSDL).

npm version License: MIT

Quick Start

  1. Get an access token from casparser.in/docs.
  2. Install the SDK or load the standalone bundle from a CDN.
  3. Drop in the widget — it ships ready to import mutual fund CAS, CDSL, and NSDL statements.

Install (npm / yarn)

npm install @cas-parser/connect
# or
yarn add @cas-parser/connect

React

import { PortfolioConnect } from '@cas-parser/connect';

function App() {
  return (
    <PortfolioConnect
      accessToken="your_access_token"
      onSuccess={(data) => console.log('Portfolio:', data)}
    >
      {({ open }) => (
        <button onClick={open}>Import Portfolio</button>
      )}
    </PortfolioConnect>
  );
}

Vanilla JS / Angular / Vue (Imperative API)

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

<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: 'your_access_token',
      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 instead:

<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)

A full vanilla-HTML demo lives in examples/vanilla-html/index.html.

Configuration

<PortfolioConnect
  accessToken="your_access_token"
  config={{
    // Branding
    logoUrl: 'https://yourapp.com/logo.png',
    title: 'Import Your Investments',
    subtitle: 'Mutual Funds, Stocks, Bonds — all in one place',

    // Home-screen layout — pick the preset that matches your audience.
    // 'actions' (default) | 'asset-type' (advisor-friendly) | 'unified' (upload-first)
    homeLayout: 'actions',

    // Features
    enableGenerator: true,     // MF statement via email
    enableCdslFetch: true,     // CDSL statement via OTP
    enableInbox: true,         // Gmail OAuth import
    enableInboundEmail: false, // Forward-email flow (unique inbound address)

    // Restrict portfolio types
    allowedTypes: ['CAMS_KFINTECH', 'CDSL', 'NSDL'],

    // Pre-fill user details (used across CDSL OTP, MF email, inbound forms)
    prefill: {
      pan: 'ABCDE1234F',
      email: 'user@example.com',
      boId: '1234567890123456',  // CDSL BO ID
      dob: '1990-01-15',         // CDSL DOB
    },

    // MF-statement-by-email options
    generator: {
      fromDate: '2020-01-01',
      toDate: '2024-12-31',
      password: 'Abcdefghi12$',  // PDF encryption password (defaults to this)
    },

    // Gmail inbox import — required when enableInbox is on
    inbox: {
      redirectUri: 'https://your-app.com/oauth/callback',
    },

    // UI options
    showShortcuts: true,    // Email search shortcuts (Gmail / Outlook / Yahoo)
    showPortalLinks: true,  // Links to download portals
  }}
  onSuccess={handleSuccess}
  onError={handleError}
  onEvent={(event, metadata) => analytics.track(event, metadata)}
>
  {({ open, isReady }) => (
    <button onClick={open} disabled={!isReady}>
      Import Portfolio
    </button>
  )}
</PortfolioConnect>

Framework Support

Framework Support Method
React ✅ Native npm package
Next.js ✅ Native npm package
Angular ✅ Via CDN UMD bundle
Vue ✅ Via CDN UMD bundle
Vanilla JS ✅ Via CDN Standalone bundle (no React) or UMD bundle
React Native ✅ WebView See examples
Flutter ✅ WebView See examples

See EXAMPLES.md for framework-specific integration guides.

API Reference

Props

Prop Type Required Description
accessToken string One of Short-lived access token (at_*) minted by POST /v1/token from your backend. Recommended for browser embeds. Learn more.
apiKey string One of Raw API key. Equivalent to accessToken; both are sent as x-api-key. Pass one or the other (or either).
apiBaseUrl string No Override the API base URL. Use this for dedicated or self-hosted CASParser instances. Defaults to https://api.casparser.in.
onSuccess (data, metadata) => void Yes Success callback with parsed data. See Response Data.
onError (error: PortfolioConnectError) => void No Error callback with structured code, title, remediation, retryable. See Error handling.
onExit () => void No Widget closed callback
onEvent (event, metadata) => void No Analytics callback. See Events.
onSubmit (input: SubmitInput, password, onProgress?) => Promise<any> No Custom submit handler that replaces the default parse API call across all intercept-capable flows (file upload, Gmail inbox, inbound email, CDSL fetch). input is a discriminated union: { kind: 'file', file, filename, source: 'UPLOAD' } or { kind: 'url', pdfUrl, filename, source, metadata? }. The MF-generator (KFintech mailback) flow is not routed through onSubmit — it sends the CAS to the investor's email out-of-band. onProgress reports 0–100. Useful for "collect-only" flows.
config PortfolioConnectConfig No Configuration options — see below

Config Options

Option Type Default Description
logoUrl string CASParser logo Your brand logo URL
title string "Import Your Investments" Widget title
subtitle string "Mutual Funds, Stocks…" Widget subtitle
homeLayout 'actions' | 'asset-type' | 'unified' 'actions' Home-screen layout variant — see Home Layout Variants
enableGenerator boolean false Enable MF fetch via email
enableCdslFetch boolean false Enable CDSL fetch via OTP
enableInbox boolean false Enable Gmail OAuth import. Requires inbox.redirectUri.
enableInboundEmail boolean false Enable inbound email forwarding (users forward their CAS to a unique one-shot address)
allowedTypes PortfolioType[] All three Restrict to a subset of 'CAMS_KFINTECH' | 'CDSL' | 'NSDL'
showBrokerPicker boolean true Show the 19-tile broker grid (with logos for major Indian brokers) before the demat upload step. Pass false to skip and drop users straight on the upload zone. Only fires on the explicit demat path — asset-agnostic upload buttons go straight to upload regardless.
prefill object - Pre-fill user details: { pan?, email?, phone?, boId?, dob? }
generator object - Generator options: { fromDate?, toDate?, password? }
inbox object - Inbox config: { redirectUri, casTypes?, startDate?, endDate? }. redirectUri is required when enableInbox is on.
inboundEmail object - Inbound-email config: { callbackUrl?, existingId?, email?, allowedSources?, reference?, metadata?, pollIntervalMs?, sessionTimeoutMs? }
brokers BrokerInfo[] Bundled list of 19 Custom broker list (overrides defaults)
theme PortfolioConnectTheme - Theme tokens: { mode?: 'light' | 'dark' | 'auto', primary?, primaryHover?, primaryForeground?, accent?, radius?, fontFamily? }. mode: 'auto' (the default when unset) follows the host app's theme by inspecting the computed background of <body> / <html> (covers Tailwind .dark, shadcn [data-theme], manual style toggles); falls back to OS prefers-color-scheme when the host root is transparent. The widget live-restyles when the host or OS theme changes.
showShortcuts boolean true Show Gmail / Outlook / Yahoo email search shortcuts on the upload screen
showPortalLinks boolean true Show "Download from {portal}" link on the upload screen
successBehavior 'summary' | 'close' 'close' 'close' (default) closes the widget immediately on success and lets the host page take over. 'summary' shows the in-widget <SuccessSummary> screen with totals + auto-close countdown.
successAutoCloseMs number 10000 Summary auto-close delay (ignored when successBehavior: 'close')
successCta { label, onClick } - Primary CTA on the summary screen (e.g. "View portfolio"). No-op when successBehavior: 'close'.
hideFooter boolean false Hide the "Secured by" footer

Home Layout Variants

Pick the preset that matches your audience. All variants share the same hero (logo + title + subtitle) and security footer — only the primary action layout differs.

Variant Best for Primary layout
'actions' (default) General-purpose fintech apps, consumer flows Three action cards: I have a file / Get it from my email / Fetch it for me
'asset-type' Advisors & wealth managers Two tiles: Mutual Funds / Stocks & Demat — Stocks tile routes through the broker picker
'unified' Upload-first flows where the user almost always has the PDF Large drop zone front-and-center, secondary chips below
// Example: advisor-friendly asset-type layout
<PortfolioConnect
  accessToken="your_access_token"
  config={{
    homeLayout: 'asset-type',
    enableCdslFetch: true,
    enableGenerator: true,
  }}
  onSuccess={(data) => console.log(data)}
/>

Events

Event When
WIDGET_OPENED Widget opened
WIDGET_CLOSED Widget closed
MODE_SWITCHED User switched between widget screens or sub-tabs
ASSET_SELECTED User picked an asset tile (asset-type layout). metadata: { asset: 'MUTUAL_FUNDS' | 'DEMAT' }
BROKER_SELECTED User selected a broker. metadata: { broker, depository }
SEARCH_CLICKED User clicked an email search shortcut (Gmail/Outlook/Yahoo)
PORTAL_CLICKED User clicked the "Download from {portal}" link
FILE_SELECTED User selected a file
FILE_REMOVED User removed selected file
UPLOAD_STARTED File upload began
UPLOAD_PROGRESS Upload progress update (during simulated phase)
PARSE_STARTED Parsing started
PARSE_SUCCESS Parsing completed
PARSE_ERROR Parsing failed
GENERATOR_STARTED Mutual fund CAS request started
GENERATOR_SUCCESS Mutual fund CAS request sent
GENERATOR_ERROR Mutual fund CAS request failed
CDSL_FETCH_STARTED CDSL fetch started
CDSL_OTP_SENT CDSL OTP sent
CDSL_OTP_VERIFIED CDSL OTP verified
CDSL_FETCH_SUCCESS CDSL files retrieved
CDSL_FETCH_ERROR CDSL fetch failed
INBOX_CONNECT_STARTED Gmail OAuth flow started
INBOX_CONNECTED Gmail connected successfully
INBOX_FILES_LOADED CAS files found in inbox
INBOX_FILE_SELECTED User selected an inbox file
INBOX_DISCONNECTED Gmail disconnected
INBOX_ERROR Inbox import failed
INBOUND_EMAIL_CREATED Unique forwarding address created
INBOUND_EMAIL_COPIED User copied the forwarding address
INBOUND_EMAIL_POLLING Widget started polling for forwarded files
INBOUND_EMAIL_FILE_RECEIVED Forwarded CAS received
INBOUND_EMAIL_TIMEOUT Polling session timed out before a file arrived
INBOUND_EMAIL_ERROR Inbound email error
Cross-flow handoffs — fired when the SDK guides the user from one import method to another. Useful for measuring how often the nudges convert.
GENERATOR_TO_INBOUND_HANDOFF After requesting a mutual fund CAS by email, user accepted the "auto-import via forwarding" CTA
INBOX_TO_GENERATOR_HANDOFF Empty Gmail inbox → user clicked "Request a fresh mutual fund CAS"
INBOUND_TO_UPLOAD_HANDOFF On the inbound-email waiting screen, user clicked "I already have the file — upload it"
UPLOAD_TO_INBOUND_HANDOFF On the upload screen, user clicked "Got it in your email? Forward via email"

Response Data

The /v4/smart/parse API returns a comprehensive structure covering mutual fund folios, demat accounts (with nested holdings), insurance policies, and NPS. The exact shape:

{
  meta: {
    cas_type: 'CAMS_KFINTECH' | 'CDSL' | 'NSDL';
    generated_at: string;
    statement_period: { from: string; to: string };
  };
  investor: {
    name: string;
    pan: string;
    email?: string;
    mobile?: string;
    address?: string;
    pincode?: string;
  };
  mutual_funds?: Array<{
    folio_number: string;
    amc: string;
    schemes: Array<{ isin: string; name: string; units: number; value: number; /* … */ }>;
    value: number;
  }>;
  demat_accounts?: Array<{
    bo_id: string;
    dp_name: string;
    holdings: {
      equities?: Array<{ isin: string; name: string; units: number; value: number; /* … */ }>;
      demat_mutual_funds?: Array<{ /* same shape */ }>;
      aifs?: Array<unknown>;
      corporate_bonds?: Array<unknown>;
      government_securities?: Array<unknown>;
    };
    value: number;
  }>;
  insurance?: { life_insurance_policies: Array<unknown> };
  nps?: Array<{ pran: string; funds: Array<unknown>; /* … */ }>;
  summary: {
    total_value: number;
    accounts: {
      mutual_funds: { count: number; total_value: number };  // count = scheme count
      demat:        { count: number; total_value: number };  // count = account count
      insurance:    { count: number; total_value: number };
      nps:          { count: number; total_value: number };
    };
  };
}

Don't reverse-engineer this shape yourself. The SDK exports extractPortfolioSummary(data) — see Reading the parsed response — which returns a stable { totalValue, folios, holdings, asOn, casType, investorName } envelope.

Error handling

onError receives a PortfolioConnectError with a code, a user-friendly title, an optional remediation hint, and a retryable flag. If you'd rather render your own error UI from a thrown SDK error (e.g. inside a custom onSubmit), use the exported classifyError helper:

import { classifyError } from '@cas-parser/connect';

try {
  await someSdkCall();
} catch (err) {
  const e = classifyError(err);
  // e.code        — see table below
  // e.title       — short, user-readable headline
  // e.message     — slightly longer description
  // e.remediation — what the user can try next
  // e.retryable   — boolean
  // e.raw         — the original API/network payload, for analytics
}

Codes

Code When it fires Reliable?
AUTHENTICATION HTTP 401 / 403 Yes (status-code based)
RATE_LIMITED HTTP 429 Yes (status-code based)
NETWORK HTTP 5xx, ERR_NETWORK, or fetch() TypeError Yes (transport-based)
INVALID_PASSWORD The API's error message contains the word "password" Best-effort heuristic
PARSE_ERROR parseStatement failed and nothing else matched Yes (caller-supplied fallback)
GENERATOR_ERROR The mutual fund CAS request failed and nothing else matched Yes (caller-supplied fallback)
CDSL_FETCH_ERROR A CDSL OTP request or verification failed and nothing else matched Yes (caller-supplied fallback)
INBOX_ERROR Gmail import failed (OAuth or list-files) Yes (caller-supplied fallback)
INBOUND_EMAIL_ERROR Inbound email provisioning or polling failed Yes (caller-supplied fallback)
UNKNOWN Catch-all for everything else Always safe

Note. The parser API returns free-form English error messages, not a formal error vocabulary. The classifier intentionally does not try to infer fancy categories like "tampered PDF" or "corrupt scan" by string-matching the API's text — those heuristics are too fragile. If you need finer-grained handling, inspect error.details (or ClassifiedError.raw) — it contains the original API payload.

Reading the parsed response

The full response (above) is comprehensive but consistent. The SDK exports a small helper that turns it into a stable summary so you don't have to reverse-engineer the shape:

import { PortfolioConnect, extractPortfolioSummary } from '@cas-parser/connect';

<PortfolioConnect
  accessToken="..."
  onSuccess={(data) => {
    const summary = extractPortfolioSummary(data);
    console.log(summary);
    // {
    //   totalValue:   number | null,  // ₹ across all assets
    //   folios:       number | null,  // mutual fund folio count
    //   holdings:     number | null,  // sum of demat instruments
    //   asOn:         string | null,  // statement date
    //   casType:      'Mutual Funds' | 'Stocks (CDSL)' | 'Stocks (NSDL)' | string | null,
    //   investorName: string | null,
    // }
  }}
>
  {({ open }) => <button onClick={open}>Import</button>}
</PortfolioConnect>

The full response is always available on data if you need to walk individual folios / accounts / holdings.

Imperative API (non-React)

PortfolioConnect.open(config) returns a Promise that never rejects on user-close. Instead it resolves with a discriminated OpenResult:

import { open } from '@cas-parser/connect';

const result = await open({
  accessToken: 'your_access_token',
  apiBaseUrl: 'https://your-self-hosted-instance.example', // optional
  config: { enableCdslFetch: true },
});

if (result.status === 'success') {
  console.log(result.data, result.metadata);
} else if (result.status === 'closed') {
  // User cancelled the import. Not an error.
} else {
  // result.status === 'error'
  console.error(result.error.code, result.error.message);
}

For long-lived handles (open the same widget multiple times), use create():

import { create } from '@cas-parser/connect';

const widget = create({
  accessToken: 'your_access_token',
  onSuccess: (data) => console.log(data),
  onError: (err) => console.error(err),
  onClose: () => console.log('user closed'),
});

document.getElementById('open-btn').onclick = () => widget.open();
document.getElementById('destroy-btn').onclick = () => widget.destroy();

Documentation

For full documentation, API reference, and examples:

📖 casparser.in/docs

Support

License

MIT © CASParser