Developer Guide

An in-depth developer guide for Portfolio Connect SDK

Portfolio Connect is a drop-in UI widget that lets your users import their investment holdings from any Indian financial statement. Once the user completes the import, your app receives structured JSON data.

Supported Statements

Statement

Covers

Import Methods

CAMS / KFintech CAS

All mutual fund holdings across AMCs

PDF upload, Email request

CDSL CAS

Stocks, ETFs, Bonds, SGBs, REITs, InvITs, Demat MFs, AIFs

PDF upload, OTP fetch

NSDL CAS

Stocks, ETFs, Bonds, SGBs, REITs, InvITs, Demat MFs, AIFs

PDF upload

Installation

npm / yarn

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

CDN (Vanilla JS / Angular / Vue)

Standalone Bundle (Recommended) — no extra dependencies:

<script src="https://cdn.jsdelivr.net/npm/@cas-parser/connect/dist/portfolio-connect.standalone.min.js"></script>

Lightweight Bundle — If you already have React 18+ on your page:

<script src="https://cdn.jsdelivr.net/npm/@cas-parser/connect/dist/portfolio-connect.umd.min.js"></script>

Bundle Sizes:

  • Standalone: ~54KB gzipped

  • UMD: ~9KB gzipped (requires React 18+)

Quick Start

React / Next.js

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

function App() {
  return (
    <PortfolioConnect
      accessToken="your_access_token"
      config={{
        enableGenerator: true,  // MF email fetch
        enableCdslFetch: true,  // CDSL OTP fetch
      }}
      onSuccess={(data, metadata) => {
        console.log('Holdings:', data.holdings);
        console.log('Total Value:', data.summary?.total_value);
        console.log('Parser Type:', metadata.parser_type);
      }}
      onError={(error) => {
        console.error('Import failed:', error.message);
      }}
    >
      {({ open, isReady }) => (
        <button onClick={open} disabled={!isReady}>
          Import Investments
        </button>
      )}
    </PortfolioConnect>
  );
}

Vanilla JavaScript

<script src="https://cdn.jsdelivr.net/npm/@cas-parser/connect/dist/portfolio-connect.standalone.min.js"></script>

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

<script>
  document.getElementById('import-btn').onclick = async () => {
    try {
      const { data, metadata } = await PortfolioConnect.open({
        accessToken: 'your_access_token',
        config: { enableCdslFetch: true }
      });
      
      console.log('Holdings:', data.holdings);
      console.log('Type:', metadata.parser_type);
      
    } catch (error) {
      if (error.message === 'Widget closed by user') {
        console.log('User cancelled');
      } else {
        console.error('Error:', error.message);
      }
    }
  };
</script>

Vue 3

<template>
  <button @click="openPortfolioConnect">Import Portfolio</button>
</template>

<script setup>
const openPortfolioConnect = async () => {
  try {
    const { data } = await PortfolioConnect.open({
      accessToken: 'your_access_token',
      config: { enableCdslFetch: true }
    });
    console.log('Portfolio:', data);
  } catch (error) {
    console.error(error);
  }
};
</script>

Angular

// component.ts
import { Component } from '@angular/core';
declare const PortfolioConnect: any;

@Component({
  selector: 'app-import',
  template: `<button (click)="openWidget()">Import Portfolio</button>`
})
export class ImportComponent {
  async openWidget() {
    try {
      const { data } = await PortfolioConnect.open({
        accessToken: 'your_access_token',
        config: { enableCdslFetch: true }
      });
      console.log('Portfolio:', data);
    } catch (error) {
      console.error(error);
    }
  }
}

Configuration

Full Configuration Example

