<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Smartdocs</title>
    <description>The latest articles on DEV Community by Smartdocs (@smartdocs198).</description>
    <link>https://dev.to/smartdocs198</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4193389%2Fb3c74d55-e8a9-4ff6-8f40-56f45f77f5d3.png</url>
      <title>DEV Community: Smartdocs</title>
      <link>https://dev.to/smartdocs198</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smartdocs198"/>
    <language>en</language>
    <item>
      <title>How to Choose a Document Parsing API: What to Test Before You Commit</title>
      <dc:creator>Smartdocs</dc:creator>
      <pubDate>Thu, 12 Nov 2026 08:01:16 +0000</pubDate>
      <link>https://dev.to/smartdocs198/how-to-choose-a-document-parsing-api-what-to-test-before-you-commit-4l95</link>
      <guid>https://dev.to/smartdocs198/how-to-choose-a-document-parsing-api-what-to-test-before-you-commit-4l95</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A document parsing API turns PDFs, scans and images into structured JSON that software can use directly. Demos run on clean samples, so a good demo says little about your real inbox. This tutorial builds a small Python harness that runs your hardest documents through a candidate API and reports field accuracy, silent errors, latency and failures. It then shows the same harness against Sensible's API and lists what else to check before you commit: cost per document, who maintains the extraction logic, and how results get verified.&lt;/p&gt;

&lt;p&gt;Business software depends on structured data, yet much of what companies receive arrives inside documents. Invoices, statements and forms show up as PDFs, scans and photos, each laid out differently by whoever produced it. For years the options were manual entry or brittle scripts.&lt;/p&gt;

&lt;p&gt;A parsing API packs that reading work into one request, and many products now offer it. They all look good on a clean sample invoice, which makes them hard to compare. By the end of this tutorial you will know how to build a test set from your own documents, which numbers to measure, and how to read the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What One Supplier Invoice Looks Like After Parsing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Suppose we have an operations team at a mid-size distributor that receives supplier invoices from hundreds of vendors. Each vendor uses its own layout, some send digital PDFs, and others send scans. A person currently types the invoice number, date, line items and total into the accounting system.&lt;/p&gt;

&lt;p&gt;A parsing API takes over that typing. The team's software sends a file and gets back JSON, a text format that programs read directly, with a named field for each value. The field names below are illustrative, because they depend on the configuration a team writes or clones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"COMPLETE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"parsed_document"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"INV-1042"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"1,284.50"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OCR, short for optical character recognition, only converts the pixels of a scanned page into text. Parsing goes further by deciding which text is the invoice number and which is the total, so OCR is one step inside most parsing APIs rather than a substitute for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Four Approaches Sit Behind Most Parsing APIs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Knowing which approach a vendor leans on predicts where it will perform well and where it will need attention. Many products combine more than one.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;OCR Services That Return Text From Each Page&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;OCR and text detection services return text, tables and form fields from a page. They suit teams that want raw material and will write the logic that decides which value is which.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Rule-Based Extraction Reads Values From Fixed Positions&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Rule-based extraction defines where a value sits, using positions, labels or patterns. Results are predictable for stable layouts, though each new layout needs its own rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;LLM-Based Extraction Answers Questions About the Page&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;LLM-based extraction has a language model answer questions such as "what is the invoice total?" It handles layouts it has never seen, but it can return a plausible wrong answer and can be slower and costlier per page.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Hybrid Extraction Chooses a Method for Each Field&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Hybrid extraction applies rules to fields with predictable structure and an LLM to fields that vary, then validates the results. Each field uses the method that suits it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How to Pick Between the Four Approaches&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No single approach wins everywhere. A fixed government form favors rules, a free-text contract favors an LLM, and a mixed inbox favors a hybrid with a way to detect when either one is unsure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcm578i3k889vaxve2l1v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcm578i3k889vaxve2l1v.png" alt="Sensible" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How to Build a Test Set From Your Hardest Documents&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A vendor demo on a clean sample invoice tells you little about production. A better test uses 20 to 30 of your own documents, chosen to include the cases that cause trouble today. Hand-label the correct values once, then reuse the set for every vendor you try.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Sort Documents Into Folders by Difficulty&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Sort the files into folders by the kind of difficulty they carry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;clean/&lt;/code&gt; holds a few easy digital PDFs as a control.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scans/&lt;/code&gt; holds photographed or scanned pages with skew, noise or stamps.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;variants/&lt;/code&gt; holds the same document type from many vendors, so one field appears in several layouts.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;long/&lt;/code&gt; holds multi-page documents with repeating rows, such as a 40-line invoice.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;high-stakes/&lt;/code&gt; holds documents where a wrong value costs money, such as payment amounts.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/
  clean/invoice-001.pdf
  scans/invoice-014.pdf
  variants/invoice-021.pdf
  long/invoice-033.pdf
expected/
  invoice-001.json
  invoice-014.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Label the Correct Values Once by Hand&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Each file in &lt;code&gt;expected/&lt;/code&gt; shares its name with a document and lists the correct values. Set aside a handful of documents and don't use them while you tune anything, so you have an unbiased check at the end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"INV-1042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"subtotal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1180.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"1,284.50"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;590.00&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;590.00&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;A Vendor-Neutral Python Harness for Comparing Parsing APIs&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How the Vendor Adapter Feeds the Test Runner&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The harness below treats every vendor the same way. An adapter takes a file path and returns two things: a dictionary of extracted fields and a flag for whether the vendor marked the result as doubtful. The runner compares each field with your expected value and prints results per folder.&lt;/p&gt;

&lt;p&gt;The runner also recomputes one rule of its own, that line items add up to the subtotal. That keeps the result from depending on whether a vendor chooses to flag a problem. Install &lt;code&gt;requests&lt;/code&gt;, save the file as &lt;code&gt;bench.py&lt;/code&gt;, and adjust the field names to match your documents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;adapter_sensible&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;extract&lt;/span&gt;  &lt;span class="c1"&gt;# swap in any vendor's adapter with the same shape&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reconciles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"""Your own check, so the result never depends on the vendor flagging a problem."""&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"subtotal"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;docs_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"*.pdf"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{pdf.stem}.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vendor_flagged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;secs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;
        &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vendor_flagged&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;reconciles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s2"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"doc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"flagged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"secs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;secs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;by_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;by_group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;silent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"flagged"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{group:&amp;lt;12} fields={len(rs):&amp;lt;4} accuracy={acc:.1%}  silent_errors={silent}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;secs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"doc"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"secs"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;times&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;secs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;p95&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"latency p50={statistics.median(times):.1f}s  p95={p95:.1f}s  failed_docs={len(failures)}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"FAILED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Why Silent Errors Are the Number to Watch&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The number to watch is &lt;code&gt;silent_errors&lt;/code&gt;. A silent error is a wrong value that arrives without any flag, so nothing stops it from reaching your database. A wrong value that gets flagged costs a review; a wrong value that does not get flagged costs whatever the value controls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnw5erq5fcy8vqszzssts.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnw5erq5fcy8vqszzssts.png" alt="Sensible" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What the Harness Output Looks Like&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A dry run with three fake documents prints lines in this shape, and your own run will show the same columns with real numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clean        fields=4    accuracy=100.0%  silent_errors=0
