Skip to content

File Upload Guide

Learn how to upload financial documents to Laminr for processing.

Overview

Files are uploaded to packages. A package is a container that groups related documents together (e.g., all documents for a single loan application).

Workflow:

  1. Create a package
  2. Upload one or more files to the package
  3. Monitor processing status
  4. Retrieve results

See the Getting Started guide for a complete walkthrough.

Supported File Types

Laminr accepts various financial document types:

  • Bank statements: PDF, PNG, JPEG
  • Pay stubs: PDF, PNG, JPEG
  • Tax returns: PDF
  • W-2 forms: PDF, PNG, JPEG

File requirements:

  • Maximum file size: 1 GB per file
  • Recommended: 300 DPI or higher for scanned documents
  • PDF format preferred for best results

Best Quality

For best extraction accuracy, use high-quality PDFs generated directly from financial institutions rather than scanned documents.

Upload Files to a Package

File uploads use a three-step process with presigned URLs for secure, direct-to-storage uploads:

Step 1: Get a Presigned Upload URL

Endpoint:

POST /api/v1/files

Request:

curl -X POST https://api.laminr.ai/api/v1/files \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_name": "bank_statement.pdf"}'

Parameters:

Parameter Type Required Description
file_name string Yes The name of the file you want to upload
content_type string No The MIME type of the file. Defaults to application/pdf when omitted. Required for non-PDF uploads (e.g. image/png, image/jpeg)

Response:

{
  "upload_url": "https://storage.example.com/presigned-url-with-credentials...",
  "uri": "tenants/123/files/1699123456-789-bank-statement-pdf"
}

The upload_url is a temporary URL that lets you upload directly to cloud storage. The uri is the permanent identifier for the file that you'll use in Step 3.

Match the content type

The upload_url is generated for the content_type you pass in Step 1 (or application/pdf by default). Use the same Content-Type header when uploading in Step 2, otherwise the storage service may reject the upload. Because the default is application/pdf, any non-PDF file (PNG, JPEG, …) must pass an explicit content_type in Step 1 and send that same MIME type on the Step 2 upload.

Step 2: Upload File to Presigned URL

Upload your file directly to the URL from Step 1 using HTTP PUT:

curl -X PUT "https://storage.example.com/presigned-url-with-credentials..." \
  --upload-file /path/to/bank_statement.pdf \
  -H "Content-Type: application/pdf"

Direct Upload

This upload goes directly to cloud storage (not through the Laminr API), which provides better performance and reliability for large files.

Step 3: Create the File Record in Your Package

After uploading, create the file record in your package:

Endpoint:

POST /api/v1/packages/{package_id}/files

Request:

curl -X POST https://api.laminr.ai/api/v1/packages/LP-2025-001/files \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "uri": "tenants/123/files/1699123456-789-bank-statement-pdf",
    "file_name": "bank_statement.pdf"
  }'

Parameters:

Parameter Type Required Description
uri string Yes The URI returned from Step 1
file_name string Yes The name of the file

Response:

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "loan_package_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "file_name": "bank_statement.pdf",
  "file_extension": "pdf",
  "file_size": 245678,
  "file_md5_hash": "abc123def456...",
  "file_content_type": "application/pdf",
  "file_created_at": "2025-11-05T10:05:00.000000+00:00",
  "status": "queued",
  "created_at": "2025-11-05T10:05:00.000000+00:00",
  "updated_at": "2025-11-05T10:05:00.000000+00:00",
  "progress": null
}

Upload Multiple Files

You can upload multiple files to the same package. Each file is processed independently. Repeat the three-step process for each file:

# File 1
curl -X POST https://api.laminr.ai/api/v1/files \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_name": "bank_statement_jan.pdf"}'
# Returns upload_url and uri
curl -X PUT "<upload_url>" --upload-file bank_statement_jan.pdf
curl -X POST https://api.laminr.ai/api/v1/packages/LP-2025-001/files \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"uri": "<uri>", "file_name": "bank_statement_jan.pdf"}'

# File 2
curl -X POST https://api.laminr.ai/api/v1/files \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_name": "bank_statement_feb.pdf"}'
# Returns upload_url and uri
curl -X PUT "<upload_url>" --upload-file bank_statement_feb.pdf
curl -X POST https://api.laminr.ai/api/v1/packages/LP-2025-001/files \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"uri": "<uri>", "file_name": "bank_statement_feb.pdf"}'

Check Processing Status

After uploading, files go through several processing stages:

Status Description
queued File has been uploaded and is waiting to be processed
processing Document is being analyzed and data extracted
processed Processing finished successfully
failed Processing failed

Check Package Status

Get the overall status of all files in a package:

curl https://api.laminr.ai/api/v1/packages/LP-2025-001 \
  -H "x-api-key: YOUR_API_KEY"

