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

> Best practices for handling PII and sensitive verification data

# 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:

```typescript theme={null}
// ✅ 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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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

```typescript theme={null}
// ✅ 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

```typescript theme={null}
// ❌ 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

```typescript theme={null}
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

```typescript theme={null}
// 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

```typescript theme={null}
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

```typescript theme={null}
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

```sql theme={null}
-- 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

```typescript theme={null}
// 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:

```typescript theme={null}
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

<AccordionGroup>
  <Accordion title="Data Collection">
    * [ ] Only collect necessary data
    * [ ] Document purpose for each data field
    * [ ] Obtain proper consent
  </Accordion>

  <Accordion title="Data Storage">
    * [ ] Encryption at rest enabled
    * [ ] Sensitive fields hashed or encrypted
    * [ ] Access controls implemented
    * [ ] Retention policies defined
  </Accordion>

  <Accordion title="Data Processing">
    * [ ] Logging sanitization in place
    * [ ] Masking for display
    * [ ] Audit trail for sensitive access
  </Accordion>

  <Accordion title="Data Deletion">
    * [ ] Automated retention enforcement
    * [ ] Right to deletion process
    * [ ] Backup purging included
  </Accordion>
</AccordionGroup>

## Related Guides

<CardGroup cols={2}>
  <Card title="Compliance" icon="shield-check" href="/security/compliance">
    Regulatory compliance guidance
  </Card>

  <Card title="Security Overview" icon="shield" href="/security/overview">
    Complete security guide
  </Card>
</CardGroup>
