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:
- Create a package
- Upload one or more files to the package
- Monitor processing status
- 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:
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:
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:
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:
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¶
- Getting Started Guide - Complete workflow walkthrough
- Authentication - API key management
- API Reference - Complete endpoint documentation
Support¶
Need help? Contact us at support@laminr.ai