Extracción de PDF con OCR


El sistema de extracción de PDF utiliza un flujo de 3 pasos para procesar documentos de forma eficiente y confiable, permitiendo el manejo de archivos grandes mediante carga fragmentada (chunked upload).

Tamaño máximo: 50MB
Formato de entrada: PDF codificado en base64
Formato de respuesta: JSON con datos estructurados extraídos mediante OCR + AI

Nota Importante: Este endpoint está especializado en documentos PDF complejos como Actas Constitutivas, documentos notariales, CFE y otros documentos legales/corporativos. Para procesar documentos de identificación (INE, Pasaporte, etc.), consulta los endpoints especializados de JAAK para estos tipos de documentos.


Flujo de Procesamiento

1. INIT → 2. UPLOAD-CHUNK (x N) → 3. COMPLETE → 4. RESULTADO

Especificaciones por Endpoint

1. INIT - Inicialización de Sesión

Endpoint: POST /v4/document/extract/pdf/init
Descripción: Inicia una nueva sesión de carga. El servidor calcula automáticamente el número de chunks necesarios y retorna un upload_id único.

Headers

ParámetroTipoRequeridoDescripción
Request-IdstringUUID v4 único para rastrear la solicitud
LanguagestringOpcionalen o es (default: en)
AuthorizationstringBearer token de autenticación

Request Body

{}

Nota importante: El endpoint INIT no requiere parámetros en el body. El servidor configura automáticamente el tamaño de chunk óptimo (1MB) y calcula el número total de chunks necesarios.

Response (200 OK)

{
  "upload_id": "5e29520d-dc1b-41e9-8c67-31a8dc8d71d4",
  "file_size": 19345358,
  "chunk_size": 1048576,
  "total_chunks": 19,
  "expires_at": "2026-01-21T02:39:42.651086827Z",
  "message": "Upload session created successfully. Upload 19 chunks."
}
CampoTipoDescripción
upload_idstringUUID único de la sesión (usar en siguientes requests)
file_sizeintegerTamaño total del archivo que el servidor espera recibir
chunk_sizeintegerTamaño de cada chunk en bytes (típicamente 1048576 = 1MB)
total_chunksintegerNúmero total de chunks que debes enviar
expires_atstringTimestamp ISO cuando expira la sesión (1 hora desde creación)
messagestringMensaje descriptivo del estado

2. UPLOAD-CHUNK - Carga de Fragmentos

Endpoint: POST /v4/document/extract/pdf/upload-chunk
Descripción: Envía cada chunk del documento. Este endpoint se llama múltiples veces (una por cada chunk).

IMPORTANTE: Los chunks están indexados desde 0 (zero-indexed), no desde 1.

Headers

ParámetroTipoRequeridoDescripción
Request-IdstringUUID v4 único para este chunk específico
LanguagestringOpcionalen o es
AuthorizationstringBearer token de autenticación

Request Body

{
  "upload_id": "5e29520d-dc1b-41e9-8c67-31a8dc8d71d4",
  "chunk_number": 0,
  "chunk_data": "JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1ID..."
}
CampoTipoRequeridoDescripción
upload_idstringID de la sesión obtenido en INIT
chunk_numberintegerNúmero secuencial del chunk (0-indexed: 0, 1, 2, ..., N-1)
chunk_datastringFragmento del PDF codificado en base64

Response (200 OK)

{
  "upload_id": "5e29520d-dc1b-41e9-8c67-31a8dc8d71d4",
  "chunk_number": 18,
  "uploaded_chunks": 1,
  "total_chunks": 19,
  "remaining_chunks": 18,
  "progress_percent": 5,
  "is_complete": false,
  "message": "Chunk 18 uploaded successfully. 1/19 chunks complete."
}
CampoTipoDescripción
upload_idstringID de la sesión
chunk_numberintegerNúmero del chunk que acabas de subir
uploaded_chunksintegerTotal de chunks únicos recibidos hasta ahora
total_chunksintegerTotal de chunks necesarios
remaining_chunksintegerChunks que faltan por subir
progress_percentintegerPorcentaje de progreso (0-100)
is_completebooleantrue cuando uploaded_chunks === total_chunks
messagestringMensaje descriptivo del progreso

Nota: Los chunks se pueden enviar en cualquier orden (no necesariamente secuencial), pero todos deben ser enviados antes de llamar a COMPLETE.


3. COMPLETE - Finalización y Procesamiento