scans        fields=8    accuracy=75.0%  silent_errors=1
latency p50=0.0s  p95=0.0s  failed_docs=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Adapting the Harness to Sensible's REST API&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Calling the Synchronous Endpoint for Testing&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Sensible exposes a REST endpoint per document type. The synchronous call is intended for testing, so it suits a harness: post the file bytes with a matching &lt;code&gt;Content-Type&lt;/code&gt; and read &lt;code&gt;parsed_document&lt;/code&gt; from the response. Authentication uses an &lt;code&gt;Authorization: Bearer&lt;/code&gt; header with a key from the account page.&lt;/p&gt;

&lt;p&gt;The adapter below also strips Sensible's &lt;code&gt;{"type": ..., "value": ...}&lt;/code&gt; wrappers so results compare cleanly with your expected JSON. It reads &lt;code&gt;validation_summary&lt;/code&gt; to see whether any validation rule raised an error or a warning. Set &lt;code&gt;SENSIBLE_API_KEY&lt;/code&gt; in your environment and pass the name of your document type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="no"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://api.sensible.so/v0"&lt;/span&gt;
&lt;span class="no"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Bearer {os.environ.get('SENSIBLE_API_KEY', '')}"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"""Strip Sensible's {"type": ..., "value": ...} wrappers down to plain values."""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="s2"&gt;"value"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;from_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"validation_summary"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;vendor_flagged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"parsed_document"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vendor_flagged&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"invoices"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"""Synchronous endpoint: intended for testing, not for production volume."""&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{BASE}/extract/{doc_type}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="no"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"application/pdf"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;from_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"invoices"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poll_secs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout_secs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"""Production path: submit a URL, then poll until the extraction finishes."""&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{BASE}/extract_from_url/{doc_type}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="no"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"document_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;document_url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;extraction_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timeout_secs&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{BASE}/documents/{extraction_id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"COMPLETE"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;from_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"FAILED"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"extraction {extraction_id} failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;poll_secs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"extraction {extraction_id} still running after {timeout_secs}s"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Calling the Asynchronous Endpoint for Production&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;extract_async&lt;/code&gt; function shows the production path. It submits a URL the API can fetch, then polls &lt;code&gt;GET /documents/{id}&lt;/code&gt; until the status is &lt;code&gt;COMPLETE&lt;/code&gt; or &lt;code&gt;FAILED&lt;/code&gt;. Sensible's &lt;a href="https://docs.sensible.so/docs/api-tutorial-async-1" rel="noopener noreferrer"&gt;asynchronous tutorial&lt;/a&gt; walks through the same calls, and a webhook can replace the polling loop.&lt;/p&gt;

&lt;p&gt;Run the harness with &lt;code&gt;python bench.py docs expected&lt;/code&gt;. To compare another vendor, write an adapter with the same two return values and change one import line.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Six Checks to Read Out of the Harness Results&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The numbers only help when you know what each one is testing. These six checks turn a run into a decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Check 1: Accuracy Per Field Across Vendor Layouts&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Read the &lt;code&gt;variants/&lt;/code&gt; line first, because layout variation is where most parsing projects stall. If accuracy there is far below &lt;code&gt;clean/&lt;/code&gt;, the product depends on layouts it has already seen. Also look at which fields fail, since one weak field, such as a tax amount, can do more damage than a low average suggests.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Check 2: Silent Errors Versus Flagged Errors&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Count the silent errors per folder and compare vendors on that number as well as accuracy. Ask whether you can write your own rules and what happens to results that fail them. In Sensible, you write validation rules against the extracted values, for instance confirming that line items sum to a stated subtotal, and any field that fails gets flagged for review instead of passing through unverified.&lt;/p&gt;

&lt;p&gt;Each rule is a JsonLogic condition with a severity of &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt; or &lt;code&gt;skipped&lt;/code&gt;, and results appear in the &lt;code&gt;validations&lt;/code&gt; array and the &lt;code&gt;validation_summary&lt;/code&gt; counts. &lt;a href="https://docs.sensible.so/docs/validate-extractions" rel="noopener noreferrer"&gt;Sensible's validation page&lt;/a&gt; shows a rule that checks a field is present, written as &lt;code&gt;{"exists":[{"var":"invoice_number.value"}]}&lt;/code&gt; when adapted to our invoice field.&lt;/p&gt;

&lt;p&gt;Confidence signals are a separate tool. They are available only for the Query Group method, with values such as &lt;code&gt;multiple_possible_answers&lt;/code&gt; and &lt;code&gt;answer_not_found&lt;/code&gt;, and an LLM can report them inaccurately. Rules are the safer check for any field where a wrong value is costly.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Check 3: Repeating Rows on Long Multi-Page Documents&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Long tables expose limits that short samples hide. Look at the &lt;code&gt;long/&lt;/code&gt; line and compare the number of extracted rows with the expected count, since a missing row can still leave other fields looking correct. Ask each vendor how it handles output that exceeds a model's response limit.&lt;/p&gt;

&lt;p&gt;Sensible's List method, for example, splits a document into chunks, scores each chunk against your field descriptions, combines the top-scoring ones, and truncates the list if the output exceeds an LLM engine's limit. The Sections method suits repeating groups such as accounts on a statement, and it returns them as an array of objects.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Check 4: Latency From Submit to Result&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Time each call and look at the median and the 95th percentile, because the slowest documents decide whether a workflow feels reliable. Test the synchronous endpoint for correctness, then time the asynchronous path from submit to &lt;code&gt;COMPLETE&lt;/code&gt;, since production volumes use asynchronous calls. Latency also depends on how you call the API, so measure it from the environment where it will run.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Check 5: Who Fixes It When a Vendor Changes Format&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Layouts change without notice, and somebody has to update the extraction logic when they do. Some teams have engineers write and maintain it, while others prefer prebuilt configurations or a vendor services team. Sensible keeps its configurations in Sensible, versioned through the API, and offers a library of more than 150 prebuilt ones that you clone to your account and test in the Extract tab.&lt;/p&gt;

