Overview
The SAT Descarga Masiva (Bulk Download) service enables automatic synchronization of CFDI invoices directly from the Mexican tax authority (SAT) servers. This service uses the profile’s FIEL (e.firma) credentials to authenticate with SAT’s web services and download all issued and received invoices.
Security Notice: FIEL credentials (certificate, private key, and password) are encrypted before storage using AES-256 encryption. Never store or transmit FIEL credentials in plain text.
FIEL (e.firma) Credentials
FIEL (Firma Electrónica) is Mexico’s digital signature system. To use SAT Descarga Masiva, you need:
- Certificate file (
.cer): Public certificate
- Private key file (
.key): Private key
- Password: Private key password
FIEL credentials are issued by the SAT and are unique to each taxpayer (RFC). They provide secure authentication for SAT web services.
Registering FIEL Credentials
Register FIEL credentials for a profile:
// From sat-descarga-masiva.service.ts:9
export async function registerCredentials(
profileId: string,
userId: string,
dto: RegisterFielDto
): Promise<void> {
const profile = await Profile.findOne({
where: { id: profileId, user_id: userId },
attributes: ['id'],
});
if (!profile) {
throw new Error('Perfil no encontrado o no pertenece al usuario');
}
const certificateBase64 = (dto.certificate_base64 ?? '').trim();
const privateKeyBase64 = (dto.private_key_base64 ?? '').trim();
const password = (dto.password ?? '').trim();
if (!certificateBase64 || !privateKeyBase64 || !password) {
throw new Error('certificate_base64, private_key_base64 y password son requeridos');
}
// Encrypt credentials before storage
const fielCerEncrypted = encrypt(certificateBase64);
const fielKeyEncrypted = encrypt(privateKeyBase64);
const fielPasswordEncrypted = encrypt(password);
await Profile.update(
{
fiel_cer_encrypted: fielCerEncrypted,
fiel_key_encrypted: fielKeyEncrypted,
fiel_password_encrypted: fielPasswordEncrypted,
sat_download_sync_enabled: true,
},
{ where: { id: profileId, user_id: userId } }
);
}
Verify Profile Ownership
Ensures the user owns the profile before storing credentials.
Validate Required Fields
All three credentials (certificate, key, password) are required.
Encrypt Credentials
Uses AES-256 encryption to secure credentials in the database.// From utils/fiel-crypto.util.ts
import { encrypt } from '../utils/fiel-crypto.util';
const fielCerEncrypted = encrypt(certificateBase64);
Enable Sync
Automatically enables SAT download sync for the profile.
Credentials should be provided as Base64-encoded strings:
interface RegisterFielDto {
certificate_base64: string; // Base64-encoded .cer file
private_key_base64: string; // Base64-encoded .key file
password: string; // Private key password (plain text, will be encrypted)
}
Encoding Credentials
// Browser/Frontend
const certFile = await file.arrayBuffer();
const certBase64 = btoa(String.fromCharCode(...new Uint8Array(certFile)));
// Node.js
const certBuffer = fs.readFileSync('certificate.cer');
const certBase64 = certBuffer.toString('base64');
Sync Status
Check the synchronization status for a profile:
// From sat-descarga-masiva.service.ts:48
export async function getSyncStatus(
profileId: string,
userId: string
): Promise<SatDescargaSyncStatus | null> {
const profile = await Profile.findOne({
where: { id: profileId, user_id: userId },
attributes: [
'id',
'sat_download_last_sync_at',
'sat_download_sync_enabled',
'fiel_cer_encrypted',
'fiel_key_encrypted',
],
});
if (!profile) return null;
return {
profile_id: profile.id,
last_sync_at: profile.sat_download_last_sync_at?.toISOString() ?? null,
sync_enabled: profile.sat_download_sync_enabled,
has_credentials:
Boolean(profile.fiel_cer_encrypted) && Boolean(profile.fiel_key_encrypted),
};
}
Sync Status Response
interface SatDescargaSyncStatus {
profile_id: string; // Profile UUID
last_sync_at: string | null; // ISO timestamp of last sync
sync_enabled: boolean; // Sync enabled flag
has_credentials: boolean; // FIEL credentials registered
}
Synchronization Process
The synchronization process downloads invoices from SAT:
// From sat-descarga-masiva.service.ts:78
export async function syncInvoicesForProfile(
profileId: string
): Promise<SatDescargaSyncResult> {
const profile = await Profile.findByPk(profileId, {
attributes: [
'id',
'fiel_cer_encrypted',
'fiel_key_encrypted',
'fiel_password_encrypted',
],
});
if (!profile) {
return { profile_id: profileId, synced: 0, errors: ['Perfil no encontrado'] };
}
if (!profile.fiel_cer_encrypted || !profile.fiel_key_encrypted) {
return {
profile_id: profileId,
synced: 0,
errors: ['El perfil no tiene credenciales FIEL registradas'],
};
}
// Note: Full SOAP integration with SAT web service is planned for future releases
// Current implementation updates the last sync timestamp
await Profile.update(
{ sat_download_last_sync_at: new Date() },
{ where: { id: profileId } }
);
return { profile_id: profileId, synced: 0, errors: [] };
}
Current Implementation: The service is currently a stub. Full SOAP integration with SAT’s Descarga Masiva web service is planned for future releases.
SAT Web Service Integration
The complete implementation will include:
Authentication
Authenticate with SAT using FIEL certificate and private key.
- Load and validate FIEL credentials
- Create SOAP security header with digital signature
- Authenticate with SAT web service
Request Download
Request a bulk download package from SAT.
- Specify date range
- Specify document type (issued/received)
- Specify RFC filter (optional)
- Submit request and receive request ID
Verify Request
Poll SAT to check if the package is ready.
- Check request status periodically
- Wait for package to be generated
- Handle timeouts and errors
Download Package
Download the ZIP package containing CFDIs.
- Download ZIP file
- Extract XML files
- Validate package integrity
Process Invoices
Parse and store downloaded invoices.
- Parse each XML file
- Validate against profile
- Store in database
- Track successfully imported invoices
Sync Result
interface SatDescargaSyncResult {
profile_id: string; // Profile UUID
synced: number; // Number of invoices synced
errors: string[]; // Array of error messages
}
Database Schema
FIEL credentials are stored in the profiles table:
ALTER TABLE profiles ADD COLUMN fiel_cer_encrypted TEXT;
ALTER TABLE profiles ADD COLUMN fiel_key_encrypted TEXT;
ALTER TABLE profiles ADD COLUMN fiel_password_encrypted TEXT;
ALTER TABLE profiles ADD COLUMN sat_download_sync_enabled BOOLEAN DEFAULT false;
ALTER TABLE profiles ADD COLUMN sat_download_last_sync_at TIMESTAMP;
Security: Never store FIEL credentials in plain text. Always use encryption at rest and in transit.
Usage Examples
Register FIEL Credentials
import { registerCredentials } from './services/sat-descarga-masiva.service';
import fs from 'fs';
// Read FIEL files
const certificateBuffer = fs.readFileSync('certificate.cer');
const privateKeyBuffer = fs.readFileSync('privatekey.key');
const dto = {
certificate_base64: certificateBuffer.toString('base64'),
private_key_base64: privateKeyBuffer.toString('base64'),
password: 'myPrivateKeyPassword',
};
try {
await registerCredentials(profileId, userId, dto);
console.log('✓ FIEL credentials registered successfully');
} catch (error) {
console.error('Failed to register credentials:', error.message);
}
Check Sync Status
import { getSyncStatus } from './services/sat-descarga-masiva.service';
const status = await getSyncStatus(profileId, userId);
if (!status) {
console.log('Profile not found');
} else {
console.log('Sync enabled:', status.sync_enabled);
console.log('Has credentials:', status.has_credentials);
console.log('Last sync:', status.last_sync_at || 'Never');
}
Trigger Sync
import { syncInvoicesForProfile } from './services/sat-descarga-masiva.service';
const result = await syncInvoicesForProfile(profileId);
if (result.errors.length > 0) {
console.error('Sync errors:', result.errors);
} else {
console.log(`✓ Synced ${result.synced} invoices`);
}
Encryption Implementation
The system uses AES-256-GCM encryption for credential storage:
// From utils/fiel-crypto.util.ts
import crypto from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const KEY = crypto.scryptSync(process.env.ENCRYPTION_KEY!, 'salt', 32);
export function encrypt(text: string): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
export function decrypt(encryptedText: string): string {
const [ivHex, authTagHex, encrypted] = encryptedText.split(':');
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
The encryption key should be stored in an environment variable and never committed to version control. Use a strong, randomly generated key.
Automated Sync Schedule
For production use, implement a cron job or scheduled task:
import cron from 'node-cron';
import { syncInvoicesForProfile } from './services/sat-descarga-masiva.service';
// Run sync daily at 2:00 AM
cron.schedule('0 2 * * *', async () => {
const profiles = await Profile.findAll({
where: { sat_download_sync_enabled: true },
attributes: ['id'],
});
for (const profile of profiles) {
try {
const result = await syncInvoicesForProfile(profile.id);
console.log(`Profile ${profile.id}: Synced ${result.synced} invoices`);
if (result.errors.length > 0) {
console.error(`Errors:`, result.errors);
}
} catch (error) {
console.error(`Failed to sync profile ${profile.id}:`, error);
}
}
});
Error Handling
Common errors:
- “Perfil no encontrado o no pertenece al usuario”: Invalid profile or unauthorized access
- “certificate_base64, private_key_base64 y password son requeridos”: Missing required credentials
- “El perfil no tiene credenciales FIEL registradas”: Credentials not configured
- SAT Service Errors: Network issues, invalid credentials, service unavailable
Implementation Details
Source Code References
- Service:
src/services/sat-descarga-masiva.service.ts
- Types:
src/types/sat-descarga.types.ts
- Encryption Utils:
src/utils/fiel-crypto.util.ts
API Integration
The SAT Descarga Masiva service is exposed through API endpoints:
POST /api/sat/fiel/register - Register FIEL credentials
GET /api/sat/sync/status - Get sync status
POST /api/sat/sync/trigger - Manually trigger sync
Future Enhancements
Planned features for future releases:
- Full SOAP integration with SAT web service
- Automatic retry on failure
- Incremental sync (download only new invoices)
- Sync progress tracking
- Email notifications on sync completion
- Detailed sync logs and audit trail