Endpoint: POST /v4/document/extract/pdf/complete
Descripción: Finaliza la carga, ensambla el documento completo e inicia el procesamiento OCR con IA.

Headers

ParámetroTipoRequeridoDescripción
Request-IdstringUUID v4 único para esta solicitud
LanguagestringOpcionalen o es
AuthorizationstringBearer token de autenticación

Request Body

{
  "upload_id": "5e29520d-dc1b-41e9-8c67-31a8dc8d71d4",
  "name": "usuario_ejemplo"
}
CampoTipoRequeridoDescripción
upload_idstringID de la sesión de carga
namestringOpcionalIdentificador del usuario/cliente que sube el documento

Response (200 OK) - Acta Constitutiva (Ejemplo)

{
  "eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "requestId": "f9e8d7c6-b5a4-3210-fedc-ba9876543210",
  "status": "SUCCESS",
  "content": {
    "data": {
      "capital_shareholders": {
        "founding_shareholders": [
          {
            "initial_participation_percentage": "50% [p. 15] (VEINTICINCO ACCIONES)",
            "name": "MARIA GUADALUPE HERNANDEZ RODRIGUEZ [p. 1, 15]"
          },
          {
            "initial_participation_percentage": "50% [p. 15] (VEINTICINCO ACCIONES)",
            "name": "JUAN CARLOS MARTINEZ LOPEZ [p. 1, 15]"
          }
        ],
        "minimum_capital_share_type": "Clase \"I\", serie \"A\" [p. 5, 15]",
        "minimum_fixed_capital": "CINCUENTA MIL PESOS [p. 5, 15]"
      },
      "company_data": {
        "corporate_type": "S.A. DE C.V. [p. 2]",
        "deed_date": "25/03/2023 [p. 1]",
        "deed_location": "Guadalajara, Jalisco [p. 1]",
        "duration": "Indefinida [p. 3]",
        "full_name": "TECNOLOGIA DIGITAL INNOVADORA, SOCIEDAD ANONIMA DE CAPITAL VARIABLE [p. 1, 2]",
        "registered_address": "Guadalajara, Jalisco [p. 4]"
      },
      "initial_governing_bodies": {
        "board_chairman": "MARIA GUADALUPE HERNANDEZ RODRIGUEZ [p. 18]",
        "board_secretary": "JUAN CARLOS MARTINEZ LOPEZ [p. 18]",
        "commissioner": "Roberto Sanchez Ramirez [p. 18]"
      },
      "main_corporate_purpose": [
        "El desarrollo, implementación y comercialización de soluciones tecnológicas [p. 3]",
        "La prestación de servicios de consultoría en transformación digital [p. 3]",
        "El diseño y desarrollo de plataformas de software [p. 3]"
      ]
    },
    "extra": {
      "country_code": "MX",
      "document_type": "ACTA_CONSTITUTIVA",
      "page_count": 22
    }
  },
  "processingTime": "18750"
}

Implementación Completa

JavaScript/TypeScript

class JAKPDFExtractor {
  constructor(apiKey, options = {}) {
    this.baseUrl = options.baseUrl || 'https://api.jaak.mx';
    this.apiKey = apiKey;
    this.language = options.language || 'es';
  }

  /**
   * Extrae datos de un archivo PDF
   * @param {File} file - Archivo PDF del navegador
   * @param {string} userName - Nombre del usuario que sube el documento
   * @param {Function} onProgress - Callback para actualizar progreso
   * @returns {Promise} Resultado de la extracción
   */
  async extractPDF(file, userName, onProgress) {
    try {
      // Validaciones
      if (!file || file.type !== 'application/pdf') {
        throw new Error('El archivo debe ser un PDF válido');
      }
      
      if (file.size > 50 * 1024 * 1024) {
        throw new Error('El archivo excede el tamaño máximo de 50MB');
      }

      // Convertir PDF a base64
      const base64PDF = await this._fileToBase64(file);
      
      // Paso 1: INIT - Obtener configuración del servidor
      const initData = await this._initUpload();
      const { upload_id, chunk_size, total_chunks } = initData;
      
      // Dividir en chunks usando el tamaño que indica el servidor
      const chunks = this._splitIntoChunks(base64PDF, chunk_size);
      
      if (chunks.length !== total_chunks) {
        console.warn(`Advertencia: chunks calculados (${chunks.length}) difiere del servidor (${total_chunks})`);
      }
      
      // Paso 2: UPLOAD-CHUNK (múltiples) - chunks indexados desde 0
      for (let i = 0; i < chunks.length; i++) {
        const progress = await this._uploadChunk(
          upload_id,
          i,  // chunk_number empieza en 0
          chunks[i]
        );
        
        if (onProgress) {
          onProgress({
            percent: progress.progress_percent,
            message: progress.message,
            uploadedChunks: progress.uploaded_chunks,
            totalChunks: progress.total_chunks,
            isComplete: progress.is_complete
          });
        }
      }
      
      // Paso 3: COMPLETE y obtener resultado
      const result = await this._completeUpload(upload_id, userName);
      return result;
      
    } catch (error) {
      console.error('Error en extracción de PDF:', error);
      throw error;
    }
  }