Response:

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "tenant": {
    "id": "tenant_123",
    "name": "Your Company"
  },
  "title": "John Doe Application",
  "public_id": "LP-2025-001",
  "created_at": "2025-11-05T10:00:00.000000+00:00",
  "updated_at": "2025-11-05T10:05:30.000000+00:00",
  "status": "Processing",
  "created_by": {
    "id": "user_123",
    "email": "you@example.com"
  },
  "progress": 0.5,
  "under_review": false
}

To see individual file statuses, list the files in the package:

curl https://api.laminr.ai/api/v1/packages/LP-2025-001/files \
  -H "x-api-key: YOUR_API_KEY"

The package status will be one of:

  • "Processing": Files are still being processed
  • "Processed": All files have been processed successfully
  • "Under Review": At least one file or summary errored and the package needs manual review

The progress field (0.0 to 1.0) indicates overall package processing progress.

Error Handling

All API errors are returned as a flat JSON envelope with the following shape:

{
  "code": 400,
  "error": "FileValidationError",
  "message": "File URI is not scoped to the loan package's tenant",
  "detail": "File URI is not scoped to the loan package's tenant"
}
Field Description
code The HTTP status code (also returned as the response status)
error The error class name (e.g. FileValidationError, RateLimitedError)
message A human-readable description of what went wrong
detail Additional detail; usually identical to message

Common upload errors:

HTTP status error When it happens
400 FileValidationError Missing/invalid file_name or uri, or a uri not scoped to the package's tenant
404 (not found) The package ID does not exist or is not visible to your API key
429 RateLimitedError Too many requests. Honor the Retry-After response header before retrying

Processing Errors

If a file fails to process, its status becomes failed. Fetch the file (or list the package's files) to see the failed state:

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "loan_package_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "file_name": "bank_statement.pdf",
  "file_extension": "pdf",
  "file_content_type": "application/pdf",
  "status": "failed",
  "created_at": "2025-11-05T10:05:00.000000+00:00",
  "updated_at": "2025-11-05T10:06:00.000000+00:00",
  "progress": null
}

When a file fails, the package status moves to "Under Review" so it can be inspected manually.

Best Practices

File Quality

  • Use PDF format when possible
  • Ensure text is readable (not too blurry or low resolution)
  • Avoid heavily compressed images
  • For scans, use at least 300 DPI

Batch Uploads

Upload files concurrently to save time:

import asyncio
import aiohttp

async def upload_file(session, package_id, filepath, api_key):
    headers = {'x-api-key': api_key}

    # Step 1: Get presigned URL
    async with session.post(
        'https://api.laminr.ai/api/v1/files',
        json={'file_name': filepath},
        headers=headers
    ) as response:
        data = await response.json()
        upload_url = data['upload_url']
        uri = data['uri']

    # Step 2: Upload file to presigned URL
    with open(filepath, 'rb') as f:
        file_data = f.read()
    async with session.put(upload_url, data=file_data) as response:
        await response.read()

    # Step 3: Create file record in package
    async with session.post(
        f'https://api.laminr.ai/api/v1/packages/{package_id}/files',
        json={'uri': uri, 'file_name': filepath},
        headers=headers
    ) as response:
        return await response.json()

async def batch_upload(package_id, filepaths, api_key):
    async with aiohttp.ClientSession() as session:
        tasks = [upload_file(session, package_id, fp, api_key) for fp in filepaths]
        return await asyncio.gather(*tasks)

# Upload 3 files concurrently
API_KEY = 'your_api_key_here'
files = ['statement1.pdf', 'statement2.pdf', 'paystub.pdf']
results = asyncio.run(batch_upload('LP-2025-001', files, API_KEY))

Monitoring Progress

Poll the package status endpoint to monitor processing:

import time
import requests

def wait_for_completion(package_id):
    headers = {'x-api-key': API_KEY}

    while True:
        response = requests.get(
            f'https://api.laminr.ai/api/v1/packages/{package_id}',
            headers=headers
        )
        package = response.json()

        if package['status'] == 'Processed':
            print("Processing complete!")
            return package
        elif package['status'] == 'Under Review':
            print("Package needs manual review (a file may have failed).")
            return package

        # Show progress (0.0 to 1.0)
        progress = package['progress']
        print(f"Progress: {progress * 100:.0f}%")

        time.sleep(5)  # Wait 5 seconds before checking again

result = wait_for_completion('LP-2025-001')

Supported Document Types

Bank Statements

  • Extract transactions, balances, account information
  • Supports most major US banks
  • Best results with statements covering 2-3 months

Pay Stubs

  • Extract income, deductions, YTD totals
  • Supports standard pay stub formats
  • Include multiple pay periods for better accuracy

Tax Returns

  • Extract income, deductions, and tax information
  • Supports Form 1040 and common schedules
  • W-2 forms can be uploaded separately

Next Steps

Support

Need help? Contact us at support@laminr.ai