Loading...
Loading...
Most engineering teams underestimate FD integration. On the surface, it looks like a standard third-party API: fetch rates, submit a booking, get a confirmation. In practice, FD distribution involves regulated KYC flows, multi-gateway payment routing, bank-specific edge cases, and a post-booking lifecycle that requires webhook-driven state management.
This guide walks you through the entire integration process — from installing the SDK to going live — so your team knows exactly what to expect. We use pseudocode and generic API examples (rather than production SDK docs) to illustrate the architecture patterns, request/response shapes, and error handling strategies that apply regardless of which FD API & SDK provider you choose.
Before writing any code, make sure you have:
You will work with two environments:
| Environment | Purpose | Data | |-------------|---------|------| | Sandbox | Integration development and testing | Mock bank responses, test KYC, simulated payments | | Production | Live FD bookings with real banks | Real bank APIs, actual KYC verification, live payments |
Never use production credentials during development. Sandbox environments simulate the full booking flow — including edge cases like KYC failures, payment timeouts, and bank downtime.
You have two integration models to choose from. Most teams start with the SDK and move to direct API calls for deeper customization later.
The SDK provides pre-built UI components for rate display, KYC flows, and payment screens. You initialize it with your credentials, theme it to match your brand, and the SDK handles the heavy lifting.
Advantages:
- 70% less code to write
- Pre-built compliance screens (consent, DICGC disclosures)
- Automatic KYC flow routing (PAN → eKYC → VKYC)
- Payment gateway abstraction
- Handles bank-specific edge cases internally
Disadvantages:
- Less granular control over every screen
- SDK updates require version bumps
- Slightly larger bundle size
Direct API calls give you complete control over every screen, animation, and user flow. You build the frontend, and the API handles backend orchestration with banks.
Advantages:
- Full control over every pixel
- No SDK dependency in your bundle
- Can integrate with any frontend framework
- Custom analytics and tracking on every step
Disadvantages:
- Significantly more code to write and maintain
- You handle compliance screens yourself
- You manage KYC flow routing logic
- Must handle bank-specific error codes
For a deeper comparison of integration models, see the one SDK ten banks architecture article.
# npm
npm install @blostem/fd-sdk
# yarn
yarn add @blostem/fd-sdk
# pnpm
pnpm add @blostem/fd-sdk
Initialize the SDK once at your application's entry point. The configuration object includes your credentials, environment, and theming options.
import { BlostemFD } from '@blostem/fd-sdk';
const fdClient = BlostemFD.init({
apiKey: process.env.BLOSTEM_API_KEY,
secret: process.env.BLOSTEM_SECRET,
environment: 'sandbox', // 'sandbox' | 'production'
partner: {
id: 'your-partner-id',
name: 'Your App Name',
},
theme: {
primaryColor: '#2563EB',
fontFamily: 'Inter, sans-serif',
borderRadius: '8px',
mode: 'light', // 'light' | 'dark'
},
callbacks: {
onBookingComplete: (booking) => {
console.log('FD booked:', booking.referenceId);
},
onError: (error) => {
console.error('FD SDK error:', error.code, error.message);
},
},
});
Important: The apiKey and secret should be stored as environment variables, never hardcoded. The secret is used only for server-side webhook verification — it should never be exposed in client-side code.
The rate API is the entry point for your FD experience. It returns current rates from all connected banks, filterable by multiple parameters.
// Fetch rates from all banks
const rates = await fdClient.rates.fetch({
depositAmount: 500000, // Rs 5,00,000
customerType: 'general', // 'general' | 'senior_citizen'
payoutType: 'cumulative', // 'cumulative' | 'non_cumulative'
});
// Or filter by specific criteria
const filteredRates = await fdClient.rates.fetch({
depositAmount: 500000,
customerType: 'senior_citizen',
payoutType: 'cumulative',
tenureMonths: 12, // Specific tenure
bankType: 'sfb', // 'sfb' | 'private' | 'public' | 'nbfc'
sortBy: 'rate_desc', // 'rate_desc' | 'rate_asc' | 'tenure_asc'
});
{
"status": "success",
"data": {
"rates": [
{
"bankId": "suryoday_sfb",
"bankName": "Suryoday Small Finance Bank",
"bankType": "sfb",
"logoUrl": "https://cdn.blostem.com/banks/suryoday.png",
"dicgcInsured": true,
"tenures": [
{
"tenureId": "sur_12m",
"tenureDays": 365,
"tenureLabel": "12 Months",
"interestRate": 8.60,
"seniorCitizenRate": 9.10,
"minAmount": 5000,
"maxAmount": 100000000,
"compoundingFrequency": "quarterly",
"maturityAmount": 544300,
"interestEarned": 44300,
"isSpecialTenure": false
},
{
"tenureId": "sur_181d",
"tenureDays": 181,
"tenureLabel": "181 Days (Special)",
"interestRate": 8.25,
"seniorCitizenRate": 8.75,
"minAmount": 5000,
"maxAmount": 100000000,
"compoundingFrequency": "quarterly",
"maturityAmount": 520438,
"interestEarned": 20438,
"isSpecialTenure": true
}
]
},
{
"bankId": "unity_sfb",
"bankName": "Unity Small Finance Bank",
"bankType": "sfb",
"logoUrl": "https://cdn.blostem.com/banks/unity.png",
"dicgcInsured": true,
"tenures": [
{
"tenureId": "uni_501d",
"tenureDays": 501,
"tenureLabel": "501 Days (Special)",
"interestRate": 8.75,
"seniorCitizenRate": 9.25,
"minAmount": 5000,
"maxAmount": 50000000,
"compoundingFrequency": "quarterly",
"maturityAmount": 562120,
"interestEarned": 62120,
"isSpecialTenure": true
}
]
}
],
"lastUpdated": "2026-03-01T05:30:00.000Z",
"totalBanks": 12,
"totalTenures": 87
}
}
Rate data changes infrequently (typically once every few weeks when banks revise rates). Implement a caching layer:
rates.updated webhook to invalidate cache when rates change.KYC is the most complex part of FD integration. Different banks require different verification levels, and the flow varies based on deposit amount, customer history, and regulatory requirements.
Before starting a KYC flow, check if the user already has valid KYC with the selected bank:
const kycStatus = await fdClient.kyc.checkStatus({
userId: 'user_123',
bankId: 'suryoday_sfb',
});
{
"status": "success",
"data": {
"kycComplete": false,
"completedSteps": ["pan_verified"],
"pendingSteps": ["aadhaar_ekyc"],
"requiredForAmount": {
"upTo50000": ["pan"],
"upTo500000": ["pan", "aadhaar_ekyc"],
"above500000": ["pan", "aadhaar_ekyc", "video_kyc"]
}
}
}
The first KYC step is always PAN verification. This is an instant API call:
const panResult = await fdClient.kyc.verifyPan({
userId: 'user_123',
panNumber: 'ABCDE1234F',
dateOfBirth: '1990-05-15',
});
{
"status": "success",
"data": {
"verified": true,
"nameOnPan": "RAHUL SHARMA",
"panType": "individual",
"kycStep": "pan_verified"
}
}
For deposits above Rs 50,000, most banks require Aadhaar-based eKYC. This is a two-step flow: initiate OTP, then verify OTP.
// Step 1: Initiate OTP
const otpResponse = await fdClient.kyc.initiateAadhaarOtp({
userId: 'user_123',
aadhaarNumber: '1234-5678-9012', // Last 4 digits masked in logs
});
// Step 2: Verify OTP (user enters OTP from their phone)
const verifyResponse = await fdClient.kyc.verifyAadhaarOtp({
userId: 'user_123',
requestId: otpResponse.data.requestId,
otp: '123456',
consent: true, // User must explicitly consent to Aadhaar verification
});
For new-to-bank customers with deposits above Rs 50,000 (threshold varies by bank), Video KYC may be required. This can be scheduled or instant:
const vkycSession = await fdClient.kyc.scheduleVideoKyc({
userId: 'user_123',
bankId: 'suryoday_sfb',
preferredSlot: '2026-03-02T10:00:00.000Z', // Optional
});
The SDK handles the video call UI, document capture, and liveness check. For a deep dive on scaling VKYC, read Video KYC at Scale.
| Scenario | Handling | |----------|----------| | PAN-Aadhaar name mismatch | Show user the mismatch and offer manual review | | Aadhaar OTP expired (3 min) | Allow re-initiation, max 3 attempts per session | | VKYC agent unavailable | Schedule for next available slot, send push notification | | CKYC already exists | Skip eKYC, use existing CKYC record (bank verifies) | | NRI customer | Redirect to bank's NRI FD flow (not all banks support via API) | | Minor (below 18) | Joint account with guardian required, additional docs needed |
Once KYC is complete, the user needs to transfer funds to book the FD. The payment flow involves gateway selection, payment initiation, and confirmation.
Different banks require different payment gateways. The API abstracts this complexity:
const paymentSession = await fdClient.payments.initiate({
userId: 'user_123',
bankId: 'suryoday_sfb',
tenureId: 'sur_12m',
amount: 500000,
paymentMethod: 'netbanking', // 'netbanking' | 'upi' | 'neft' | 'rtgs'
});
{
"status": "success",
"data": {
"paymentSessionId": "pay_sess_abc123",
"gateway": "razorpay",
"gatewayOrderId": "order_xyz789",
"amount": 500000,
"currency": "INR",
"paymentMethods": ["netbanking", "upi"],
"expiresAt": "2026-03-01T11:00:00.000Z",
"callbackUrl": "https://yourapp.com/api/fd/payment-callback"
}
}
After the user completes payment, the gateway redirects to your callback URL:
// Server-side payment callback handler
app.post('/api/fd/payment-callback', async (req, res) => {
const { paymentSessionId, gatewayPaymentId, status } = req.body;
// Verify the payment with the FD API
const verification = await fdClient.payments.verify({
paymentSessionId,
gatewayPaymentId,
});
if (verification.data.verified) {
// Payment confirmed — FD booking will be triggered automatically
// The booking confirmation comes via webhook
res.redirect('/fd/booking-pending');
} else {
res.redirect('/fd/payment-failed');
}
});
For an in-depth look at payment routing for FD bookings, see the payment gateway deep dive.
After payment verification, the FD is booked with the bank. This can happen synchronously (instant confirmation) or asynchronously (confirmation within minutes to hours, depending on the bank).
const booking = await fdClient.bookings.create({
userId: 'user_123',
bankId: 'suryoday_sfb',
tenureId: 'sur_12m',
amount: 500000,
paymentSessionId: 'pay_sess_abc123',
nominee: {
name: 'PRIYA SHARMA',
relationship: 'spouse',
},
});
{
"status": "success",
"data": {
"bookingId": "bk_fd_98765",
"bankReferenceId": "SUR2026030100123",
"state": "confirmed",
"bankName": "Suryoday Small Finance Bank",
"principal": 500000,
"interestRate": 8.60,
"tenure": "12 Months",
"tenureDays": 365,
"bookingDate": "2026-03-01",
"maturityDate": "2027-03-01",
"maturityAmount": 544300,
"interestEarned": 44300,
"compounding": "quarterly",
"payoutType": "cumulative",
"nominee": {
"name": "PRIYA SHARMA",
"relationship": "spouse"
},
"certificateUrl": "https://api.blostem.com/certificates/bk_fd_98765.pdf"
}
}
FD bookings move through the following states:
| State | Description | Typical Duration |
|-------|-------------|-----------------|
| payment_pending | Waiting for payment confirmation | Seconds to minutes |
| payment_confirmed | Payment received, booking in progress | — |
| processing | Bank is processing the FD booking | Minutes to hours |
| confirmed | FD booked successfully | — |
| rejected | Bank rejected the booking (KYC issue, amount limit) | — |
| active | FD is active and earning interest | Duration of tenure |
| matured | FD has reached maturity | — |
| withdrawn | Premature withdrawal processed | — |
| renewed | Auto-renewed or manually renewed | — |
Webhooks are essential for keeping your system in sync with FD lifecycle events. Configure your webhook endpoint during SDK initialization or via the dashboard.
// Server-side webhook handler
app.post('/api/fd/webhooks', async (req, res) => {
// Verify webhook signature
const signature = req.headers['x-blostem-signature'];
const isValid = fdClient.webhooks.verify(req.body, signature);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, data } = req.body;
switch (event) {
case 'booking.confirmed':
await handleBookingConfirmed(data);
break;
case 'booking.rejected':
await handleBookingRejected(data);
break;
case 'fd.maturity_approaching':
await sendMaturityReminder(data); // 7 days before
break;
case 'fd.matured':
await handleMaturity(data);
break;
case 'fd.interest_credited':
await updatePortfolio(data); // For non-cumulative FDs
break;
case 'fd.withdrawn':
await handleWithdrawal(data);
break;
case 'rates.updated':
await invalidateRateCache();
break;
case 'kyc.expired':
await promptKycRenewal(data);
break;
default:
console.log('Unhandled webhook event:', event);
}
res.status(200).json({ received: true });
});
{
"event": "booking.confirmed",
"timestamp": "2026-03-01T06:15:00.000Z",
"data": {
"bookingId": "bk_fd_98765",
"userId": "user_123",
"bankId": "suryoday_sfb",
"bankReferenceId": "SUR2026030100123",
"principal": 500000,
"interestRate": 8.60,
"maturityDate": "2027-03-01",
"maturityAmount": 544300
}
}
bookingId and event combination as a deduplication key.x-blostem-signature header to prevent spoofed webhook calls.After users have booked FDs, they need a way to see their holdings. The portfolio API provides a consolidated view across all banks.
const portfolio = await fdClient.portfolio.list({
userId: 'user_123',
status: 'active', // 'active' | 'matured' | 'all'
});
{
"status": "success",
"data": {
"summary": {
"totalInvested": 1500000,
"totalCurrentValue": 1572400,
"totalInterestEarned": 72400,
"activeFDs": 3,
"nextMaturity": "2026-06-15"
},
"deposits": [
{
"bookingId": "bk_fd_98765",
"bankName": "Suryoday Small Finance Bank",
"principal": 500000,
"interestRate": 8.60,
"bookingDate": "2026-03-01",
"maturityDate": "2027-03-01",
"currentValue": 512150,
"interestAccrued": 12150,
"daysRemaining": 365,
"status": "active"
}
]
}
}
Robust error handling is critical for financial products. Here are the most common error scenarios and recommended handling:
{
"status": "error",
"error": {
"code": "KYC_AADHAAR_OTP_EXPIRED",
"message": "The Aadhaar OTP has expired. Please request a new OTP.",
"category": "kyc",
"retryable": true,
"details": {
"maxRetries": 3,
"retryAfterSeconds": 30
}
}
}
| Error Code | Category | Cause | Recommended Action |
|-----------|----------|-------|-------------------|
| RATE_STALE | rates | Rate changed since user viewed it | Re-fetch rates, show updated rate, ask user to confirm |
| KYC_PAN_MISMATCH | kyc | PAN details don't match user profile | Show mismatch details, allow correction |
| KYC_AADHAAR_OTP_EXPIRED | kyc | OTP expired (3 min timeout) | Allow OTP re-initiation |
| KYC_VKYC_UNAVAILABLE | kyc | VKYC agent not available | Schedule for later, show available slots |
| PAYMENT_TIMEOUT | payment | Payment gateway timeout | Check payment status, retry if not captured |
| PAYMENT_FAILED | payment | Insufficient funds or declined | Show failure reason, allow retry |
| BANK_DOWNTIME | booking | Bank API is temporarily unavailable | Show maintenance message, retry with backoff |
| AMOUNT_BELOW_MINIMUM | booking | Amount below bank minimum | Show minimum amount requirement |
| AMOUNT_ABOVE_MAXIMUM | booking | Amount exceeds bank limit | Show maximum amount, suggest splitting |
| TENURE_UNAVAILABLE | booking | Selected tenure no longer available | Re-fetch tenures, show alternatives |
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
backoffMs: number = 1000
): Promise<T> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (!error.retryable || attempt === maxRetries) {
throw error;
}
await sleep(backoffMs * Math.pow(2, attempt - 1));
}
}
throw new Error('Max retries exceeded');
}
The sandbox environment provides test scenarios for every part of the flow:
| Test PAN | Behavior |
|----------|----------|
| AAAAA0001A | KYC passes successfully |
| AAAAA0002B | PAN-name mismatch error |
| AAAAA0003C | PAN not found |
| Test Aadhaar | Behavior |
|-------------|----------|
| 1111-2222-3333 | eKYC passes (OTP: 123456) |
| 4444-5555-6666 | OTP expired scenario |
| 7777-8888-9999 | Aadhaar-PAN link failure |
| Test Amount | Behavior | |-------------|----------| | Rs 10,000 | Booking succeeds instantly | | Rs 50,001 | Triggers VKYC requirement | | Rs 99,99,999 | Triggers processing delay (async booking) |
Write automated tests for the critical paths:
describe('FD Booking Flow', () => {
it('should complete a full booking with KYC', async () => {
// 1. Fetch rates
const rates = await fdClient.rates.fetch({ depositAmount: 100000 });
expect(rates.data.rates.length).toBeGreaterThan(0);
// 2. Verify PAN
const pan = await fdClient.kyc.verifyPan({
userId: 'test_user',
panNumber: 'AAAAA0001A',
dateOfBirth: '1990-01-01',
});
expect(pan.data.verified).toBe(true);
// 3. Initiate payment
const payment = await fdClient.payments.initiate({
userId: 'test_user',
bankId: rates.data.rates[0].bankId,
tenureId: rates.data.rates[0].tenures[0].tenureId,
amount: 100000,
paymentMethod: 'netbanking',
});
expect(payment.data.paymentSessionId).toBeDefined();
// 4. Simulate payment callback (sandbox only)
const verification = await fdClient.payments.simulateSuccess({
paymentSessionId: payment.data.paymentSessionId,
});
expect(verification.data.verified).toBe(true);
// 5. Create booking
const booking = await fdClient.bookings.create({
userId: 'test_user',
bankId: rates.data.rates[0].bankId,
tenureId: rates.data.rates[0].tenures[0].tenureId,
amount: 100000,
paymentSessionId: payment.data.paymentSessionId,
});
expect(booking.data.state).toBe('confirmed');
});
});
Before switching from sandbox to production, verify every item on this list:
The SDK supports deep theming to match your brand identity. Every user-facing screen can be customized:
const fdClient = BlostemFD.init({
// ... credentials
theme: {
colors: {
primary: '#2563EB',
primaryLight: '#3B82F6',
background: '#FFFFFF',
surface: '#F8FAFC',
text: '#0F172A',
textSecondary: '#64748B',
success: '#10B981',
error: '#EF4444',
warning: '#F59E0B',
},
typography: {
fontFamily: 'Inter, sans-serif',
headingWeight: '700',
bodyWeight: '400',
},
shape: {
borderRadius: '8px',
cardRadius: '12px',
buttonRadius: '8px',
},
branding: {
logoUrl: 'https://yourapp.com/logo.svg',
appName: 'Your App',
poweredByVisible: false, // Hide "Powered by Blostem" badge
},
},
});
For more on the white-label architecture and token-first theming approach, see the SDK documentation and API reference.
Once your integration is live, the focus shifts to optimization:
For questions about integration patterns, architecture decisions, or production readiness, explore the FD API provider comparison or reach out through the why Blostem page to connect with the integration team.