  async _fileToBase64(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (e) => {
        // Remover el prefijo data:application/pdf;base64,
        const base64 = e.target.result.split(',')[1];
        resolve(base64);
      };
      reader.onerror = () => reject(new Error('Error al leer el archivo'));
      reader.readAsDataURL(file);
    });
  }

  async _initUpload() {
    const response = await fetch(`${this.baseUrl}/v4/document/extract/pdf/init`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Request-Id': this._generateUUID(),
        'Language': this.language,
        'Authorization': `Bearer ${this.apiKey}`
      },
      body: JSON.stringify({})  // Body vacío
    });
    
    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Error al inicializar carga');
    }
    
    return await response.json();
  }

  async _uploadChunk(uploadId, chunkNumber, chunkData, maxRetries = 3) {
    let lastError;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        const response = await fetch(`${this.baseUrl}/v4/document/extract/pdf/upload-chunk`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Request-Id': this._generateUUID(),
            'Language': this.language,
            'Authorization': `Bearer ${this.apiKey}`
          },
          body: JSON.stringify({
            upload_id: uploadId,
            chunk_number: chunkNumber,  // 0-indexed
            chunk_data: chunkData
          })
        });
        
        if (!response.ok) {
          const error = await response.json();
          throw new Error(error.message || `Error al subir chunk ${chunkNumber}`);
        }
        
        return await response.json();
        
      } catch (error) {
        lastError = error;
        
        if (attempt < maxRetries) {
          // Exponential backoff: 1s, 2s, 4s
          await this._sleep(Math.pow(2, attempt - 1) * 1000);
          console.log(`Reintentando chunk ${chunkNumber}, intento ${attempt + 1}/${maxRetries}`);
        }
      }
    }
    
    throw lastError;
  }

  async _completeUpload(uploadId, userName) {
    const response = await fetch(`${this.baseUrl}/v4/document/extract/pdf/complete`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Request-Id': this._generateUUID(),
        'Language': this.language,
        'Authorization': `Bearer ${this.apiKey}`
      },
      body: JSON.stringify({
        upload_id: uploadId,
        name: userName  // Opcional
      })
    });
    
    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Error al completar procesamiento');
    }
    
    return await response.json();
  }

  _splitIntoChunks(base64String, chunkSize) {
    const chunks = [];
    // El servidor espera chunks del tamaño especificado en INIT
    for (let i = 0; i < base64String.length; i += chunkSize) {
      chunks.push(base64String.slice(i, i + chunkSize));
    }
    return chunks;
  }

  _generateUUID() {
    return crypto.randomUUID();
  }

  _sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// ============================================
// EJEMPLO DE USO
// ============================================

const extractor = new JAKPDFExtractor('your-api-key-here', {
  baseUrl: 'https://api.jaak.mx',
  language: 'es'
});

document.getElementById('pdf-input').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const userName = 'juan_perez'; // O el nombre del usuario actual
  const progressBar = document.getElementById('progress-bar');
  const resultDiv = document.getElementById('result');
  
  if (!file) return;
  
  try {
    progressBar.innerHTML = '<p>Iniciando...</p>';
    
    const result = await extractor.extractPDF(file, userName, (progress) => {
      progressBar.innerHTML = `
        <div class="progress">
          <div class="progress-bar" style="width: ${progress.percent}%">
            ${progress.percent}%
          </div>
        </div>
        <p>${progress.message}</p>
        <small>Chunks: ${progress.uploadedChunks}/${progress.totalChunks}</small>
      `;
    });
    
    // Mostrar resultado
    resultDiv.innerHTML = `
      <h3>Extracción Completada</h3>
      <p><strong>Tipo:</strong> ${result.content.extra.document_type}</p>
      <p><strong>País:</strong> ${result.content.extra.country_code}</p>
      <p><strong>Páginas:</strong> ${result.content.extra.page_count}</p>
      <p><strong>Tiempo:</strong> ${result.processingTime}ms</p>
      <pre>${JSON.stringify(result.content.data, null, 2)}</pre>
    `;
    
  } catch (error) {
    resultDiv.innerHTML = `<p class="error">Error: ${error.message}</p>`;
  }
});

