@cas-parser/connect
v2.1.0
Published
Portfolio Connect - Lightweight widget for importing Indian investment statements (MF, CDSL, NSDL). Works with React, Angular, Vue, and vanilla JS.
Downloads
944
Maintainers
Readme
Portfolio Connect SDK
Drop-in widget for importing Indian investment statements (Mutual Funds, CDSL, NSDL).
Quick Start
- Get an access token from casparser.in/docs.
- Install the SDK or load the standalone bundle from a CDN.
- 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/connectReact
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: '[email protected]',
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(orClassifiedError.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:
Support
License
MIT © CASParser
