Documentation Index Fetch the complete documentation index at: https://docs.txncheck.com/llms.txt
Use this file to discover all available pages before exploring further.
Data Handling
TxnCheck processes sensitive personal information (PII) including mobile numbers, PAN, Aadhaar, and UPI addresses. This guide covers best practices for handling this data securely and compliantly.
Data Classification
Data Type Classification Storage Guidance Mobile Number PII Hash or mask for storage Full Name PII Store if business need exists PAN Sensitive PII Never store full PAN Masked Aadhaar PII Store only last 4 digits DOB PII Store if required for KYC UPI/VPA PII May store for transaction history API Request ID Internal Store for audit trail
Data Minimization
Only request and store data you actually need:
// ✅ Good: Only request what you need
// If you only need blocklist check, don't do full-check
const result = await client . vpaChargebackCheck ([ vpa ], { sync: true });
// ❌ Bad: Requesting more data than needed
const result = await client . fullCheck ( mobile , { sync: true });
// Then ignoring kyc data you didn't need
Selective Data Storage
interface CustomerVerification {
mobile : string ; // Full mobile for communication
mobileHash : string ; // Hash for lookups
name : string ; // From KYC
kycVerified : boolean ; // Flag only, not full KYC data
panLast4 : string ; // Only last 4 digits
verifiedAt : Date ;
// DON'T store: full PAN, full Aadhaar
}
function processVerificationResult ( result : any ) : CustomerVerification {
const kyc = result . result ?. kycByMobile || {};
return {
mobile: result . mobile ,
mobileHash: hashMobile ( result . mobile ),
name: kyc . fullName ,
kycVerified: Boolean ( kyc . pan && kyc . maskedAadhaar ),
panLast4: kyc . pan ? kyc . pan . slice ( - 4 ) : '' ,
verifiedAt: new Date (),
};
}
Data Masking
Mobile Number Masking
function maskMobile ( mobile : string ) : string {
// +919876543210 -> +91****543210
if ( mobile . length < 10 ) return mobile ;
const prefix = mobile . slice ( 0 , 3 );
const suffix = mobile . slice ( - 6 );
return ` ${ prefix } **** ${ suffix } ` ;
}
// For logging
console . log ( `Verifying ${ maskMobile ( mobile ) } ` );
PAN Masking
function maskPAN ( pan : string ) : string {
// ABCDE1234F -> ****E1234F
if ( pan . length !== 10 ) return '**********' ;
return '****' + pan . slice ( 4 );
}
// For display to user
displayElement . textContent = maskPAN ( kycData . pan );
VPA Masking
function maskVPA ( vpa : string ) : string {
// user.name@bank -> u***e@bank
const [ localPart , domain ] = vpa . split ( '@' );
if ( localPart . length <= 2 ) return vpa ;
const masked = localPart [ 0 ] + '***' + localPart . slice ( - 1 );
return ` ${ masked } @ ${ domain } ` ;
}
Hashing for Storage
Use cryptographic hashing when you need to look up data but don’t need the original value:
import crypto from 'crypto' ;
// Use a consistent salt for your application
const HASH_SALT = process . env . DATA_HASH_SALT ! ;
function hashMobile ( mobile : string ) : string {
return crypto
. createHmac ( 'sha256' , HASH_SALT )
. update ( mobile . toLowerCase ())
. digest ( 'hex' );
}
function hashVPA ( vpa : string ) : string {
return crypto
. createHmac ( 'sha256' , HASH_SALT )
. update ( vpa . toLowerCase ())
. digest ( 'hex' );
}
// Store hash, lookup by hash
const customer = await db . customer . findFirst ({
where: { mobileHash: hashMobile ( inputMobile ) },
});
Encryption at Rest
For data that must be stored in recoverable form:
import crypto from 'crypto' ;
const ENCRYPTION_KEY = Buffer . from ( process . env . DATA_ENCRYPTION_KEY ! , 'hex' ); // 32 bytes
const IV_LENGTH = 16 ;
function encrypt ( text : string ) : string {
const iv = crypto . randomBytes ( IV_LENGTH );
const cipher = crypto . createCipheriv ( 'aes-256-gcm' , ENCRYPTION_KEY , iv );
let encrypted = cipher . update ( text , 'utf8' , 'hex' );
encrypted += cipher . final ( 'hex' );
const authTag = cipher . getAuthTag ();
// Return iv:authTag:encrypted
return ` ${ iv . toString ( 'hex' ) } : ${ authTag . toString ( 'hex' ) } : ${ encrypted } ` ;
}
function decrypt ( encrypted : string ) : string {
const [ ivHex , authTagHex , encryptedText ] = encrypted . split ( ':' );
const iv = Buffer . from ( ivHex , 'hex' );
const authTag = Buffer . from ( authTagHex , 'hex' );
const decipher = crypto . createDecipheriv ( 'aes-256-gcm' , ENCRYPTION_KEY , iv );
decipher . setAuthTag ( authTag );
let decrypted = decipher . update ( encryptedText , 'hex' , 'utf8' );
decrypted += decipher . final ( 'utf8' );
return decrypted ;
}
// Usage
const encryptedMobile = encrypt ( mobile );
// Store encryptedMobile in database
Secure Logging
What to Log
// ✅ Good: Log request metadata, not data
logger . info ( 'Verification request' , {
requestId: result . requestId ,
method: 'upi-by-mobile' ,
status: result . status ,
mobile: maskMobile ( mobile ), // Masked
processingTimeMs: endTime - startTime ,
timestamp: new Date (). toISOString (),
});
What NOT to Log
// ❌ Bad: Logging sensitive data
logger . info ( 'KYC Result' , {
mobile: mobile , // Full mobile
pan: kycData . pan , // Full PAN!
aadhaar: kycData . maskedAadhaar , // Even masked Aadhaar
result: JSON . stringify ( result ), // Full response
});
Log Sanitization Middleware
function sanitizeLogData ( data : any ) : any {
const sensitiveFields = [ 'mobile' , 'pan' , 'aadhaar' , 'dob' , 'vpa' ];
const sanitized = { ... data };
for ( const field of sensitiveFields ) {
if ( sanitized [ field ]) {
sanitized [ field ] = '[REDACTED]' ;
}
}
// Deep sanitize nested objects
if ( sanitized . result ) {
sanitized . result = '[REDACTED]' ;
}
return sanitized ;
}
// Use in logger
logger . info ( 'API call' , sanitizeLogData ( logData ));
Data Retention
Retention Policies
Data Type Retention Period Justification API Request Logs 90 days Debugging and support Verification Results 7 days Dispute resolution Customer KYC Flag Account lifetime Compliance requirement Full KYC Data Not stored Data minimization Audit Logs 7 years Regulatory compliance
Implementing Retention
// Scheduled job to purge old data
async function purgeOldData () {
const retentionDays = {
verificationResults: 7 ,
apiLogs: 90 ,
};
// Delete old verification results
await db . verificationResult . deleteMany ({
where: {
createdAt: {
lt: new Date ( Date . now () - retentionDays . verificationResults * 24 * 60 * 60 * 1000 ),
},
},
});
// Delete old API logs
await db . apiLog . deleteMany ({
where: {
timestamp: {
lt: new Date ( Date . now () - retentionDays . apiLogs * 24 * 60 * 60 * 1000 ),
},
},
});
}
// Run daily
cron . schedule ( '0 3 * * *' , purgeOldData );
Data Access Controls
Role-Based Access
enum DataAccessRole {
VIEWER = 'viewer' , // Can see masked data
OPERATOR = 'operator' , // Can trigger verifications
ADMIN = 'admin' , // Can see full data (with audit)
COMPLIANCE = 'compliance' , // Can export for audits
}
function canAccessFullData ( role : DataAccessRole ) : boolean {
return role === DataAccessRole . ADMIN || role === DataAccessRole . COMPLIANCE ;
}
// API endpoint
app . get ( '/customers/:id' , authorize (), async ( req , res ) => {
const customer = await getCustomer ( req . params . id );
if ( canAccessFullData ( req . user . role )) {
// Log access for audit
await logDataAccess ( req . user . id , customer . id , 'full' );
return res . json ( customer );
}
// Return masked data for regular users
return res . json ( maskCustomerData ( customer ));
});
Audit Trail
interface DataAccessLog {
userId : string ;
action : 'view' | 'export' | 'verify' | 'delete' ;
dataType : 'customer' | 'verification' | 'report' ;
dataId : string ;
accessLevel : 'masked' | 'full' ;
ipAddress : string ;
userAgent : string ;
timestamp : Date ;
}
async function logDataAccess ( log : DataAccessLog ) {
await db . dataAccessLog . create ({ data: log });
// Alert on sensitive access
if ( log . accessLevel === 'full' ) {
await alertSecurityTeam ({
message: `Full data access by ${ log . userId } ` ,
... log ,
});
}
}
Database Security
Encryption in Database
-- PostgreSQL with pgcrypto
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Encrypted column
ALTER TABLE customers ADD COLUMN mobile_encrypted BYTEA ;
-- Encrypt on insert
INSERT INTO customers (mobile_encrypted)
VALUES (pgp_sym_encrypt( '9876543210' , 'encryption_key' ));
-- Decrypt on select (requires key)
SELECT pgp_sym_decrypt(mobile_encrypted, 'encryption_key' )
FROM customers WHERE id = 1 ;
Field-Level Encryption with ORM
// Using TypeORM with custom transformer
import { ValueTransformer } from 'typeorm' ;
const encryptedTransformer : ValueTransformer = {
to : ( value : string ) => encrypt ( value ),
from : ( value : string ) => decrypt ( value ),
};
@ Entity ()
class Customer {
@ Column ({
transformer: encryptedTransformer ,
})
mobile : string ;
}
Data Export & Portability
For GDPR/data portability requests:
async function exportCustomerData ( customerId : string ) : Promise < CustomerExport > {
const customer = await db . customer . findUnique ({
where: { id: customerId },
include: {
verifications: {
select: {
id: true ,
method: true ,
status: true ,
createdAt: true ,
// Don't include result data
},
},
},
});
return {
personalData: {
name: customer . name ,
mobile: customer . mobile ,
email: customer . email ,
createdAt: customer . createdAt ,
},
verificationHistory: customer . verifications . map ( v => ({
date: v . createdAt ,
type: v . method ,
status: v . status ,
})),
exportDate: new Date (),
format: 'JSON' ,
};
}
Checklist
Compliance Regulatory compliance guidance
Security Overview Complete security guide