&lt;p&gt;Teams that do not want to build configurations themselves can have Sensible's &lt;a href="https://www.sensible.so/managed-services" rel="noopener noreferrer"&gt;managed services team&lt;/a&gt; build them from their own sample documents and adjust them when a vendor changes its format. Whichever route you choose, put a changed layout in your test set and see how long the fix takes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Check 6: Cost Per Document at Your Volume&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A per-page headline price rarely matches your bill, so divide the plan fee plus any overage by your monthly volume. Sensible's &lt;a href="https://www.sensible.so/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; lists Growth at \$449 per month billed annually, or \$499 billed monthly, with 750 documents included and \$0.57 per extra document. For example, a team that processes 1,000 documents a month on annual billing pays \$449 plus 250 overage documents at \$0.57, which is about \$0.59 per document.&lt;/p&gt;

&lt;p&gt;Scale lists \$1,349 per month billed annually, with 3,200 documents included and \$0.53 per extra document. Enterprise pricing is custom from 10,000 documents a month. SOC 2 Type II and GDPR appear on every plan, and HIPAA with a signed BAA is listed on Enterprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How Sensible Maps to the Six Checks&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Sensible is a hybrid platform that combines AI extraction with deterministic, rule-based methods behind one REST API. Its unit of work is a document type, a named set of similar documents such as invoices, and each type holds one or more configurations written in SenseML, a JSON-based configuration language. Each field can use either kind of method: layout-based rules where a layout is predictable, and an LLM method where it varies. Both live in one configuration and return the same schema.&lt;/p&gt;

&lt;p&gt;The notes below show how that hybrid design answers the earlier checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Layout Variation Follows an 80/20 Pattern&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Sensible follows an 80/20 pattern, with deterministic methods handling most fields first and a generalized LLM-based template covering the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Fallbacks Cover Fields That Return Nothing&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Consecutive fields with the same ID let an LLM field back up a layout field, or the reverse, when the first returns an empty array or null.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Classification Routes Each Document Before Extraction&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Classification matches a document to a document type before extraction, so a team can decide what to extract or where to route it in a mixed inbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Validation Rules Plus a Review Queue&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Validation rules you configure check the output, and flagged documents can be corrected in a review queue.&lt;/p&gt;

&lt;p&gt;The quickest way to judge all of this is to run your &lt;code&gt;variants/&lt;/code&gt; and &lt;code&gt;long/&lt;/code&gt; folders through a trial and read the harness output. The same harness works for every other API you shortlist.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where Should You Start&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A parsing API is easiest to judge on the documents that cause the most trouble: unusual layouts, repeating groups and values that cannot be wrong. Accuracy per folder, silent errors, tail latency, maintenance effort and cost per document together give a fuller picture than any demo. Keep the test set, because you will want it again when a vendor ships an update or your document mix changes.&lt;/p&gt;

&lt;p&gt;Sensible offers a 14-day free trial with no credit card to start, so you can run the harness above against your own files. Start with the &lt;a href="https://www.sensible.so/extract/invoices" rel="noopener noreferrer"&gt;invoice extraction page&lt;/a&gt; to see a document type in the app, then send your hardest ten documents through the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Frequently Asked Questions&lt;/strong&gt;   &lt;strong&gt;What Does a Document Parsing API Return?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;It accepts a file such as a PDF, image or spreadsheet and returns the data inside it as named fields, usually JSON. The values come back with a type, so software can act on them without retyping.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Is OCR Enough for Extracting Invoice Fields?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;OCR converts the pixels of a scan into text and nothing more. A parsing API also decides which text is the invoice number, the date or the total. Most parsing APIs run OCR as one internal step when a file has no embedded text.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How Many Documents Should a Parsing Test Set Contain?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;There is no fixed number. Twenty to thirty documents spread across clean, scanned, variant, long and high-stakes folders gives a useful first read. Add more documents wherever two vendors score close together.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Should I Test With Synchronous or Asynchronous Calls?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Use synchronous calls to check correctness, because they return the result in the same response. Use asynchronous calls to measure production behavior, where you submit a document and collect the result later by polling or through a webhook.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Does Sensible Use LLMs or Rules to Extract Data?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Both. Each field in a Sensible configuration can use a layout-based method for predictable structure or an LLM-based method for content that varies, and both return the same schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Does Sensible Validate Extracted Data Automatically?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Validation is configured by the team. You write rules in JsonLogic against the extracted values, and results that fail appear in the &lt;code&gt;validations&lt;/code&gt; array so they can be flagged for review.&lt;/p&gt;

</description>
      <category>api</category>
      <category>documentation</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Instabase Alternatives: What Mid-Market Teams Choose</title>
      <dc:creator>Smartdocs</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:40:16 +0000</pubDate>
      <link>https://dev.to/smartdocs198/instabase-alternatives-what-mid-market-teams-choose-gbo</link>
      <guid>https://dev.to/smartdocs198/instabase-alternatives-what-mid-market-teams-choose-gbo</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Instabase is built for large enterprises processing complex, multi-document packets, and it sells that way, through a demo and a custom quote, with no public pricing. A mid-market team that just needs invoices, statements, or forms extracted into usable fields often finds that scale and sales process is more than they need. This piece looks at five platforms mid-market teams evaluate instead, including Sensible, and what each one is built for.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Getting Started: What Instabase Does and Who It's Built For&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Instabase describes itself as an agentic automation platform that turns complex documents into what it calls verifiable intelligence, with capabilities like cross-document validation across a full "packet" of related files and dynamic selection of different AI models per task, according to &lt;a href="https://www.instabase.com/" rel="noopener noreferrer"&gt;Instabase's own site&lt;/a&gt;. Its customer list leans heavily toward large financial services, insurance, and public sector organizations, including names like AXA, NatWest, and Rocket Mortgage.&lt;/p&gt;

&lt;p&gt;That scope shows up in how Instabase sells. There is no pricing published anywhere on its site. The only call to action is to request a demo, which puts every prospect through a sales conversation before they see a number. For an enterprise buying a platform to handle loan packets or claims files across thousands of documents a month, that process makes sense. For a mid-market team that wants to extract fields from invoices or bank statements and start seeing results in a week, it's a mismatch before the first call even happens.&lt;/p&gt;