Python

import base64
import uuid
import time
import requests
from typing import Callable, Optional, Dict, Any

class JAKPDFExtractor:
    """Cliente para el API de extracción de PDF de JAAK"""
    
    def __init__(self, api_key: str, base_url: str = 'https://api.jaak.mx', 
                 language: str = 'es'):
        self.base_url = base_url
        self.api_key = api_key
        self.language = language
        
    def extract_pdf(self, pdf_path: str, user_name: str,
                   on_progress: Optional[Callable[[Dict], None]] = None) -> Dict[str, Any]:
        """
        Extrae datos de un archivo PDF usando el flujo completo de 3 pasos
        
        Args:
            pdf_path: Ruta al archivo PDF
            user_name: Nombre del usuario que sube el documento
            on_progress: Función callback para reportar progreso
            
        Returns:
            Dict con los datos extraídos y metadata
        """
        try:
            # Validar archivo
            import os
            if not os.path.exists(pdf_path):
                raise FileNotFoundError(f"Archivo no encontrado: {pdf_path}")
                
            file_size = os.path.getsize(pdf_path)
            if file_size > 50 * 1024 * 1024:
                raise ValueError("El archivo excede el tamaño máximo de 50MB")
            
            # Leer y convertir a base64
            with open(pdf_path, 'rb') as f:
                pdf_bytes = f.read()
                base64_pdf = base64.b64encode(pdf_bytes).decode('utf-8')
            
            # Paso 1: INIT - Obtener configuración del servidor
            init_data = self._init_upload()
            upload_id = init_data['upload_id']
            chunk_size = init_data['chunk_size']
            total_chunks = init_data['total_chunks']
            
            print(f"Sesión iniciada. Upload ID: {upload_id}")
            print(f"Total chunks: {total_chunks}, tamaño: {chunk_size} bytes")
            
            # Dividir en chunks usando el tamaño del servidor
            chunks = self._split_into_chunks(base64_pdf, chunk_size)
            
            if len(chunks) != total_chunks:
                print(f"Advertencia: chunks calculados ({len(chunks)}) difiere del servidor ({total_chunks})")
            
            # Paso 2: UPLOAD-CHUNK - chunks indexados desde 0
            for i, chunk in enumerate(chunks):
                progress = self._upload_chunk(upload_id, i, chunk)  # i empieza en 0
                
                if on_progress:
                    on_progress({
                        'percent': progress['progress_percent'],
                        'message': progress['message'],
                        'uploaded_chunks': progress['uploaded_chunks'],
                        'total_chunks': progress['total_chunks'],
                        'is_complete': progress['is_complete']
                    })
            
            # Paso 3: COMPLETE
            result = self._complete_upload(upload_id, user_name)
            return result
            
        except Exception as e:
            print(f"Error en extracción de PDF: {str(e)}")
            raise
    
    def _init_upload(self) -> Dict:
        """Inicializa la sesión de carga"""
        response = requests.post(
            f'{self.base_url}/v4/document/extract/pdf/init',
            headers={
                'Content-Type': 'application/json',
                'Request-Id': str(uuid.uuid4()),
                'Language': self.language,
                'Authorization': f'Bearer {self.api_key}'
            },
            json={}  # Body vacío
        )
        
        response.raise_for_status()
        return response.json()
    
    def _upload_chunk(self, upload_id: str, chunk_number: int,
                     chunk_data: str, max_retries: int = 3) -> Dict:
        """Envía un chunk individual con retry logic"""
        last_error = None
        
        for attempt in range(1, max_retries + 1):
            try:
                response = requests.post(
                    f'{self.base_url}/v4/document/extract/pdf/upload-chunk',
                    headers={
                        'Content-Type': 'application/json',
                        'Request-Id': str(uuid.uuid4()),
                        'Language': self.language,
                        'Authorization': f'Bearer {self.api_key}'
                    },
                    json={
                        'upload_id': upload_id,
                        'chunk_number': chunk_number,  # 0-indexed
                        'chunk_data': chunk_data
                    },
                    timeout=30
                )
                
                response.raise_for_status()
                return response.json()
                
            except Exception as e:
                last_error = e
                
                if attempt < max_retries:
                    # Exponential backoff
                    sleep_time = 2 ** (attempt - 1)
                    print(f"Reintentando chunk {chunk_number}, intento {attempt + 1}/{max_retries}")
                    time.sleep(sleep_time)
        
        raise last_error
    
    def _complete_upload(self, upload_id: str, user_name: str) -> Dict:
        """Finaliza y procesa el documento"""
        response = requests.post(
            f'{self.base_url}/v4/document/extract/pdf/complete',
            headers={
                'Content-Type': 'application/json',
                'Request-Id': str(uuid.uuid4()),
                'Language': self.language,
                'Authorization': f'Bearer {self.api_key}'
            },
            json={
                'upload_id': upload_id,
                'name': user_name  # Opcional
            },
            timeout=120  # Mayor timeout para procesamiento OCR
        )
        
        response.raise_for_status()
        return response.json()
    
    def _split_into_chunks(self, base64_string: str, chunk_size: int) -> list:
        """Divide el string base64 en chunks del tamaño especificado"""
        chunks = []
        for i in range(0, len(base64_string), chunk_size):
            chunks.append(base64_string[i:i + chunk_size])
        return chunks


