Skip to main content
After creating an invoice, you can download the associated PDF and XML documents. The Factus API returns these documents as Base64-encoded strings, which you can decode and save to your filesystem.

Overview

The API provides two endpoints for downloading invoice documents:
  • PDF Download: /api/v1/invoices/{number}/pdf
  • XML Download: /api/v1/invoices/{number}/xml
Both endpoints require authentication and return Base64-encoded file content.

Prerequisites

Before downloading documents, ensure you have:
  • ✅ Local JWT token (from /api/v1/auth/login)
  • ✅ Factus access token (from /api/v1/auth/factus/login)
  • ✅ Invoice number (returned from invoice creation)

Downloading PDF Documents

1

Get the Invoice Number

When you create an invoice, the response includes a number field:
{
  "success": true,
  "message": "Factura creada exitosamente",
  "data": {
    "number": "SETP990000123",
    "prefix": "SETP",
    "cufe": "abc123...",
    "qr_url": "https://..."
  }
}
Use this number value to download the PDF.
2

Request the PDF

Make a GET request to the PDF endpoint:
curl -X GET "http://localhost:8000/api/v1/invoices/SETP990000123/pdf" \
  -H "Authorization: Bearer YOUR_LOCAL_JWT_TOKEN" \
  -H "X-Factus-Token: YOUR_FACTUS_TOKEN"
Response:
{
  "success": true,
  "message": "PDF obtenido exitosamente",
  "data": {
    "file_name": "SETP990000123.pdf",
    "file_content": "JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9UeXBlL...",
    "extension": "pdf"
  }
}
3

Decode and Save the File

The file_content field contains the Base64-encoded PDF. Decode it and save to a file.See Code Examples below for implementation details.

Downloading XML Documents

The process for downloading XML documents is identical to PDF downloads:
curl -X GET "http://localhost:8000/api/v1/invoices/SETP990000123/xml" \
  -H "Authorization: Bearer YOUR_LOCAL_JWT_TOKEN" \
  -H "X-Factus-Token: YOUR_FACTUS_TOKEN"
Response:
{
  "success": true,
  "message": "XML obtenido exitosamente",
  "data": {
    "file_name": "SETP990000123.xml",
    "file_content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVR...",
    "extension": "xml"
  }
}

Understanding Base64 Encoding

Base64 is a binary-to-text encoding scheme that represents binary data in an ASCII string format. The Factus API uses Base64 encoding to safely transmit PDF and XML files through JSON responses.
Base64 encoding increases the file size by approximately 33%. A 100KB PDF will be roughly 133KB when Base64-encoded.

Why Base64?

  • JSON Compatibility: Binary data (like PDFs) cannot be directly embedded in JSON. Base64 converts binary to text.
  • Safe Transport: Ensures data integrity during transmission over HTTP.
  • Standard Format: Widely supported across all programming languages.

Response Structure

Both PDF and XML endpoints return the same response structure:
file_name
string
The suggested filename for saving the document (e.g., "SETP990000123.pdf").
file_content
string
Base64-encoded file content. Decode this string to get the actual file bytes.
extension
string
File extension: "pdf" or "xml".

Code Examples

Python

import httpx
import base64
from pathlib import Path

async def download_invoice_pdf(
    base_url: str,
    local_token: str,
    factus_token: str,
    invoice_number: str,
    output_dir: str = "./downloads"
) -> str:
    """
    Download and save invoice PDF.
    
    Args:
        base_url: API base URL
        local_token: Local JWT token
        factus_token: Factus access token
        invoice_number: Invoice number (e.g., "SETP990000123")
        output_dir: Directory to save the PDF
    
    Returns:
        Path to the saved PDF file
    """
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"{base_url}/api/v1/invoices/{invoice_number}/pdf",
            headers={
                "Authorization": f"Bearer {local_token}",
                "X-Factus-Token": factus_token
            }
        )
        response.raise_for_status()
        
        result = response.json()
        file_data = result["data"]
        
        # Decode Base64 content
        file_bytes = base64.b64decode(file_data["file_content"])
        
        # Save to file
        output_path = Path(output_dir) / file_data["file_name"]
        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_bytes(file_bytes)
        
        return str(output_path)