&lt;p&gt;The running example for this piece is a 60-person lending operations team at a regional fintech. They got a quote from Instabase after a demo call, and the number and implementation timeline both assumed a document volume and packet complexity the team didn't have. They went looking for something built closer to their actual size, and the five platforms below were what came up.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;1. Sensible: Best for Mid-Market Teams That Want Self-Serve Configuration&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://sensible.so/" rel="noopener noreferrer"&gt;Sensible&lt;/a&gt; is a document extraction platform built around SenseML, a configuration language where each field in a document type can use either a layout-based rule, for a format that's predictable, or an LLM-based method, for one that varies, with both returning the same schema either way. A team can see and version that configuration through Sensible's API, rather than having it live as an opaque model behind a sales-gated platform.&lt;/p&gt;

&lt;p&gt;The difference that matters most for a mid-market team is how they get started. &lt;a href="https://sensible.so/pricing" rel="noopener noreferrer"&gt;Sensible's pricing page&lt;/a&gt; lists two self-serve tiers: Growth at \$499 a month, or \$449 a month billed annually, for 750 documents a month, and Scale at \$1,499 a month, or \$1,349 a month billed annually, for 3,200 documents a month, each with a per-document overage rate beyond that. Every paid plan starts with a 14-day free trial that includes full access to extraction, validation, and the configuration editor, and no credit card is required to start it. A team can sign up, run their own documents through it, and see real output before talking to anyone, which is exactly the step Instabase's demo-only model skips.&lt;/p&gt;

&lt;p&gt;For bank statements specifically, Sensible runs layout-based configurations for formats it has already mapped, plus a general LLM configuration that handles everything else, with both returning the same output schema. That's a narrower claim than "any format," but it means a new, unmapped statement layout still gets handled instead of falling through. Validation rules, like confirming that line items sum to a stated subtotal, get written once against the field names in the schema and flag any document that fails them, instead of requiring custom code the way an unstructured extraction pipeline does. Teams that don't want to own the configuration work at all can hand it to Sensible's managed services team, who build the extraction configuration from a team's own sample documents and adjust it when a format changes, on the same underlying engine available to self-serve customers. Output can also come back as an Excel or CSV file directly, either by calling the API with multiple extraction IDs or by selecting documents that already have extraction results and downloading them as Excel directly in the Sensible app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqdg4wkzr7zqpnkp5vefu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqdg4wkzr7zqpnkp5vefu.png" alt="Sensible pricing" width="512" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Docsumo: Best for Teams Wanting a Guided Implementation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docsumo.com/" rel="noopener noreferrer"&gt;Docsumo&lt;/a&gt; offers a 14-day free trial that includes up to 1,000 free pages and ten user licenses, but its paid Business and Enterprise tiers are both listed as custom quotes rather than published prices, according to &lt;a href="https://docsumo.com/pricing" rel="noopener noreferrer"&gt;Docsumo's pricing page&lt;/a&gt;. The page also notes that setup fees are charged on top of the subscription, scaled to document complexity and the support level a team needs.&lt;/p&gt;

&lt;p&gt;That structure fits a team that wants a vendor-led implementation rather than one it configures itself. Docsumo is a reasonable fit for finance and insurance teams that have a defined document set and want a guided setup, and a less direct fit for a team trying to compare real monthly costs before a sales call, since no self-serve paid plan exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. Nanonets: Best for Self-Serve, Pay-as-You-Go Automation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://nanonets.com/" rel="noopener noreferrer"&gt;Nanonets&lt;/a&gt; is the other genuinely self-serve option on this list. Its Starter tier is free to begin, moving to \$100 a month for 100 credits afterward, with signup requiring no card and \$50 in starting credits, per &lt;a href="https://nanonets.com/pricing" rel="noopener noreferrer"&gt;Nanonets' pricing page&lt;/a&gt;. Usage is metered per workflow step, called a block, at roughly \$0.02 for a simple block up to \$0.30 for a complex AI block, which the page estimates lands under \$2 per document for a typical invoice workflow.&lt;/p&gt;

&lt;p&gt;The Growth and Enterprise tiers above Starter move to volume-based quotes and add classification, custom integrations, and compliance features like SSO and HIPAA support. For a mid-market team that wants to start small, see real per-document costs immediately, and scale usage without a new contract negotiation at every step, Nanonets' metered model is a close match to Sensible's self-serve approach, built around granular, pay-per-block pricing rather than Sensible's flat per-document plan tiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4. Rossum: Best for Finance Teams Needing Workflow Depth&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://rossum.ai/" rel="noopener noreferrer"&gt;Rossum&lt;/a&gt; lists a starting price of \$18,000 a year for its entry Starter tier, according to &lt;a href="https://rossum.ai/pricing" rel="noopener noreferrer"&gt;Rossum's pricing page&lt;/a&gt;, which already puts its floor well above Sensible's or Nanonets' self-serve entry points. Its Business, Enterprise, and Ultimate tiers above that show no published price and require a custom quote based on page volume, workflow complexity, and contract term.&lt;/p&gt;

&lt;p&gt;Rossum's strength is workflow depth for finance-heavy document types like invoices and purchase orders, including approval routing and exception handling built into the platform rather than bolted on. That depth, and the price that comes with it, tends to fit teams with an established AP or finance operations workflow looking to replace an existing tool, more than a team trying to stand up extraction for the first time on a mid-market budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. Hyperscience: Best for Large-Scale, Regulated Document Volume&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.hyperscience.com/" rel="noopener noreferrer"&gt;Hyperscience&lt;/a&gt; is priced closer to Instabase than to any of the other four. Its entry tier starts at \$50,000 a year, with enterprise deployments running roughly €50,000 to €300,000 a year, according to &lt;a href="https://erpresearch.com/erp-add-ons/ocr/hyperscience/pricing" rel="noopener noreferrer"&gt;erpresearch's pricing breakdown&lt;/a&gt;, and there is no free trial. The page places Hyperscience in the same enterprise intelligent-document-processing band as ABBYY and Tungsten, and notes that its machine-learning approach pays off mainly for organizations with enough document variety and volume to justify it.&lt;/p&gt;

&lt;p&gt;That positions Hyperscience, like Instabase, as a platform built for large, often regulated document volumes where the per-document cost at scale matters more than the entry price or the time to a first extraction. A mid-market team evaluating it alongside Instabase is likely to run into the same mismatch: real capability, built for a scale and budget the team hasn't reached yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where Should You Start&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsj39ab80drgvidb14bpu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsj39ab80drgvidb14bpu.png" alt="Sensible" width="512" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real split across these five platforms isn't about which one can technically extract a field from a document, most of them can, on a clean sample in a demo. It's about whether a team can see real pricing and get a real extraction running before committing to a sales process sized for an enterprise buyer. Sensible and Nanonets publish self-serve pricing and let a team start immediately; Docsumo, Rossum, and Hyperscience all require a quote before a team sees a number, and Hyperscience's entry price alone puts it closer to Instabase than to a mid-market budget.&lt;/p&gt;

