Skip to content

Getting Started

This guide will walk you through the complete workflow of using the Laminr API to process bank statements and retrieve income calculations.

Prerequisites

Before you begin, you'll need:

  • A Laminr account
  • An API key (see Authentication guide for setup instructions)
  • Bank statements to upload

Quick Start Workflow

The typical Laminr workflow follows these steps:

  1. Create a Package - Container for your financial documents
  2. Upload Files - Add documents to the package for processing
  3. Monitor Progress - Check processing status
  4. Retrieve Results - Get income calculations and analysis

Step 1: Create a Package

A package is a container that holds related financial documents for a single applicant or loan request.

curl -X POST https://api.laminr.ai/api/v2/packages \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "John Doe Application"
  }'

Create with files in one call

POST /api/v2/packages also accepts file_names and file_uris arrays, so you can create a package and attach already-uploaded files in a single request. This guide creates the package first and attaches files in Step 2 for clarity.

Response:

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

Save the id field - you'll need it to upload files and retrieve results. (You can also use the human-readable public_id, e.g. LP-2025-001, anywhere a package id is accepted.)

Step 2: Upload Files

File uploads use a three-step process with presigned URLs:

Step 2a: Get a presigned upload URL

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"}'

Response:

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

Step 2b: Upload the file to the presigned URL

The upload_url is a resumable upload session, so the PUT must include a Content-Range header declaring the file's total size — cloud storage only finalizes the object once it knows the size:

SIZE=$(wc -c < /path/to/bank_statement.pdf)
curl -X PUT "https://storage.example.com/presigned-url..." \
  --upload-file /path/to/bank_statement.pdf \
  -H "Content-Type: application/pdf" \
  -H "Content-Range: bytes 0-$((SIZE - 1))/$SIZE"

Step 2c: Attach the file to your package

Attach the uploaded file to your package with a PATCH to the v2 package endpoint. Pass parallel file_names and file_uris arrays (up to 100 per request) — this starts the processing pipeline.

curl -X PATCH https://api.laminr.ai/api/v2/packages/LP-2025-001 \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "file_names": ["bank_statement.pdf"],
    "file_uris": ["tenants/123/files/1699123456-789-bank-statement-pdf"]
  }'

Response: the updated package.

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "title": "John Doe Application",
  "public_id": "LP-2025-001",
  "status": "Processing",
  "progress": 0.0,
  "under_review": false,
  "created_at": "2025-11-05T10:00:00.000000+00:00",
  "updated_at": "2025-11-05T10:05:00.000000+00:00"
}

Each PATCH adds the listed files to the package, so you can call it again to attach more documents.

Step 3: Monitor Progress

Check the package status to see when processing is complete.

curl https://api.laminr.ai/api/v2/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
}

(The detail response also includes settings, selected_eligibility_model, metadata, alerts, and plaid_link fields, omitted here for brevity.)

The status field will be "Processing" while files are being analyzed, then "Processed" when complete. If a file errors, the status moves to "Under Review" — see the File Upload guide for the full status reference.

Step 4: Retrieve Income Results

Once processing is complete (status: "Processed"), retrieve the income calculation models.

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

Response:

{
  "total": 1,
  "pages": 1,
  "current_page": 1,
  "has_next": false,
  "has_previous": false,
  "results": [
    {
      "id": "model_123",
      "eligibility_model_id": "em_456",
      "model_name": "Standard Income Model",
      "public_id": "MDL-2025-001",
      "status": "Success",
      "created_at": "2025-11-05T10:10:00.000000+00:00",
      "updated_at": "2025-11-05T10:15:00.000000+00:00",
      "started_at": "2025-11-05T10:10:00.000000+00:00",
      "finished_at": "2025-11-05T10:15:00.000000+00:00",
      "total_debits": "15234.50",
      "total_credits": "18450.00",
      "eligible_debit": "12000.00",
      "eligible_credit": "16500.00",
      "ineligible_debit": "3234.50",
      "ineligible_credit": "1950.00",
      "overridden_eligible_debits": "0.00",
      "overridden_eligible_credits": "0.00",
      "overridden_ineligible_debits": "0.00",
      "overridden_ineligible_credits": "0.00",
      "eligible_total_debits": "12000.00",
      "eligible_total_credits": "16500.00",
      "net_eligible_debits": "12000.00",
      "net_eligible_credits": "16500.00"
    }
  ]
}

Complete Example (Python)

Here's a complete example showing the entire workflow:

import os
import requests
import time

API_KEY = "your_api_key_here"
BASE_URL = "https://api.laminr.ai/api"

headers = {
    "x-api-key": API_KEY
}

# Step 1: Create a package
response = requests.post(
    f"{BASE_URL}/v2/packages",
    headers=headers,
    json={
        "title": "John Doe Application"
    }
)
package = response.json()
package_id = package["id"]
print(f"Created package: {package_id}")

