parsr.

For lenders, BNPL, and KYC platforms

Underwrite faster on European bank statements.

The US-based document APIs you've evaluated were trained on Chase and Wells Fargo. They will not handle a Belfius statement, a Sparkasse IBAN block, or a Crédit Agricole multi-account PDF. parsr was built in the EU, on EU bank formats, with the validation hooks regulated credit teams ask for on day one.

Why credit teams pick parsr

Built for the EU credit stack

01 / Coverage

50+ EU bank statement formats, validated

Belfius, KBC, BNP Paribas Fortis, ING, Sparkasse, Deutsche Bank, Crédit Agricole, Société Générale, Santander, Intesa, Revolut, N26, Wise, and the long tail. Schema-versioned at bank_statement.v2 — when we add a format, your code does not change.

02 / Anti-fraud

Balance-chain validation in the response

Every parse returns validation.balance_chain — a structured verdict that re-runs the running balance from line items, compares declared vs. computed closing, and flags inserted or deleted rows. One field tells you whether the document adds up.

03 / Sovereignty

EU residency, DORA / NIS2 aligned

EU-region keys (sk_eu_…) bind requests to the Frankfurt and Paris regions — no cross-border fall-through, no US sub-processor in the parse path. Vendor-risk-friendly DPA and a short, auditable sub-processor list.

The underwriting loop

Five steps from upload to decision

The same flow our design partners run in production, condensed. You'll wire it together in an afternoon — see the recipe below.

  1. 01

    01 / borrower.upload(pdf)

    Borrower uploads a 3–12 month bank statement to your application portal. You forward it to parsr's /v1/parse/bank-statement endpoint with a region-bound API key and an idempotency key tied to the application ID.

  2. 02

    02 / parsr.parse → structured JSON

    parsr returns a schema-validated bank_statement.v2 payload — IBAN, account holder, transaction list with ISO-8601 dates, normalized amounts, currency, and validation.balance_chain. Every field carries a confidence score.

  3. 03

    03 / score.income_signals(payload)

    Group recurring credits by description fingerprint to detect salary, benefits, and predictable inflows. Compute net inflow, expense volatility, and salary-to-rent ratio. Feed these features into your scorecard or model.

  4. 04

    04 / review.escalate_if(confidence < threshold)

    Any field with confidence below your operating threshold (we recommend 0.85 for credit decisioning) is routed to a human reviewer with the bounding box pre-rendered. Everything above flows straight through.

  5. 05

    05 / underwrite.decide()

    Your decisioning service consumes the JSON, applies your rules, and writes a decision back to the application. The full parse — input hash, model version, schema version, and balance-chain verdict — is preserved for audit.

The integration

One parse, one decision signal

Parse the statement, check the balance chain, fingerprint recurring credits, and surface the income signals your scorecard expects. Replace the scorecard with yours — the inputs are stable.

underwrite/parse_statement.pypython
import os
import httpx
from collections import defaultdict

API = "https://eu-api.tryparsr.dev"
KEY = os.environ["PARSR_KEY"]  # sk_eu_live_…

async def underwrite(pdf_path: str, application_id: str) -> dict:
    async with httpx.AsyncClient(timeout=60) as client:
        with open(pdf_path, "rb") as f:
            resp = await client.post(
                f"{API}/v1/parse/bank-statement?wait=60",
                headers={
                    "Authorization": f"Bearer {KEY}",
                    "Idempotency-Key": application_id,
                },
                files={"file": f},
                data={"metadata": f'{{"application_id":"{application_id}"}}'},
            )
        resp.raise_for_status()
        doc = resp.json()

    # 1. Hard fraud signal: did the running balance reconcile?
    chain = doc["validation"]["balance_chain"]
    if not chain["valid"] and abs(chain["diff"]) > 0.50:
        return {"decision": "manual_review", "reason": "balance_chain_failed"}

    # 2. Group recurring credits by description fingerprint
    recurring = defaultdict(list)
    for tx in doc["transactions"]:
        if tx["amount"] > 0 and tx["confidence"] > 0.85:
            recurring[tx["description_fingerprint"]].append(tx)

    salary = max(
        (txs for txs in recurring.values() if len(txs) >= 3),
        key=lambda txs: sum(t["amount"] for t in txs),
        default=[],
    )

    # 3. Feed into your scorecard
    return {
        "decision": "approved" if salary else "manual_review",
        "monthly_income": sum(t["amount"] for t in salary) / max(len(salary), 1),
        "balance_chain_valid": chain["valid"],
        "schema_version": doc["schema_version"],
    }