# ============================================
# EJEMPLO DE USO
# ============================================

def progress_callback(progress):
    """Callback para mostrar progreso"""
    print(f"Progreso: {progress['percent']}% - {progress['message']}")
    print(f"Chunks: {progress['uploaded_chunks']}/{progress['total_chunks']}")

# Inicializar extractor
extractor = JAKPDFExtractor(
    api_key='your-api-key-here',
    base_url='https://api.jaak.mx',
    language='es'
)

# Extraer PDF
try:
    result = extractor.extract_pdf(
        pdf_path='acta_constitutiva.pdf',
        user_name='juan_perez',
        on_progress=progress_callback
    )
    
    print("\nExtracción completada!")
    print(f"Tipo: {result['content']['extra']['document_type']}")
    print(f"País: {result['content']['extra']['country_code']}")
    print(f"Páginas: {result['content']['extra']['page_count']}")
    print(f"Tiempo: {result['processingTime']}ms")
    print(f"\nDatos extraídos:")
    
    import json
    print(json.dumps(result['content']['data'], indent=2, ensure_ascii=False))
    
except Exception as e:
    print(f"Error: {str(e)}")

Puntos Importantes

Indexación de Chunks (Zero-based)

Los chunk_number empiezan en 0, no en 1:

Chunk 0 = primer chunk
Chunk 1 = segundo chunk
...
Chunk 18 = último chunk (para un total de 19 chunks)

Configuración Automática del Servidor

El servidor determina automáticamente:

  • chunk_size: Típicamente 1048576 bytes (1MB)
  • total_chunks: Calculado basado en el tamaño esperado del archivo

No necesitas calcular estos valores manualmente en el INIT.

Orden de Envío de Chunks

Los chunks pueden enviarse en cualquier orden. El servidor los ensamblará correctamente. Sin embargo, se recomienda enviarlos secuencialmente para facilitar el tracking.

Expiración de Sesión

Las sesiones expiran 1 hora después de crearse. El campo expires_at indica el timestamp exacto de expiración.


Manejo de Errores

Códigos de Error Comunes

Código HTTPError CodeDescripciónSolución
400INVALID_FILE_SIZEArchivo excede 50MBReducir tamaño o comprimir PDF
400INVALID_FORMATArchivo no es PDF válidoVerificar formato del archivo
400CHUNK_MISMATCHNúmero de chunk incorrectoVerificar secuencia de chunks
400INCOMPLETE_UPLOADFaltan chunksVerificar que se enviaron todos (0 a N-1)
404INVALID_UPLOAD_SESSIONSesión expirada o no existeReiniciar proceso desde INIT
401UNAUTHORIZEDToken inválido o expiradoVerificar API key
429RATE_LIMIT_EXCEEDEDDemasiadas solicitudesImplementar rate limiting
500PROCESSING_FAILEDError en procesamiento OCRReintentar o contactar soporte

Ejemplos de Respuestas de Error

Chunks Incompletos

{
  "errorCode": "INCOMPLETE_UPLOAD",
  "message": "Missing chunks: 3, 7, 12",
  "statusCode": 400
}

Solución: Verificar que enviaste todos los chunks (0 a N-1).

Sesión Expirada