&lt;p&gt;A team currently sitting on an Instabase quote that feels oversized for its actual document volume is a good candidate to try a self-serve platform directly against its own documents before signing anything. &lt;a href="https://sensible.so/pricing" rel="noopener noreferrer"&gt;Sensible's 14-day free trial&lt;/a&gt; requires no credit card and gives full access to extraction, validation, and the configuration editor, which is the fastest way to find out whether a lighter platform actually covers what a team needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;FAQs&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What does Instabase actually do?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Instabase is an agentic document automation platform built for large enterprises, processing complex multi-document packets with cross-document validation and business rules applied by AI agents. It's built and sold for organizations in financial services, insurance, healthcare, and the public sector, with named customers like AXA, NatWest, and Rocket Mortgage.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Why doesn't Instabase publish pricing?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Instabase's pricing depends on document volume, packet complexity, and the specific workflows a customer needs automated, which is typical of enterprise software sold through a sales team rather than a self-serve signup. The only listed path to pricing is requesting a demo.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Which Instabase alternatives let a team see pricing without a sales call?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Sensible and Nanonets both publish self-serve pricing tiers that a team can sign up for directly. Sensible's Growth and Scale plans start at \$499 and \$1,499 a month respectively, each with a 14-day free trial and no card required, and Nanonets' Starter tier is free to begin with pay-as-you-go credits afterward.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Is Hyperscience a mid-market option?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Not based on its published pricing. Its entry tier starts at \$50,000 a year, with enterprise deployments running into the hundreds of thousands annually, which places it in the same band as large enterprise platforms like Instabase rather than alongside self-serve, mid-market tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What's the real difference between a self-serve platform and a quote-only one?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A self-serve platform lets a team see pricing and run its own documents through a trial before talking to anyone, which surfaces real fit and real output fast. A quote-only platform requires a sales conversation first, which usually means a longer evaluation cycle and a price shaped around the vendor's enterprise pricing model rather than a transparent, published rate.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Does a mid-market team ever need an enterprise platform like Instabase or Hyperscience?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Sometimes. Teams processing genuinely complex, multi-document packets at high volume, with cross-document validation needs across thousands of files a month, are the actual use case these platforms are built for. A team extracting fields from a single document type, like invoices or bank statements, at a few hundred to a few thousand documents a month usually gets there faster and cheaper with a self-serve platform instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Can extracted data from a self-serve platform still be exported into existing finance tools?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;With Sensible specifically, yes. Results can be returned as an Excel or CSV file directly, either by calling the API with multiple extraction IDs or by selecting documents in the Sensible app and downloading them as Excel, without requiring a custom export script.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>documentation</category>
      <category>automation</category>
    </item>
    <item>
      <title>Top Ocrolus Competitors for Lenders and Fintech Teams in 2026</title>
      <dc:creator>Smartdocs</dc:creator>
      <pubDate>Thu, 22 Oct 2026 06:53:45 +0000</pubDate>
      <link>https://dev.to/smartdocs198/top-ocrolus-competitors-for-lenders-and-fintech-teams-in-2026-511h</link>
      <guid>https://dev.to/smartdocs198/top-ocrolus-competitors-for-lenders-and-fintech-teams-in-2026-511h</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Ocrolus built its platform specifically for lending, with bank statement and pay stub extraction backed by mandatory human review for accuracy. That review step is also what makes its cost and turnaround time variable, since pricing depends on how much human validation a document needs. This piece covers five platforms lending and fintech teams compare against Ocrolus, including Sensible, and what makes each one a fit or a mismatch for a lending document workflow specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Getting Started: What Ocrolus Does and Who It Serves&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Ocrolus describes itself as an AI workflow and analytics platform built for lenders, with document understanding specialized for bank statements, pay stubs, and tax documents, according to &lt;a href="https://www.ocrolus.com/" rel="noopener noreferrer"&gt;Ocrolus's own site&lt;/a&gt;. Its product line goes beyond extraction into fraud detection, cash flow analysis, and automated underwriting conditions, and its customer logos include PayPal, SoFi, Square, and Lendio, spanning small business lending, mortgage, auto finance, and consumer lending.&lt;/p&gt;