async def download_invoice_xml(
    base_url: str,
    local_token: str,
    factus_token: str,
    invoice_number: str,
    output_dir: str = "./downloads"
) -> str:
    """
    Download and save invoice XML.
    
    Args:
        base_url: API base URL
        local_token: Local JWT token
        factus_token: Factus access token
        invoice_number: Invoice number
        output_dir: Directory to save the XML
    
    Returns:
        Path to the saved XML file
    """
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"{base_url}/api/v1/invoices/{invoice_number}/xml",
            headers={
                "Authorization": f"Bearer {local_token}",
                "X-Factus-Token": factus_token
            }
        )
        response.raise_for_status()
        
        result = response.json()
        file_data = result["data"]
        
        # Decode Base64 content
        file_bytes = base64.b64decode(file_data["file_content"])
        
        # Save to file
        output_path = Path(output_dir) / file_data["file_name"]
        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_bytes(file_bytes)
        
        return str(output_path)

# Usage
pdf_path = await download_invoice_pdf(
    base_url="http://localhost:8000",
    local_token="your_jwt_token",
    factus_token="your_factus_token",
    invoice_number="SETP990000123"
)
print(f"PDF saved to: {pdf_path}")

xml_path = await download_invoice_xml(
    base_url="http://localhost:8000",
    local_token="your_jwt_token",
    factus_token="your_factus_token",
    invoice_number="SETP990000123"
)
print(f"XML saved to: {xml_path}")

JavaScript/TypeScript

import fs from 'fs';
import path from 'path';

interface DownloadResponse {
  file_name: string;
  file_content: string;
  extension: string;
}

async function downloadInvoicePDF(
  baseUrl: string,
  localToken: string,
  factusToken: string,
  invoiceNumber: string,
  outputDir: string = './downloads'
): Promise<string> {
  const response = await fetch(
    `${baseUrl}/api/v1/invoices/${invoiceNumber}/pdf`,
    {
      headers: {
        'Authorization': `Bearer ${localToken}`,
        'X-Factus-Token': factusToken
      }
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to download PDF: ${response.statusText}`);
  }

  const result = await response.json();
  const fileData: DownloadResponse = result.data;

  // Decode Base64 content
  const fileBuffer = Buffer.from(fileData.file_content, 'base64');

  // Ensure output directory exists
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }

  // Save to file
  const outputPath = path.join(outputDir, fileData.file_name);
  fs.writeFileSync(outputPath, fileBuffer);

  return outputPath;
}

async function downloadInvoiceXML(
  baseUrl: string,
  localToken: string,
  factusToken: string,
  invoiceNumber: string,
  outputDir: string = './downloads'
): Promise<string> {
  const response = await fetch(
    `${baseUrl}/api/v1/invoices/${invoiceNumber}/xml`,
    {
      headers: {
        'Authorization': `Bearer ${localToken}`,
        'X-Factus-Token': factusToken
      }
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to download XML: ${response.statusText}`);
  }

  const result = await response.json();
  const fileData: DownloadResponse = result.data;

  // Decode Base64 content
  const fileBuffer = Buffer.from(fileData.file_content, 'base64');

  // Ensure output directory exists
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }

  // Save to file
  const outputPath = path.join(outputDir, fileData.file_name);
  fs.writeFileSync(outputPath, fileBuffer);

  return outputPath;
}

// Usage
const pdfPath = await downloadInvoicePDF(
  'http://localhost:8000',
  'your_jwt_token',
  'your_factus_token',
  'SETP990000123'
);
console.log(`PDF saved to: ${pdfPath}`);

const xmlPath = await downloadInvoiceXML(
  'http://localhost:8000',
  'your_jwt_token',
  'your_factus_token',
  'SETP990000123'
);
console.log(`XML saved to: ${xmlPath}`);

Browser/Frontend (JavaScript)