{
  "errorCode": "INVALID_UPLOAD_SESSION",
  "message": "Upload session expired or not found",
  "statusCode": 404
}

Solución: Reiniciar el proceso desde INIT.

Archivo muy Grande

{
  "errorCode": "INVALID_FILE_SIZE",
  "message": "File size exceeds 50MB limit",
  "statusCode": 400
}

Solución: Reducir el tamaño del PDF o comprimirlo.


Mejores Prácticas

1. Validación Previa

function validatePDF(file) {
  // Verificar tipo
  if (file.type !== 'application/pdf') {
    throw new Error('Solo se permiten archivos PDF');
  }
  
  // Verificar tamaño
  if (file.size > 50 * 1024 * 1024) {
    throw new Error('El archivo excede los 50MB permitidos');
  }
  
  // Verificar extensión
  if (!file.name.toLowerCase().endsWith('.pdf')) {
    throw new Error('La extensión del archivo debe ser .pdf');
  }
  
  return true;
}

2. Manejo de Sesión con LocalStorage

class UploadSession {
  constructor(uploadId) {
    this.uploadId = uploadId;
    this.startTime = Date.now();
    this.uploadedChunks = new Set();
  }
  
  markChunkUploaded(chunkNumber) {
    this.uploadedChunks.add(chunkNumber);
    localStorage.setItem(
      `upload_${this.uploadId}`,
      JSON.stringify({
        uploadId: this.uploadId,
        uploadedChunks: Array.from(this.uploadedChunks),
        startTime: this.startTime
      })
    );
  }
  
  static restore(uploadId) {
    const data = localStorage.getItem(`upload_${uploadId}`);
    if (!data) return null;
    
    const session = JSON.parse(data);
    const restored = new UploadSession(session.uploadId);
    restored.uploadedChunks = new Set(session.uploadedChunks);
    restored.startTime = session.startTime;
    
    return restored;
  }
  
  getMissingChunks(totalChunks) {
    const missing = [];
    for (let i = 0; i < totalChunks; i++) {
      if (!this.uploadedChunks.has(i)) {
        missing.push(i);
      }
    }
    return missing;
  }
}

3. UI de Progreso Profesional

function createProgressUI() {
  return {
    container: null,
    
    init() {
      this.container = document.getElementById('progress-container');
      this.container.innerHTML = `
        <div class="upload-progress">
          <div class="progress-bar-wrapper">
            <div class="progress-bar" id="progress-bar"></div>
          </div>
          <div class="progress-text">
            <span id="progress-percent">0%</span>
            <span id="progress-message">Preparando...</span>
          </div>
          <div class="upload-stats">
            <span id="uploaded-chunks">0</span> / 
            <span id="total-chunks">0</span> chunks
          </div>
          <button id="cancel-btn" class="btn-cancel">Cancelar</button>
        </div>
      `;
    },
    
    update(progress) {
      document.getElementById('progress-bar').style.width = `${progress.percent}%`;
      document.getElementById('progress-percent').textContent = `${progress.percent}%`;
      document.getElementById('progress-message').textContent = progress.message;
      document.getElementById('uploaded-chunks').textContent = progress.uploadedChunks;
      document.getElementById('total-chunks').textContent = progress.totalChunks;
    },
    
    complete(result) {
      this.container.innerHTML = `
        <div class="upload-complete">
          <h3>Extracción Completada</h3>
          <p>Documento procesado exitosamente en ${result.processingTime}ms</p>
          <p><strong>Tipo:</strong> ${result.content.extra.document_type}</p>
          <p><strong>Páginas:</strong> ${result.content.extra.page_count}</p>
        </div>
      `;
    },
    
    error(message) {
      this.container.innerHTML = `
        <div class="upload-error">
          <h3>Error</h3>
          <p>${message}</p>
          <button onclick="location.reload()">Reintentar</button>
        </div>
      `;
    }
  };
}

4. Retry Logic con Exponential Backoff

import time
from typing import Callable, Any

def retry_with_backoff(
    func: Callable,
    max_retries: int = 3,
    base_delay: float = 1.0
) -> Any:
    """
    Ejecuta una función con retry exponencial
    
    Args:
        func: Función a ejecutar
        max_retries: Número máximo de reintentos
        base_delay: Delay base en segundos
        
    Returns:
        Resultado de la función
    """
    last_error = None
    
    for attempt in range(1, max_retries + 1):
        try:
            return func()
        except Exception as e:
            last_error = e
            
            if attempt < max_retries:
                delay = base_delay * (2 ** (attempt - 1))
                print(f"Reintento {attempt}/{max_retries} en {delay}s...")
                time.sleep(delay)
    
    raise last_error

