Skip to main content

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 } }
  );
}
1

Verify Profile Ownership

Ensures the user owns the profile before storing credentials.
2

Validate Required Fields

All three credentials (certificate, key, password) are required.
3

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);
4

Enable Sync

Automatically enables SAT download sync for the profile.

Credential Format

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

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
2

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
3

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
4

Download Package

Download the ZIP package containing CFDIs.
  • Download ZIP file
  • Extract XML files
  • Validate package integrity
5

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:
  1. Full SOAP integration with SAT web service
  2. Automatic retry on failure
  3. Incremental sync (download only new invoices)
  4. Sync progress tracking
  5. Email notifications on sync completion
  6. Detailed sync logs and audit trail

Build docs developers (and LLMs) love