<PortfolioConnect
  accessToken="your_access_token"
  config={{
    // Statement types to allow
    allowedTypes: ['CAMS_KFINTECH', 'CDSL', 'NSDL'],
    
    // Features
    enableGenerator: true,      // MF statement via email (KFintech)
    enableCdslFetch: true,      // CDSL statement via OTP
    
    // UI Options
    showShortcuts: true,        // Email search shortcuts (Gmail, Outlook)
    showPortalLinks: true,      // Links to CAS download portals
    
    // Branding
    logoUrl: 'https://yourapp.com/logo.png',
    title: 'Import Your Investments',
    subtitle: 'Mutual Funds, Stocks, Bonds — all in one place',
    
    // Pre-fill user data (optional)
    prefill: {
      pan: 'ABCDE1234F',
      email: 'user@example.com',
      phone: '9876543210',
      boId: '1234567890123456',  // CDSL BO ID (16 digits)
      dob: '1990-01-15',         // Date of birth (YYYY-MM-DD)
    },
    
    // MF Generator options
    generator: {
      fromDate: '2020-01-01',    // Statement start date
      toDate: '2024-12-31',      // Statement end date
    },
    
    // Custom broker list (optional)
    brokers: [
      { name: 'Zerodha', depository: 'CDSL', logo: 'https://...' },
      { name: 'Groww', depository: 'CDSL', logo: 'https://...' },
    ],
  }}
  onSuccess={handleSuccess}
  onError={handleError}
  onEvent={handleEvent}
  onExit={handleExit}
>
  {({ open }) => <button onClick={open}>Import</button>}
</PortfolioConnect>

Props Reference

Prop

Type

Required

Description

accessToken

string

Yes

Your access token (get one)

config

object

No

Configuration options (see below)

onSuccess

(data, metadata) => void

Yes

Called when import succeeds

onError

(error) => void

No

Called when import fails

onExit

() => void

No

Called when widget is closed

onEvent

(event, metadata) => void

No

Called for analytics/tracking

children

({ open, isReady }) => ReactNode

Yes

Render prop for trigger button

Config Options

Option

Type

Default

Description

allowedTypes

string[]

All types

Restrict to specific statement types

enableGenerator

boolean

false

Enable MF statement request via email

enableCdslFetch

boolean

false

Enable CDSL statement fetch via OTP

showShortcuts

boolean

true

Show email search shortcuts

showPortalLinks

boolean

true

Show links to CAS download portals

logoUrl

string

CASParser logo

Your brand logo URL

title

string

"Import Your Investments"

Widget title

subtitle

string

"Mutual Funds, Stocks..."

Widget subtitle

prefill

object

-

Pre-fill user details

generator

object

-

MF generator date range options

brokers

array

Default list

Custom broker list

Prefill Options

Field

Type

Description

pan

string

User's PAN (for CAMS/KFintech and CDSL)

email

string

User's email address

phone

string

User's mobile number

boId

string

CDSL BO ID (16 digits)

dob

string

Date of birth (YYYY-MM-DD format)

Response Data

Success Callback

onSuccess={(data, metadata) => {
  // data: ParsedData
  // metadata: { filename, parser_type, parse_duration_ms }
}}

ParsedData Structure

interface ParsedData {
  cas_type: 'CAMS_KFINTECH' | 'CDSL' | 'NSDL';
  status: 'success' | 'failed';
  
  investor_info?: {
    name: string;
    email?: string;
    mobile?: string;
    pan?: string;
    address?: string;
  };
  
  // For Mutual Funds (CAMS_KFINTECH)
  folios?: Array<{
    folio_number: string;
    amc: string;
    schemes: Array<{
      scheme_name: string;
      isin: string;
      units: number;
      nav: number;
      value: number;
    }>;
  }>;
  
  // For Demat (CDSL/NSDL)
  holdings?: Array<{
    isin: string;
    name: string;
    quantity: number;
    value: number;
  }>;
  
  summary?: {
    total_value: number;
    as_on_date: string;
  };
}

Full JSON schema: docs.casparser.in/reference

Events & Analytics

Track user journey with the onEvent callback:

onEvent={(event, metadata) => {
  analytics.track(event, metadata);
}}

Available Events

Event

Description

WIDGET_OPENED

Widget opened

WIDGET_CLOSED

Widget closed

MODE_SWITCHED

User switched between Upload/Fetch modes

TYPE_CHANGED

User changed portfolio type (MF/CDSL/NSDL)

BROKER_SELECTED

User selected a broker

SEARCH_CLICKED

User clicked email search shortcut

PORTAL_CLICKED

User clicked portal link

FILE_SELECTED

User selected a file

FILE_REMOVED

User removed selected file

UPLOAD_STARTED

File upload began

UPLOAD_PROGRESS

Upload progress update

PARSE_STARTED

Parsing started

PARSE_SUCCESS