Límites y Restricciones

ParámetroValorNotas
Tamaño máximo de archivo50MBLímite estricto
Tamaño de chunk (servidor)1MB (1048576 bytes)Configurado automáticamente
Chunks mínimos1Para archivos pequeños
Expiración de sesión1 horaDesde INIT
Máximo de reintentos3Por chunk recomendado
Timeout INIT10s
Timeout UPLOAD-CHUNK30sPor chunk
Timeout COMPLETE120sProcesamiento OCR
Rate limit100 req/minPor API key

Tipos de Documentos Soportados

Este endpoint está especializado en la extracción de documentos PDF complejos con múltiples páginas y estructura de texto compleja. Es ideal para documentos legales, notariales y corporativos.

Acta Constitutiva (MX)

Campos extraídos:

  • capital_shareholders: Estructura accionaria
    • founding_shareholders: Lista de socios fundadores con porcentajes y acciones
    • minimum_fixed_capital: Capital social mínimo
    • minimum_capital_share_type: Tipo de acciones
  • company_data: Información de la empresa
    • full_name: Razón social completa
    • corporate_type: Tipo societario (S.A. DE C.V., S.A.P.I. DE C.V., etc.)
    • deed_date: Fecha de escritura
    • deed_location: Lugar de constitución
    • registered_address: Domicilio social
    • duration: Duración de la sociedad
  • initial_governing_bodies: Órganos de gobierno
    • board_chairman: Presidente del consejo
    • board_secretary: Secretario
    • commissioner: Comisario
  • main_corporate_purpose: Objeto social (array)

Nota: Todos los campos incluyen referencias de página [p. N]

CFE (Comisión Federal de Electricidad)

Campos extraídos:

  • holder_name: Nombre del titular
  • service_address: Dirección de suministro
  • service_number: Número de servicio
  • rate: Tarifa contratada
  • consumption: Consumo del período
  • amount_to_pay: Monto a pagar
  • billing_period: Período de facturación

Otros Documentos PDF

El endpoint puede procesar cualquier documento PDF y extraerá la información de forma estructurada según el contenido detectado. Los documentos soportados incluyen:

  • Contratos comerciales
  • Documentos notariales
  • Estados financieros
  • Reportes corporativos
  • Documentos legales en general

Nota: Para documentos como INE, Pasaporte u otras identificaciones oficiales, consulta con el equipo de JAAK sobre endpoints especializados para estos tipos de documentos.


Casos de Uso

1. Verificación Empresarial (KYB - Know Your Business)

async function onboardBusiness(businessId, documents) {
  const extractor = new JAKPDFExtractor(API_KEY);
  const results = {};
  
  try {
    // Extraer Acta Constitutiva
    if (documents.actaConstitutiva) {
      results.actaConstitutiva = await extractor.extractPDF(
        documents.actaConstitutiva, 
        businessId
      );
      
      // Validar capital mínimo
      const capital = parseFloat(
        results.actaConstitutiva.content.data.capital_shareholders
          .minimum_fixed_capital.replace(/[^0-9]/g, '')
      );
      
      if (capital < 50000) {
        throw new Error('Capital social insuficiente');
      }
    }
    
    // Extraer Comprobante de domicilio (CFE)
    if (documents.proofOfAddress) {
      results.address = await extractor.extractPDF(
        documents.proofOfAddress,
        businessId
      );
    }
    
    // Guardar en base de datos
    await saveBusinessData(businessId, results);
    
    return {
      success: true,
      businessId,
      data: results
    };
    
  } catch (error) {
    return {
      success: false,
      businessId,
      error: error.message
    };
  }
}

2. Análisis de Estructura Corporativa

async function analyzeBusinessStructure(businessId, constitutiveAct) {
  const extractor = new JAKPDFExtractor(API_KEY);
  
  const result = await extractor.extractPDF(constitutiveAct, businessId);
  const businessData = result.content.data;
  
  // Validaciones
  const validation = {
    hasValidCapital: parseFloat(
      businessData.capital_shareholders.minimum_fixed_capital
        .replace(/[^0-9]/g, '')
    ) >= 50000,
    
    hasLegalRepresentative: !!businessData.initial_governing_bodies.board_chairman,
    
    hasValidCorporateType: [
      'S.A. DE C.V.',
      'S.A.P.I. DE C.V.',
      'S. DE R.L. DE C.V.'
    ].some(type => 
      businessData.company_data.corporate_type.includes(type)
    ),
    
    isActive: businessData.company_data.duration === 'Indefinida'
  };
  
  return {
    businessId,
    isValid: Object.values(validation).every(v => v === true),
    validation,
    extractedData: businessData
  };
}