For browser environments, you can trigger file downloads without saving to the filesystem:
async function downloadInvoiceDocument(
  baseUrl,
  localToken,
  factusToken,
  invoiceNumber,
  documentType // 'pdf' or 'xml'
) {
  const response = await fetch(
    `${baseUrl}/api/v1/invoices/${invoiceNumber}/${documentType}`,
    {
      headers: {
        'Authorization': `Bearer ${localToken}`,
        'X-Factus-Token': factusToken
      }
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to download ${documentType.toUpperCase()}`);
  }

  const result = await response.json();
  const fileData = result.data;

  // Convert Base64 to Blob
  const byteCharacters = atob(fileData.file_content);
  const byteNumbers = new Array(byteCharacters.length);
  for (let i = 0; i < byteCharacters.length; i++) {
    byteNumbers[i] = byteCharacters.charCodeAt(i);
  }
  const byteArray = new Uint8Array(byteNumbers);
  
  const mimeType = documentType === 'pdf' 
    ? 'application/pdf' 
    : 'application/xml';
  const blob = new Blob([byteArray], { type: mimeType });

  // Trigger download
  const url = window.URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = fileData.file_name;
  link.click();
  
  // Cleanup
  window.URL.revokeObjectURL(url);
}

// Usage
await downloadInvoiceDocument(
  'http://localhost:8000',
  'your_jwt_token',
  'your_factus_token',
  'SETP990000123',
  'pdf'
);

Batch Downloads

Download both PDF and XML for an invoice:

Python

async def download_invoice_documents(
    base_url: str,
    local_token: str,
    factus_token: str,
    invoice_number: str,
    output_dir: str = "./downloads"
) -> dict[str, str]:
    """
    Download both PDF and XML for an invoice.
    
    Returns:
        Dictionary with 'pdf' and 'xml' file paths
    """
    # Download both concurrently
    pdf_task = download_invoice_pdf(
        base_url, local_token, factus_token, invoice_number, output_dir
    )
    xml_task = download_invoice_xml(
        base_url, local_token, factus_token, invoice_number, output_dir
    )
    
    pdf_path, xml_path = await asyncio.gather(pdf_task, xml_task)
    
    return {
        "pdf": pdf_path,
        "xml": xml_path
    }

# Usage
import asyncio

paths = await download_invoice_documents(
    base_url="http://localhost:8000",
    local_token="your_jwt_token",
    factus_token="your_factus_token",
    invoice_number="SETP990000123"
)
print(f"Downloaded: {paths}")

TypeScript

async function downloadInvoiceDocuments(
  baseUrl: string,
  localToken: string,
  factusToken: string,
  invoiceNumber: string,
  outputDir: string = './downloads'
): Promise<{ pdf: string; xml: string }> {
  // Download both concurrently
  const [pdfPath, xmlPath] = await Promise.all([
    downloadInvoicePDF(baseUrl, localToken, factusToken, invoiceNumber, outputDir),
    downloadInvoiceXML(baseUrl, localToken, factusToken, invoiceNumber, outputDir)
  ]);

  return { pdf: pdfPath, xml: xmlPath };
}

// Usage
const paths = await downloadInvoiceDocuments(
  'http://localhost:8000',
  'your_jwt_token',
  'your_factus_token',
  'SETP990000123'
);
console.log(`Downloaded: ${JSON.stringify(paths)}`);

Error Handling

Invoice Not Found

{
  "detail": "Error al descargar el PDF: Invoice not found"
}
Solution: Verify the invoice number is correct and the invoice exists in Factus.

Missing Authentication

{
  "detail": "Could not validate credentials"
}
Solution: Ensure both Authorization and X-Factus-Token headers are provided.

Factus Service Error

{
  "detail": "Error al descargar el PDF: Factus API timeout"
}
Solution: The Factus service may be temporarily unavailable. Retry after a short delay.

Best Practices

For large invoices with many items, the PDF file can be several megabytes. Consider:
  • Implementing download progress indicators
  • Using streaming for very large files
  • Setting appropriate timeout values for HTTP requests
  • Store downloaded files in secure directories with restricted permissions
  • Encrypt sensitive invoice documents at rest
  • Implement access controls for document retrieval
  • Consider using cloud storage with encryption (S3, Azure Blob, etc.)
Network issues or temporary Factus service unavailability can cause download failures. Implement exponential backoff retry logic:
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=4, max=10)
)
async def download_with_retry(invoice_number: str):
    return await download_invoice_pdf(
        base_url, local_token, factus_token, invoice_number
    )
After decoding Base64 content, validate the file:
  • Check file size is non-zero
  • Verify PDF/XML file signatures (magic bytes)
  • For PDFs: Check for valid PDF header (%PDF-)
  • For XML: Validate XML structure

Troubleshooting

If you receive corrupted files after decoding, ensure you’re decoding the Base64 string correctly without modifying it. Some HTTP clients may automatically decode Base64, causing double-decoding issues.

Verify Base64 Decoding

# Python: Test Base64 decoding
import base64

def is_valid_base64(s: str) -> bool:
    try:
        base64.b64decode(s, validate=True)
        return True
    except Exception:
        return False

if not is_valid_base64(file_content):
    print("Invalid Base64 string")

Verify File Integrity

# Python: Check PDF signature
def is_valid_pdf(file_bytes: bytes) -> bool:
    return file_bytes.startswith(b'%PDF-')

# Python: Check XML structure
import xml.etree.ElementTree as ET

def is_valid_xml(file_bytes: bytes) -> bool:
    try:
        ET.fromstring(file_bytes)
        return True
    except ET.ParseError:
        return False

Next Steps

Creating Invoices

Learn how to create invoices

Error Handling

Handle errors and troubleshoot issues

Build docs developers (and LLMs) love