Parsing completed successfully

PARSE_ERROR

Parsing failed

GENERATOR_STARTED

MF email request initiated

GENERATOR_SUCCESS

MF email request sent

GENERATOR_ERROR

MF email request failed

CDSL_FETCH_STARTED

CDSL OTP flow initiated

CDSL_OTP_SENT

CDSL OTP sent to user

CDSL_OTP_VERIFIED

CDSL OTP verified

CDSL_FETCH_SUCCESS

CDSL holdings fetched

CDSL_FETCH_ERROR

CDSL fetch failed

Error Handling

Error Object

interface PortfolioConnectError {
  code: string;
  message: string;
  details?: any;
}

Common Error Codes

Code

Description

INVALID_PASSWORD

Wrong PDF password

PARSE_FAILED

Failed to parse the PDF

UNSUPPORTED_FORMAT

PDF format not supported

NETWORK_ERROR

Network connectivity issue

OTP_EXPIRED

CDSL OTP expired

OTP_INVALID

Invalid OTP entered

GENERATOR_FAILED

MF email request failed

Error Handling Example

onError={(error) => {
  switch (error.code) {
    case 'INVALID_PASSWORD':
      showToast('Incorrect password. Try your PAN.');
      break;
    case 'OTP_EXPIRED':
      showToast('OTP expired. Please try again.');
      break;
    default:
      showToast(error.message);
  }
}}

Framework Support

Framework

Support

Method

React

✅ Native

npm package

Next.js

✅ Native

npm package

Angular

✅ Via CDN

Standalone bundle

Vue

✅ Via CDN

Standalone bundle

Svelte

✅ Via CDN

Standalone bundle

Vanilla JS

✅ Via CDN

Standalone bundle

React Native

✅ WebView

See below

Flutter

✅ WebView

See below

React Native / Flutter

Load the widget in a WebView pointing to a hosted HTML page:

<!-- host this page and load in WebView -->
<!DOCTYPE html>
<html>
<head>
  <script src="https://cdn.jsdelivr.net/npm/@cas-parser/connect/dist/portfolio-connect.standalone.min.js"></script>
</head>
<body>
  <script>
    // Listen for messages from native app
    window.addEventListener('message', async (e) => {
      if (e.data.action === 'open') {
        try {
          const result = await PortfolioConnect.open({
            accessToken: e.data.accessToken,
            config: e.data.config
          });
          // Send result back to native app
          window.ReactNativeWebView?.postMessage(JSON.stringify({ type: 'success', data: result }));
        } catch (error) {
          window.ReactNativeWebView?.postMessage(JSON.stringify({ type: 'error', error }));
        }
      }
    });
  </script>
</body>
</html>

Security

Feature

Details

Data Hosting

100% India-hosted infrastructure

Encryption

TLS 1.3 in transit, AES-256 at rest

Compliance

SOC 2 Type II, ISO 27001

Data Retention

PDFs deleted immediately after parsing

Credentials

We never store user passwords

Access Tokens

For frontend use, generate short-lived access tokens (max 60 minutes) from your backend:

// Your backend
const token = await casparserAPI.createAccessToken({
  expiresIn: '60m'
});

// Send to frontend
res.json({ accessToken: token });

Troubleshooting

Widget not opening?

  1. Check that accessToken is valid

  2. Ensure the CDN script loaded (check console for errors)

  3. Verify isReady is true before allowing button clicks

PDF parsing fails?

  1. Ensure PDF is a valid CAS statement (CAMS, KFintech, CDSL, or NSDL)

  2. Check if password is correct (try PAN in uppercase)

  3. For encrypted PDFs, password is required

CDSL OTP not received?

  1. Verify BO ID is exactly 16 digits

  2. Check mobile number is registered with CDSL

  3. OTP expires in 3 minutes — retry if expired

Live Demo

Try the widget: connect.casparser.in

Support

Changelog

v1.1.0

  • Added standalone bundle (no React dependency needed)

  • Added broker selection UI

  • Added brokers config option

  • Added phone and dob prefill options

  • Improved error handling

v1.0.0

  • Initial release

  • React component + UMD bundle

  • CAMS/KFintech, CDSL, NSDL support

  • PDF upload, MF generator, CDSL OTP fetch