&lt;p&gt;The part of Ocrolus that shapes how it fits (or doesn't) into a given team's workflow is its extraction method. Per &lt;a href="https://www.sensible.so/sensible-vs-ocrolus" rel="noopener noreferrer"&gt;Sensible's comparison of the two platforms&lt;/a&gt;, Ocrolus uses AI models paired with mandatory human-in-the-loop validation, and lets users choose between an "Instant" mode (faster, lower accuracy) or a "Complete" mode (slower, with human review). That human review step is also why Ocrolus publishes no pricing page: the comparison page describes its pricing as variable, depending on processing type and how much human validation a given document needs.&lt;/p&gt;

&lt;p&gt;The running example for this piece is the underwriting team at a 45-person online consumer lender. They were running pay stubs and bank statements through Ocrolus's Complete mode to get the accuracy their credit decisions needed, and found both their monthly bill and their turnaround time moved around more than they could plan against. That unpredictability is what sent them looking at the alternatives below.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;1. Sensible: Best for Lenders Who Want Predictable Cost Without Giving Up Accuracy&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://sensible.so/" rel="noopener noreferrer"&gt;Sensible&lt;/a&gt; is a document extraction platform where SenseML, its configuration language, lets each field in a document type use either a layout-based rule, for formats that are predictable, or an LLM-based method, for ones that vary, with both returning the same schema. A team can see and version that configuration through Sensible's API, rather than depending on a human reviewer to catch what an AI model missed on every document that passes through.&lt;/p&gt;

&lt;p&gt;That difference in approach drives the two practical gaps the comparison page draws between Ocrolus and Sensible. First, cost and turnaround time: Sensible's &lt;a href="https://sensible.so/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; lists two self-serve tiers, Growth at $499amonth($449 billed annually) for 750 documents, and Scale at $1,499amonth($1,349 billed annually) for 3,200 documents, each with a flat per-document overage rate beyond that. There's no human-review surcharge that moves the bill depending on which documents needed extra validation. Second, speed to a working integration: every paid plan includes a 14-day free trial with full access to extraction, validation, and the configuration editor, no credit card required, so a lending team can run its own bank statements and pay stubs through the platform and see real output before committing to anything.&lt;/p&gt;

&lt;p&gt;For bank statements specifically, Sensible runs layout-based configurations for formats it has already mapped, plus a general LLM-based configuration that handles anything unmapped, with both returning the same output schema either way. Validation rules, like confirming that line items sum to a stated subtotal, get written once against the schema's field names and flag any document that fails them, instead of routing every document through a human reviewer by default. Lending and fintech teams that don't want to own the configuration work can hand it to Sensible's managed services team, who build the extraction configuration from a team's own sample documents and adjust it when a lender or issuer changes its statement format, on the same underlying engine available to self-serve customers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcm96y7xqz01uigf24lrh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcm96y7xqz01uigf24lrh.png" alt="Sensible pricing" width="512" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Docsumo: Best for Credit and Underwriting Teams Wanting a Guided Setup&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docsumo.com/" rel="noopener noreferrer"&gt;Docsumo&lt;/a&gt; has built out dedicated lending coverage, with a solution aimed specifically at "credit, underwriting and loan operations teams." Its bank statement extraction is built to produce income and cash-flow figures ready for a credit memo, reconciling deposits against stated income, and it supports pay stubs, W-2s, 1099s, and both business and personal tax returns, according to &lt;a href="https://www.docsumo.com/" rel="noopener noreferrer"&gt;Docsumo's own site&lt;/a&gt;. One of its lending customers, Grid Finance, reports extraction accuracy above 94% and under a minute of processing time per complex bank statement.&lt;/p&gt;

&lt;p&gt;Docsumo's trial covers up to 1,000 free pages over 14 days with ten user licenses, but its paid Business and Enterprise tiers are both custom quotes rather than published prices, per &lt;a href="https://docsumo.com/pricing" rel="noopener noreferrer"&gt;Docsumo's pricing page&lt;/a&gt;, and the page notes that setup fees apply on top of the subscription, scaled to document complexity and support level. That structure fits a lending team that wants a vendor-led implementation with lending-specific document models already built, and is a less direct fit for a team that wants to see a real monthly number before a sales conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. Nanonets: Best for Fintech Teams That Want Metered, Pay-as-You-Go Pricing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://nanonets.com/" rel="noopener noreferrer"&gt;Nanonets&lt;/a&gt; is the other genuinely self-serve option on this list. Its Starter tier is free to begin, moving to \$100 a month for 100 credits afterward, with signup requiring no card and \$50 in starting credits, per &lt;a href="https://nanonets.com/pricing" rel="noopener noreferrer"&gt;Nanonets' pricing page&lt;/a&gt;. Usage is metered per workflow step, called a block, at roughly \$0.02 for a simple block up to \$0.30 for a complex AI block, which the page estimates lands under \$2 per document for a typical extraction workflow.&lt;/p&gt;

&lt;p&gt;Nanonets isn't built specifically for lending the way Ocrolus or Docsumo are, so a fintech team gets a general-purpose extraction engine rather than pre-built underwriting logic. What it offers instead is a close match to Sensible's self-serve model: a team can start small, see real per-document costs immediately, and scale usage without renegotiating a contract at every volume tier. Its Growth and Enterprise tiers above Starter move to volume-based quotes and add classification, custom integrations, and compliance features like SSO and HIPAA support.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4h0g6iv10t8alur7zq6g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4h0g6iv10t8alur7zq6g.png" alt="Price leadings" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4. Hyperscience: Best for Large, Regulated Lenders With High Document Volume&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.hyperscience.com/" rel="noopener noreferrer"&gt;Hyperscience&lt;/a&gt; sits in a different pricing band entirely. Its entry tier starts at \$50,000 a year, with enterprise deployments running roughly €50,000 to €300,000 a year, according to &lt;a href="https://erpresearch.com/erp-add-ons/ocr/hyperscience/pricing" rel="noopener noreferrer"&gt;erpresearch's pricing breakdown&lt;/a&gt;, and there's no free trial. That places it alongside large enterprise intelligent-document-processing platforms rather than self-serve tools, and its machine-learning approach is built to pay off for organizations with enough document variety and volume, think a bank or large regulated lender processing loan packets across multiple product lines, to justify the investment.&lt;/p&gt;

&lt;p&gt;A mid-market or growth-stage fintech team evaluating Hyperscience against Ocrolus is likely running into a scale mismatch before it runs into a feature gap. Both platforms are built for lenders with established, high-volume document operations, which makes Hyperscience a reasonable alternative for a large bank's lending division and a poor fit for a team the size of the underwriting group in this piece's running example.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. ABBYY Vantage: Best for Enterprise Lenders Already Standardized on RPA&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.abbyy.com/vantage/" rel="noopener noreferrer"&gt;ABBYY Vantage&lt;/a&gt; is a low-code enterprise intelligent document processing platform built around a Skill Designer and a marketplace of pre-trained document models, with connectors for RPA tools, per &lt;a href="https://www.sensible.so/sensible-vs-abbyy" rel="noopener noreferrer"&gt;Sensible's comparison page&lt;/a&gt;. ABBYY says Vantage is ready to use within minutes for financial services documents including bank statements, utility bills, tax forms, and the broader set of banking documents tied to lending, and it also covers KYC document routing for onboarding.&lt;/p&gt;

&lt;p&gt;Vantage requires a sales conversation for pricing and a guided proof-of-concept engagement before a team can fully evaluate it, with no self-serve trial or published rate. That consulting-led setup tends to fit an enterprise lender that already runs RPA tooling and wants document extraction wired into that existing orchestration layer, more than a fintech team trying to compare real costs and get a trial running the same week.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where Should You Start&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69sylqp3r66chfhgg6ow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69sylqp3r66chfhgg6ow.png" alt="Sensible Data" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The split across these five platforms isn't really about which one can extract a field from a bank statement, most of them can, on a clean sample during a demo. It's about whether a lending or fintech team can see real pricing and get its own documents running through a trial before committing to a process built for Ocrolus's enterprise lending customers. Sensible and Nanonets publish self-serve pricing and let a team start the same day; Docsumo, Hyperscience, and ABBYY Vantage all require a quote or a guided engagement first, and Hyperscience's entry price alone puts it in a different budget tier than a growth-stage fintech is likely working with.&lt;/p&gt;

&lt;p&gt;A team currently weighing Ocrolus's variable, human-review-driven pricing against its own document volume is a good candidate to test a flat-rate, self-serve platform directly against its own bank statements and pay stubs before signing anything. &lt;a href="https://sensible.so/pricing" rel="noopener noreferrer"&gt;Sensible's 14-day free trial&lt;/a&gt; requires no credit card and gives full access to extraction, validation, and the configuration editor, which is the fastest way to find out whether a lighter platform covers what a lending workflow actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;FAQs&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What makes Ocrolus different from a general document extraction platform?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Ocrolus combines AI-based extraction with mandatory human-in-the-loop validation and lending-specific products like fraud detection, cash flow analysis, and automated underwriting conditions. That combination is built specifically for lending workflows, which is also why its pricing and turnaround time vary by how much human review a document needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Why doesn't Ocrolus publish pricing?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Per Sensible's comparison of the two platforms, Ocrolus's cost depends on which processing mode a document runs through and how much human validation it requires, which makes a flat published rate difficult to offer. The only way to get a number is through a sales conversation.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Which Ocrolus alternatives let a lending team see pricing without a sales call?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Sensible and Nanonets both publish self-serve pricing tiers a team can sign up for directly. Sensible's Growth and Scale plans start at \$499 and \$1,499 a month respectively, each with a 14-day free trial and no card required, and Nanonets' Starter tier is free to begin with pay-as-you-go credits afterward.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Is Hyperscience or ABBYY Vantage a realistic option for a smaller fintech team?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Not based on their published pricing and sales process. Hyperscience's entry tier starts at \$50,000 a year with no free trial, and ABBYY Vantage requires a guided proof-of-concept engagement before pricing is even discussed. Both are built for large, established lenders rather than growth-stage teams comparing options on their own.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Does mandatory human review make extraction more accurate?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Human review can catch errors an AI model misses, which is part of why Ocrolus requires it. The tradeoff is cost and turnaround time that move with document volume and complexity. Platforms that pair deterministic rules with LLM-based methods for the fields that need them, and let a team write validation rules against the extracted values, aim to reach similar reliability without a human reviewer on every document by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Can extracted bank statement or pay stub data be exported into underwriting or loan origination systems?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;With Sensible specifically, yes. Results can be returned as an Excel or CSV file directly, either by calling the API with multiple extraction IDs or by selecting documents in the Sensible app and downloading them as Excel, without requiring a custom export script.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Do any of these platforms offer the fraud detection features Ocrolus provides?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Not as a direct, documented equivalent. Ocrolus's fraud detection and document authenticity checks are a distinct product built on top of its lending-specific document models. The platforms covered here compete primarily on extraction accuracy, pricing transparency, and setup speed rather than on matching that specific fraud-detection capability.&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>ai</category>
      <category>datastructures</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Convert Multi-Page Credit Card Statements to Excel Without Manual Entry</title>
      <dc:creator>Smartdocs</dc:creator>
      <pubDate>Mon, 19 Oct 2026 12:32:28 +0000</pubDate>
      <link>https://dev.to/smartdocs198/how-to-convert-multi-page-credit-card-statements-to-excel-without-manual-entry-527e</link>
      <guid>https://dev.to/smartdocs198/how-to-convert-multi-page-credit-card-statements-to-excel-without-manual-entry-527e</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A multi-page credit card statement is hard to convert to Excel because transactions, running balances, and account context split across page breaks, and every issuer formats that split differently. Manual entry is the default fallback, and it doesn't scale past a handful of statements a week before errors creep in. This guide covers why page breaks are the real technical problem, what a correctly converted statement actually needs to include, and how to get a reliable extraction running without re-keying every line by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Getting Started: What Multi-Page Statement Conversion Actually Involves&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A credit card statement is a PDF that lists an account's transactions for a billing cycle, along with a running balance, fees, interest, and payment history. Converting it to Excel means turning that PDF into rows and columns a spreadsheet can sort, filter, and total, without a person retyping each line.&lt;/p&gt;

&lt;p&gt;For a single-page statement, this is close to a solved problem. Most PDF tools can pull a table off one page reasonably well. The difficulty shows up specifically with multi-page statements, where a table of transactions continues from the bottom of one page to the top of the next, and the second page usually has no header row, no account number repeated, and no obvious signal that it's a continuation rather than a new table. A tool built to read one page at a time treats each page as its own document, and the moment it does that, the statement breaks apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why the Page Break Is the Real Technical Problem&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Three things specifically go wrong at a page boundary, and any conversion method eventually has to handle all three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Running balances lose their anchor.&lt;/strong&gt; The balance column depends on every prior row. If a tool re-starts extraction on page two without knowing the closing balance from page one, the numbers it produces are disconnected from the actual account state.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuer formats vary in how they signal continuation.&lt;/strong&gt; Some issuers repeat the account number and a "continued" label on every page. Others just keep listing transactions with no header at all. A method tuned to one issuer's layout often fails silently on another's.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-account statements mix data mid-document.&lt;/strong&gt; A business card statement covering several cardholders can switch from one account's transactions to another's partway down a page, with no page break to signal the change. A method that only watches for page boundaries misses this entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regional issuers change their statement layout more often than the big national ones, since they tend to run on smaller design teams and update templates without much notice. Any extraction approach that hardcodes a single layout eventually meets a statement it wasn't built for.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What People Try Before Finding a Reliable Method&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Manual entry is where most teams start, and it works fine at low volume. Someone opens the PDF, reads each transaction, and types it into a spreadsheet. It's slow, and the error rate climbs with document length. A twelve-page statement with three hundred transactions is a different task than a one-page statement with fifteen, even though both get typed the same way.&lt;/p&gt;

&lt;p&gt;Bank-provided CSV or QFX exports are the next thing people reach for, and they solve the problem when they're available. The catch is availability itself. Not every card issuer offers a downloadable export, and when a company processes statements from customers or vendors rather than its own accounts, there's usually no login to request an export from in the first place.&lt;/p&gt;

&lt;p&gt;Generic PDF-to-Excel converters are the most common next attempt, and they're where the page-break problem shows up most visibly. These tools are built to detect tables on a page, and they do that reasonably well for a single page. Feed them a twelve-page statement and they return twelve separate, disconnected tables, with no shared running balance and no way to know that row 40 on page 3 continues row 38 on page 2. Someone still has to stitch the pieces together by hand, which is most of the original manual work, just with extra steps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fte703bmc7714z3uhodpd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fte703bmc7714z3uhodpd.png" alt="Sensible" width="512" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where This Breaks Down at Real-World Volume&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A regional credit union's lending team processes around fifty statements a week as part of income and debt verification for loan applications. At that volume, someone on the team was spending most of a workday just on statement conversion, opening each PDF, checking whether the transaction table continued past page one, and manually reconciling the running balance whenever it did.&lt;/p&gt;

&lt;p&gt;The team tried a generic conversion tool to speed this up. It worked for statements from the two largest national issuers they saw most often, both of which repeat the account number and a running balance on every page. It failed consistently on regional and community bank statements, where the second page of a transaction table often had no repeated header at all. The tool would return the first page's transactions correctly and then either drop the second page's rows or merge them into the wrong table, and there was no way to tell which had happened without manually checking every output against the source PDF. That manual check ate back most of the time the tool was supposed to save.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What a Properly Converted Statement Has to Include&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Whatever method does the extraction, the output needs to capture the statement at three levels, or a spreadsheet built from it will be missing the context that makes it usable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Account-level data&lt;/strong&gt;: card number (masked), statement period, credit limit, and both the opening and closing balance for the cycle.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction-level data&lt;/strong&gt;: date, merchant or description, amount, and category if the issuer provides one, with every row correctly assigned to its account even when a statement covers multiple cards.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statement-level data&lt;/strong&gt;: total fees, interest charged, minimum payment due, and payment due date, since these numbers usually live in a summary block that's easy for a page-by-page tool to miss entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A conversion that gets the transaction rows right but drops the statement-level summary still leaves someone opening the original PDF to check the due date and minimum payment, which defeats much of the point.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff7dqvmha4hsm4peuj5fd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff7dqvmha4hsm4peuj5fd.png" alt="Sensible" width="512" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How Sensible Extracts Multi-Page Statements Without Manual Entry&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Sensible is a document extraction platform built to handle exactly the structural problems described above, rather than treating each page as an isolated table. It combines deterministic, layout-based extraction for the parts of a statement that follow a predictable structure with LLM-based extraction for the parts that vary, and both run through the same schema-validated output.&lt;/p&gt;

&lt;p&gt;For the page-break problem specifically, Sensible includes table-continuation logic that tracks a transaction table across page boundaries rather than resetting at each new page. When a table's rows stop at the bottom of page one and pick up at the top of page two with no repeated header, Sensible recognizes the continuation and carries the running balance forward, instead of treating page two as an unrelated block of text. This directly addresses the running-balance and missing-continuation-signal problems described earlier.&lt;/p&gt;

&lt;p&gt;For the issuer-variation problem, Sensible ships with prebuilt configurations for &lt;code&gt;credit_card_statements&lt;/code&gt; and &lt;code&gt;bank_statements&lt;/code&gt; that already account for common layout differences across issuers, and per-issuer configuration matching lets a team refine extraction further for a specific bank's format without rebuilding the whole pipeline from scratch. Configurations are written in SenseML, a declarative language managed through the API with its own versioning, so a fix for one issuer's layout doesn't have to be re-derived by hand every time that issuer tweaks their template.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3iggiqrs86r8tpjqdqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3iggiqrs86r8tpjqdqd.png" alt="Sensible" width="512" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the multi-account problem, Sensible's multi-account statement splitting detects where one account's transactions end and another's begin within a single document, even when that split happens mid-page rather than at a page boundary, and routes each account's rows to their own output block accordingly.&lt;/p&gt;

&lt;p&gt;Every extracted field carries a source-coordinate audit trail back to the original PDF, visible in the Sensible app, so a reviewer can check a specific number against the source document in seconds instead of re-reading the whole statement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo338ffi781o0wf8wjdou.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo338ffi781o0wf8wjdou.png" alt="Sensible" width="512" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Getting a First Extraction Running&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Getting started doesn't require writing a configuration from zero. Sensible's prebuilt &lt;code&gt;credit_card_statements&lt;/code&gt; configuration handles common issuer layouts out of the box, so the first step is usually just sending a sample statement through the API or the Sensible app and checking the output against the source PDF. From there, a team can promote the configuration from a development environment to production once it's been tested against a representative sample of the statements they actually receive, and refine it further for any issuer whose layout the prebuilt configuration doesn't fully cover. Output comes back through a REST API or the Python and Node SDKs, in a schema that maps directly onto spreadsheet columns, so building the Excel file from Sensible's output is a formatting step rather than a data-cleaning one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where Should You Start&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The core problem with multi-page credit card statements isn't extracting text from a PDF; most tools can do that much. It's that a transaction table, a running balance, and an account's identity all have to survive the transition from one page to the next, and that's exactly where naive, page-by-page tools fall apart. A method that treats a statement as one continuous document, rather than a stack of independent pages, closes that gap.&lt;/p&gt;

&lt;p&gt;If statement conversion is currently eating hours of manual review every week, the fastest way to see whether this fixes it is to run one of the statements that's been causing the most trouble through Sensible's &lt;a href="https://sensible.so/" rel="noopener noreferrer"&gt;credit card statement extraction&lt;/a&gt; and compare the output directly against the source PDF. A 14-day free trial is available with no credit card required, which is enough to test against a real, messy sample rather than a clean demo file.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;FAQs&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Why do generic PDF-to-Excel tools fail on multi-page credit card statements specifically?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Generic tools typically process a PDF one page at a time and look for a table on each page independently. When a transaction table continues from one page to the next without a repeated header or account number, the tool has no signal that the two pages belong to the same table, so it returns them as separate, disconnected outputs instead of one continuous set of rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Does every card issuer format multi-page statements the same way?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No. Some issuers repeat the account number, statement period, and running balance at the top of every page. Others list transactions with no repeated header at all once the table continues past the first page. Regional and community banks change their statement templates more often than large national issuers, which makes a method tuned to only one layout unreliable across a mixed set of statements.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What data does a converted statement need beyond the transaction list?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A complete conversion needs account-level data (statement period, credit limit, opening and closing balance), transaction-level data (date, merchant, amount, category), and statement-level data (fees, interest charged, minimum payment, and due date). Tools that only capture the transaction rows typically miss the statement-level summary, which usually sits in a separate block that page-by-page extraction methods overlook.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Can a single statement contain more than one account's transactions?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Yes, this is common on business or shared card statements covering multiple cardholders. The transactions for different accounts can switch mid-page with no page break to mark the change, which means a method that only tracks page boundaries can merge two accounts' transactions into one table by mistake.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How is a running balance affected by a page break?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A running balance depends on every transaction before it in the statement. If an extraction method resets at each new page instead of carrying the balance forward, the numbers on page two are calculated with no reference to page one's closing balance, and the output balance column stops matching the actual account state.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Is bank-provided CSV or QFX export a reliable alternative to PDF conversion?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;It's a reliable option when it's available, but availability varies by issuer, and it isn't an option at all for a company processing statements it doesn't have login access to, such as statements submitted by customers or loan applicants rather than the company's own accounts.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How long does it take to get a first statement extraction working?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;With a prebuilt configuration for common statement layouts, the first extraction is typically a matter of sending a sample statement through and checking the output against the source PDF, rather than building a layout from scratch. Refining accuracy for a specific issuer's format on top of that usually takes longer, depending on how much the layout diverges from the common template.&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>automation</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