# Step 2: Upload files
files_to_upload = [
    "bank_statement_jan.pdf",
    "bank_statement_feb.pdf",
]

for filepath in files_to_upload:
    # 2a: Get presigned URL
    response = requests.post(
        f"{BASE_URL}/v1/files",
        headers=headers,
        json={"file_name": filepath}
    )
    upload_data = response.json()
    upload_url = upload_data["upload_url"]
    uri = upload_data["uri"]

    # 2b: Upload file to the resumable session. Content-Range must declare the
    # total size so cloud storage finalizes the object. Stream the file handle
    # rather than reading the whole file into memory (requests sets
    # Content-Length from the file's size).
    total = os.path.getsize(filepath)
    with open(filepath, "rb") as f:
        upload_response = requests.put(
            upload_url,
            data=f,
            headers={
                "Content-Type": "application/pdf",
                "Content-Range": f"bytes 0-{total - 1}/{total}",
            },
        )
    upload_response.raise_for_status()  # fail fast if the upload didn't finalize

    # 2c: Attach the file to the package (starts processing)
    requests.patch(
        f"{BASE_URL}/v2/packages/{package_id}",
        headers=headers,
        json={"file_names": [filepath], "file_uris": [uri]}
    )
    print(f"Uploaded: {filepath}")

# Step 3: Monitor progress
while True:
    response = requests.get(
        f"{BASE_URL}/v2/packages/{package_id}",
        headers=headers
    )
    package = response.json()
    status = package["status"]

    print(f"Status: {status}")

    if status == "Processed":
        break
    elif status == "Under Review":
        print("Package needs manual review (a file may have failed).")
        exit(1)

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

# Step 4: Get results
response = requests.get(
    f"{BASE_URL}/v1/packages/{package_id}/models",
    headers=headers
)
results = response.json()

print(f"Found {results['total']} income models:")
for model in results['results']:
    print(f"\nModel: {model['model_name']}")
    print(f"Status: {model['status']}")  # "Success" or "Failed"
    print(f"Total Credits: ${model['total_credits']}")
    print(f"Eligible Credits: ${model['eligible_credit']}")

Complete Example (JavaScript/Node.js)

const axios = require('axios');
const fs = require('fs');

const API_KEY = 'your_api_key_here';
const BASE_URL = 'https://api.laminr.ai/api';

const headers = {
  'x-api-key': API_KEY
};

async function processDocuments() {
  // Step 1: Create a package
  const packageRes = await axios.post(
    `${BASE_URL}/v2/packages`,
    {
      title: 'John Doe Application'
    },
    { headers }
  );

  const packageId = packageRes.data.id;
  console.log(`Created package: ${packageId}`);

  // Step 2: Upload files
  const files = ['bank_statement_jan.pdf', 'bank_statement_feb.pdf'];

  for (const filepath of files) {
    // 2a: Get presigned URL
    const uploadRes = await axios.post(
      `${BASE_URL}/v1/files`,
      { file_name: filepath },
      { headers }
    );
    const { upload_url, uri } = uploadRes.data;

    // 2b: Upload file to the resumable session. Content-Range must declare the
    // total size so cloud storage finalizes the object. Stream the file rather
    // than buffering it; set Content-Length so the streamed body isn't chunked.
    const total = fs.statSync(filepath).size;
    await axios.put(upload_url, fs.createReadStream(filepath), {
      headers: {
        'Content-Type': 'application/pdf',
        'Content-Length': total,
        'Content-Range': `bytes 0-${total - 1}/${total}`,
      },
      maxBodyLength: Infinity,
    });

    // 2c: Attach the file to the package (starts processing)
    await axios.patch(
      `${BASE_URL}/v2/packages/${packageId}`,
      { file_names: [filepath], file_uris: [uri] },
      { headers }
    );
    console.log(`Uploaded: ${filepath}`);
  }

  // Step 3: Monitor progress
  while (true) {
    const statusRes = await axios.get(
      `${BASE_URL}/v2/packages/${packageId}`,
      { headers }
    );

    const status = statusRes.data.status;
    console.log(`Status: ${status}`);

    if (status === 'Processed') {
      break;
    } else if (status === 'Under Review') {
      console.log('Package needs manual review (a file may have failed).');
      process.exit(1);
    }

    await new Promise(resolve => setTimeout(resolve, 10000));
  }

  // Step 4: Get results
  const resultsRes = await axios.get(
    `${BASE_URL}/v1/packages/${packageId}/models`,
    { headers }
  );

  const results = resultsRes.data;
  console.log(`Found ${results.total} income models:`);
  results.results.forEach(model => {
    console.log(`\nModel: ${model.model_name}`);
    console.log(`Status: ${model.status}`);  // "Success" or "Failed"
    console.log(`Total Credits: $${model.total_credits}`);
    console.log(`Eligible Credits: $${model.eligible_credit}`);
  });
}

processDocuments();

Next Steps

Support

Need help? Contact us at support@laminr.ai