What credit-risk reviewers ask about

The controls your second-line wants on a vendor sheet

  • EU residency by default — Frankfurt + Paris regions, no US fall-through
  • DORA, NIS2, and EU AI Act-aligned operating model
  • Schrems II-safe: no US sub-processor sits on the parse path
  • GDPR-compliant with a vendor DPA available before signup
  • SOC 2 Type II audit window opens Q3 2026 (ISO 27001 to follow)
  • BaFin-friendly: regional keys, signed audit trails, retention controls
  • KYC integration: parse output flows into Onfido, Veriff, Sumsub via webhook
  • Region-bound API keys (sk_eu_… vs sk_us_…) — no accidental cross-border traffic
  • HMAC-signed webhooks with replay-window protection
  • 30-day file-hash cache — re-uploads of the same statement don't double-bill

Pricing fit

Where lenders land on the ladder

Recommended tier

Growth — €99 / month for 5,000 pages

A lender doing ~100 applications per day, with a typical 3–6 page statement, sits comfortably in the Growth tier. Cross 500 applications per day and Scale (€399 / 25,000 pages) is the right number; above that, talk to us about an Enterprise floor (~€2,000) with annual commit and a custom SLA. The Free tier (200 pages) is for evaluation, not production.

Questions credit teams ask

FAQ

How does parsr fit alongside our KYC vendor (Onfido / Veriff / Sumsub)?

KYC vendors confirm the borrower is who they claim to be. parsr confirms the bank statement they uploaded is the document they claim it is, then turns it into structured JSON for your scorecard. Most lenders run KYC and parsr in parallel — KYC verdict on the borrower, parsr verdict on the document, both feed the underwriting decision. We expose webhook outputs that you can fan out to either.

How do you handle false positives on balance-chain validation?

validation.balance_chain.valid is a soft signal, not a reject. The same response includes computed_closing, declared_closing, and diff — you set the tolerance. Most production teams treat any diff above ±€0.50 as a manual-review trigger rather than a hard reject, because rounding artefacts and merchant-fee timing produce small legitimate gaps. We document this end-to-end in the validation guide.

Can a borrower upload statements from multiple accounts at once?

Yes. Submit each statement as its own parse call with the same application_id in metadata. parsr returns one bank_statement.v2 per file. On your side, group by metadata.application_id and reconcile the income signals across accounts. The 30-day file-hash cache means a borrower re-submitting the same PDF won't burn additional pages.

What is your data retention policy for parsed statements?

By default we retain the raw upload for 30 days (for the file-hash cache and for re-parse on schema upgrade) and the structured JSON for 90 days. Both windows are configurable per organization down to zero — pass retention_days: 0 on the request and we discard the raw PDF the moment the parse returns. Enterprise customers can pin retention to a specific Exoscale or Hetzner region.

Do you provide an audit trail we can show our regulator?

Every parse writes an audit record with the input hash, the schema version, the model version, the balance-chain verdict, the requesting API key fingerprint, and the regional endpoint that handled it. Audit records are exportable as NDJSON and signed. We don't claim a SOC 2 report yet — the audit window opens Q3 2026 — but the audit log itself is production today.

Which EU bank formats are supported, and what happens for unknown banks?

We currently cover 50+ EU bank statement formats including all top-five retail banks in DE, FR, NL, BE, ES, IT, and the major neobanks (Revolut, N26, Wise, Bunq). For unknown formats we fall back to a generic statement parser that still returns a schema-valid bank_statement.v2 — confidence scores will be lower and balance_chain may be marked needs_review, but you get structured output rather than a 422. Coverage is documented per-issuer; pages live in /coverage.

200 free pages. No credit card. No sales call.

Drop parsr into your underwriting pipeline this week. If the balance-chain check or the EU format coverage doesn't earn its keep, walk away — no contract, no lock-in.

Start underwriting