3. Procesamiento en Lote

import asyncio
from concurrent.futures import ThreadPoolExecutor

async def process_documents_batch(pdf_files: list, max_concurrent: int = 5):
    """Procesa múltiples PDFs con límite de concurrencia"""
    
    extractor = JAKPDFExtractor(API_KEY)
    
    def process_single(pdf_path: str, user_name: str):
        try:
            result = extractor.extract_pdf(pdf_path, user_name)
            return {
                'success': True,
                'file': pdf_path,
                'data': result
            }
        except Exception as e:
            return {
                'success': False,
                'file': pdf_path,
                'error': str(e)
            }
    
    # Procesar en lotes para respetar rate limits
    results = []
    with ThreadPoolExecutor(max_workers=max_concurrent) as executor:
        futures = []
        
        for pdf_path in pdf_files:
            user_name = pdf_path.split('/')[-1].replace('.pdf', '')
            future = executor.submit(process_single, pdf_path, user_name)
            futures.append(future)
        
        for future in futures:
            results.append(future.result())
    
    # Estadísticas
    successful = sum(1 for r in results if r['success'])
    failed = len(results) - successful
    
    return {
        'total': len(results),
        'successful': successful,
        'failed': failed,
        'results': results
    }

# Uso
files = ['doc1.pdf', 'doc2.pdf', 'doc3.pdf']
batch_result = await process_documents_batch(files)
print(f"Procesados: {batch_result['successful']}/{batch_result['total']}")

Seguridad y Privacidad

Manejo de Datos

  • No almacenamiento persistente: Los documentos se procesan en memoria y se eliminan después del procesamiento
  • Expiración de sesiones: Las sesiones de upload expiran después de 1 hora automáticamente
  • Cifrado en tránsito: Todas las comunicaciones usan HTTPS/TLS 1.3
  • Aislamiento de datos: Cada sesión está aislada por upload_id único

Cumplimiento

  • Cumplimiento con Ley Federal de Protección de Datos Personales en Posesión de Particulares (México)
  • Los datos extraídos son responsabilidad del cliente integrador
  • Auditoría completa mediante Request-Id y eventId

Recomendaciones

  1. Nunca almacenar API keys en el frontend
// MAL - API key expuesta
const extractor = new JAKPDFExtractor('sk_live_123456789');

// BIEN - Usar backend proxy
async function extractPDF(file) {
  const formData = new FormData();
  formData.append('file', file);
  
  const response = await fetch('/api/extract-pdf', {
    method: 'POST',
    body: formData
  });
  
  return response.json();
}
  1. Validar archivos antes de enviar
  2. Implementar rate limiting en tu backend
  3. Sanitizar datos extraídos antes de mostrar al usuario
  4. Usar HTTPS en todas las comunicaciones

Soporte y Contacto

Documentación

  • Portal de documentación: https://docs.jaak.mx
  • API Reference: https://api.jaak.mx/docs
  • Guías y tutoriales: https://docs.jaak.mx/guides

Soporte Técnico

  • Email: [email protected]
  • Slack: Canal #api-support (clientes enterprise)
  • WhatsApp Business: +52 XXX XXX XXXX
  • Horario: Lunes a Viernes, 9:00 AM - 6:00 PM (Hora de México)

Reportar Issues

Si encuentras un problema, por favor incluye:

  1. Request-Id: UUID de la solicitud fallida
  2. Event-Id: Si está disponible en la respuesta
  3. Descripción: Comportamiento esperado vs. actual
  4. Logs: Sin exponer datos sensibles
  5. Reproducción: Pasos para reproducir el error

Ejemplo de reporte:

Subject: Error en procesamiento de Acta Constitutiva

Request-Id: 63800ae6-cae1-4849-a9f0-a596dece4cd5
Event-Id: 4b2d569e-d93c-4e33-84f3-1098a107977d
Endpoint: POST /v4/document/extract/pdf/complete

Descripción:
El procesamiento se completó pero falta el campo "board_secretary" 
en initial_governing_bodies a pesar de estar presente en el documento.

Timestamp: 2026-01-19T20:30:00Z