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ámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
Request-Id | string | Sí | UUID v4 único para rastrear la solicitud |
Language | string | Opcional | en o es (default: en) |
Authorization | string | Sí | Bearer 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."
}| Campo | Tipo | Descripción |
|---|---|---|
upload_id | string | UUID único de la sesión (usar en siguientes requests) |
file_size | integer | Tamaño total del archivo que el servidor espera recibir |
chunk_size | integer | Tamaño de cada chunk en bytes (típicamente 1048576 = 1MB) |
total_chunks | integer | Número total de chunks que debes enviar |
expires_at | string | Timestamp ISO cuando expira la sesión (1 hora desde creación) |
message | string | Mensaje 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ámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
Request-Id | string | Sí | UUID v4 único para este chunk específico |
Language | string | Opcional | en o es |
Authorization | string | Sí | Bearer token de autenticación |
Request Body
{
"upload_id": "5e29520d-dc1b-41e9-8c67-31a8dc8d71d4",
"chunk_number": 0,
"chunk_data": "JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1ID..."
}| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
upload_id | string | Sí | ID de la sesión obtenido en INIT |
chunk_number | integer | Sí | Número secuencial del chunk (0-indexed: 0, 1, 2, ..., N-1) |
chunk_data | string | Sí | Fragmento 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."
}| Campo | Tipo | Descripción |
|---|---|---|
upload_id | string | ID de la sesión |
chunk_number | integer | Número del chunk que acabas de subir |
uploaded_chunks | integer | Total de chunks únicos recibidos hasta ahora |
total_chunks | integer | Total de chunks necesarios |
remaining_chunks | integer | Chunks que faltan por subir |
progress_percent | integer | Porcentaje de progreso (0-100) |
is_complete | boolean | true cuando uploaded_chunks === total_chunks |
message | string | Mensaje 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ámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
Request-Id | string | Sí | UUID v4 único para esta solicitud |
Language | string | Opcional | en o es |
Authorization | string | Sí | Bearer token de autenticación |
Request Body
{
"upload_id": "5e29520d-dc1b-41e9-8c67-31a8dc8d71d4",
"name": "usuario_ejemplo"
}| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
upload_id | string | Sí | ID de la sesión de carga |
name | string | Opcional | Identificador 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 HTTP | Error Code | Descripción | Solución |
|---|---|---|---|
| 400 | INVALID_FILE_SIZE | Archivo excede 50MB | Reducir tamaño o comprimir PDF |
| 400 | INVALID_FORMAT | Archivo no es PDF válido | Verificar formato del archivo |
| 400 | CHUNK_MISMATCH | Número de chunk incorrecto | Verificar secuencia de chunks |
| 400 | INCOMPLETE_UPLOAD | Faltan chunks | Verificar que se enviaron todos (0 a N-1) |
| 404 | INVALID_UPLOAD_SESSION | Sesión expirada o no existe | Reiniciar proceso desde INIT |
| 401 | UNAUTHORIZED | Token inválido o expirado | Verificar API key |
| 429 | RATE_LIMIT_EXCEEDED | Demasiadas solicitudes | Implementar rate limiting |
| 500 | PROCESSING_FAILED | Error en procesamiento OCR | Reintentar 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_errorLímites y Restricciones
| Parámetro | Valor | Notas |
|---|---|---|
| Tamaño máximo de archivo | 50MB | Límite estricto |
| Tamaño de chunk (servidor) | 1MB (1048576 bytes) | Configurado automáticamente |
| Chunks mínimos | 1 | Para archivos pequeños |
| Expiración de sesión | 1 hora | Desde INIT |
| Máximo de reintentos | 3 | Por chunk recomendado |
| Timeout INIT | 10s | |
| Timeout UPLOAD-CHUNK | 30s | Por chunk |
| Timeout COMPLETE | 120s | Procesamiento OCR |
| Rate limit | 100 req/min | Por 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 accionariafounding_shareholders: Lista de socios fundadores con porcentajes y accionesminimum_fixed_capital: Capital social mínimominimum_capital_share_type: Tipo de acciones
company_data: Información de la empresafull_name: Razón social completacorporate_type: Tipo societario (S.A. DE C.V., S.A.P.I. DE C.V., etc.)deed_date: Fecha de escrituradeed_location: Lugar de constituciónregistered_address: Domicilio socialduration: Duración de la sociedad
initial_governing_bodies: Órganos de gobiernoboard_chairman: Presidente del consejoboard_secretary: Secretariocommissioner: 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 titularservice_address: Dirección de suministroservice_number: Número de serviciorate: Tarifa contratadaconsumption: Consumo del períodoamount_to_pay: Monto a pagarbilling_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-IdyeventId
Recomendaciones
- 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();
}- Validar archivos antes de enviar
- Implementar rate limiting en tu backend
- Sanitizar datos extraídos antes de mostrar al usuario
- 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:
- Request-Id: UUID de la solicitud fallida
- Event-Id: Si está disponible en la respuesta
- Descripción: Comportamiento esperado vs. actual
- Logs: Sin exponer datos sensibles
- 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
Updated about 5 hours ago
